CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to agent-bom are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versions follow [Semantic Versioning](https://semver.org/).
---
## [Unreleased]
### Upgrade notes
- Parquet and Iceberg exports move to schema version 6, which appends `malicious_basis` after `sla_due_at_source`. 0.114.0 wrote that column at position 11 under version-5 metadata, which moved every later column; consumers that read 0.114.0 files by position should re-read them by name. Iceberg tables keep their field IDs and gain the column at the end.
- Ollama model agents are named with the full `name:tag` (`ollama-model-llama3:8b` instead of `ollama-model-llama3`), so two tags of one model on one host are two assets. Their asset identities change once.
### Added
- `--serving-endpoint RUNTIME=URL` (repeatable) reads one vLLM, Text Generation Inference, Triton, llama.cpp or LM Studio endpoint with that runtime's documented read-only request. Each endpoint reports success, partial, denied, timeout or unavailable; only success and partial produce records, and a failed endpoint never falls back to local files. Each model records runtime, endpoint, artifact and collection evidence separately in `metadata.serving_evidence`. See [AI infrastructure scanning](docs/AI_INFRASTRUCTURE_SCANNING.md).
- Deployment support tiers list, for each profile, what was validated locally and what has not been installed live. See the [deployment overview](site-docs/deployment/overview.md).
### Changed
- Standalone HTML reports put only the first page of vulnerabilities, policy findings, impact rows and remediation items in the page. The other records stay in the file as inert text, and the paginator loads them a page at a time. Search, filters, sorting and counts still cover every record, and printing loads them all. Without JavaScript, a notice says how many records are stored but not shown. A 2,000-finding report opens with about 16,000 page elements instead of 220,000 (#6128).
### Fixed
- An Ollama endpoint that answers 401, 403 or times out reads as denied or timed out, not unavailable, and one malformed model entry no longer discards the rest of the inventory; the result is marked partial.
- `GET /v1/graph` no longer returns `503 current_estate_building` on every request when all the evidence behind the current graph is older than `AGENT_BOM_GRAPH_RETENTION_DAYS`. Age retention was deleting the rebuildable current-estate view as soon as it was built. That view is now exempt from age retention and keeps its evidence time as `created_at` (#6228).
## [0.114.0] - 2026-11-08
### Highlights
- Every finding has a scan-driven lifecycle that outlasts scan retention: status, first and last seen, resolved and reopened dates, a `resolution_reason`, and a `stale` label for open findings not seen recently.
- AI triage is measured against a labelled dataset and appears beside each finding as labelled advice that never changes scanner severity; "Explain this path" writes each sentence from recorded graph edges.
- Container scan accuracy: end-of-life Debian releases read `partial` until local tracker data is loaded, multi-page OSV results are followed, kernel header packages are not matched against kernel advisories, and Go binaries and their standard library are looked up.
### Upgrade notes
- An MCP server with OAuth (`AGENT_BOM_MCP_OAUTH_*`) refuses to start until `AGENT_BOM_MCP_TENANT_ID`, `AGENT_BOM_MCP_OAUTH_INTROSPECTION_URL`, `AGENT_BOM_MCP_OAUTH_INTROSPECTION_CLIENT_ID` and the introspection client secret are set, and it rejects tokens without a signed `tenant_id` claim equal to that tenant. Configure the issuer first; see [MCP_OAUTH.md](docs/MCP_OAUTH.md#self-hosted-keycloak-setup).
- Enabling `controlPlane.ingress` in Helm requires `controlPlane.api.trustedProxy` (or `sharedSourceAccepted`); rendering fails without it.
- The `sqlite-pilot` and `synthetic-enterprise-story` Helm profiles require `AGENT_BOM_API_KEYS` in the `agent-bom-control-plane` Secret.
- The Helm teardown hook's Role grants `deletecollection`, drops `get` and `list`, and holds `delete` on Secrets only for the names it removes.
- Compose secret files are mode 0640 with your primary group and are read through `group_add`. Set `AGENT_BOM_SECRETS_GID` for a direct `docker compose up`.
- The compose files no longer set fixed `container_name` values. Scripts that address containers by the old names should use `docker compose` service names.
- Scans of end-of-life Debian releases read `partial` until `agent-bom db update --source debian` loads the Debian Security Tracker data.
- `/health` adds `sso_provider` (`okta`, `entra`, `google`, `generic` or null).
- Apply Postgres migrations through `20261106_01`. `20261104_01` and `20261105_01` backfill existing rows in bounded batches; `20261105_03` cannot be downgraded. See [0.114.0 upgrade guidance](docs/UPGRADING.md) for disk headroom, duration and rollback.
### Added
- Helm `controlPlane.logging.json` (default `false`) and `AGENT_BOM_LOG_JSON` in the fullstack, platform and pilot compose files (default `0`) switch the API and worker pods to JSON logs.
- AI triage is measured: a labelled dataset and `scripts/ai_triage_eval.py` score false-positive precision and recall, severe dismissals, calibration, invalid responses, injection resistance, latency and tokens per model, and CI replays recorded responses so a regression fails. Results are in `docs/AI_ENRICHMENT.md`.
- AI advice now appears with its finding: `ai_assessment` on `GET /v1/findings`, the findings page and drawer, CLI output, CSV, SARIF and HTML, stored per scan and tenant on every store. It is labelled as advice and never changes scanner severity. `GET /v1/ai/status` says when it is off or failed.
- "Explain this path" (`POST /v1/graph/explain`): on demand. The model selects recorded edges and nodes and the server writes each sentence from the record, so direction and reachability come from the graph. Evidence limits such as non-traversable hops are stated.
- Each AI call is recorded with hashes rather than prompt text: in the audit chain on the control plane, with `agent_bom_ai_calls_total` and latency metrics, and in a local chained log on the CLI (`AGENT_BOM_AI_LOCAL_AUDIT=0` opts out). On the control plane, output whose audit record cannot be written is discarded.
- Finding dates now outlast scan retention: `first_seen`, `last_seen`, resolved and reopened dates live in their own per-finding table, written in the same transaction as the scan. A resolved finding stays listed after its scans expire, and late, repeated or concurrent scans cannot move a finding's state incorrectly. The cleanup tick seeds existing dates from retained scans after upgrade, outside any read. Reads no longer rebuild findings for every retained scan.
- Every resolved finding states why: `resolution_reason` is `no_longer_detected` or `asset_deleted`. `asset_deleted` needs a complete AWS inventory covering the asset's account, region and service, with no warnings or missing permissions, from the same source and scope. Other sources never produce it; see `docs/FINDING_LIFECYCLE.md`.
- `stale` marks an open finding not observed within `AGENT_BOM_FINDING_STALE_DAYS` (default 30). It stays open and counted, can be filtered with `status=stale` on the API, `--status stale` on the CLI and the Status filter on the Findings page, and never resolves anything.
- `docs/PLUGIN_SUBMISSION.md` and `make plugin-submission-check`, which stops a directory submission that would ship stale pins or draft legal pages. It stays red until the next release.
- Review cases for each plugin directory in `docs/PLUGIN_REVIEW_CASES.md`, with machine-readable copies, and negative eval cases for over-triggering, chat history requests and live server launches.
### Fixed
- Shared request rate limits on Postgres no longer queue in the database during a large ingest. A hit made three or four round trips, each waiting for CPU-bound ingest and graph threads to release the interpreter, and held its row locks across those waits, so other hits on the same bucket waited in the database (25 statements waited 0.4 to 0.8 s in a measured run; none after). A hit is now one pipelined round trip on a small admission pool shared with authentication state (at most four connections per process), takes no lock another hit waits on, carries 400 ms database bounds, and a request waits up to two seconds. A store outage still falls back to process-local limits. Under sustained CPU-bound load in the API process the fallback can still occur, because the request itself waits for the interpreter.
- Authentication state queries on Postgres bind every value as a parameter and pipeline their transaction, 400 ms bounds and query in one round trip, instead of sending several statements as one string.
- Wrong keys no longer get 503 instead of 401 when several arrive at once on SQLite. Failure writers competed for the authentication sidecar through SQLite's busy handler, which polls with growing sleeps, so a writer could lose until its 400 ms bound even when the total work fit inside it. Writers in a process now queue on one lock for the same bound.
- Graph and findings reads stay fast on large estates. Effective-reach scoring walked every graph edge once per vulnerability, server and advisory row; on a 5,000-agent estate that background work held the CPU for minutes after each ingest, so the first `/v1/graph` read took 99 s and later reads were refused with 429. It now walks the edges once, with identical scores. Graph snapshot statistics (node, edge and relationship counts) are computed once per committed snapshot revision instead of on every page, on SQLite and Postgres. Measured locally at 5,000 agents: graph read p95 fell from 99 s to 2.2 s (first read after ingest 99 s to 21 s) and findings reads from 2.0 s to 32 ms; responses are unchanged.
- Dashboard text and form fields now meet WCAG AA contrast in both themes. Page lane labels and scope chips used fixed dark-theme colours, so in light mode "Posture" read at 2.34:1 and "Governance" at 2.35:1; they now use theme tokens and read at 4.5:1 or better. Filled actions use the accent text token instead of white (3.65:1 before on the Security graph), the focused "Skip to content" link reads at 9.8:1 in dark mode instead of 1.92:1, and status chips use the status tokens. Form fields now draw their border with `--border-strong` (about 1.4:1 before, 3:1 or better now). Light `--status-success`, `--status-warn` and `--border-strong` and dark `--status-danger` were adjusted so each clears its threshold on every surface, and a test checks these token pairs in both themes.
- Valid API keys no longer get 503 "Authentication protection unavailable" while a large ingest or graph read runs on Postgres. That work keeps the API's own threads busy, and each of the six round trips in an admission check had to wait for them, so checks missed their 500 ms deadline. Authentication and rate-limit state now share their own pool of at most four connections per process, a check is one round trip with the same 400 ms database bounds, and a caller waits up to two seconds for local scheduling. A store error or an outage still fails closed. In a 5,000-agent ingest on Postgres, 4 of 1,490 valid-key checks got 503 before and none after.
- A scan of an end-of-life Debian release without local Debian Security Tracker data no longer reports `complete`. After end of life, OSV's per-CVE Debian records stop listing the release and a live query returns only DSA/DLA advisories, so fixes shipped without one were missed. The scan is now `partial` with a `vulnerability_coverage_gap` that names `agent-bom db update --source debian`. On `nginx:1.21.6` (Debian 11) from a clean install: 195 fixed CVEs in 53 packages, outcome `partial`; after `db update --source debian`: 320 fixed CVEs in 63 packages, outcome `complete`.
- OSV results that span several pages are now fetched. A query with more than about 1,000 advisories returns a `next_page_token`; the scan treated it as an invalid response and marked every package in the batch as failed. Each paginated query now follows its token within firm bounds of 10 extra pages and 60 seconds per batch. A package cut off by either bound keeps the advisories read so far, is not cached, and gets an `osv_result_page_limit` coverage gap naming it, so the scan is `partial`.
- Kernel header and UAPI packages (`linux-libc-dev`, `linux-headers-*`, RPM `kernel-headers` and `kernel-devel`, Alpine `linux-headers`) are no longer matched against kernel advisories. A container runs on the host kernel, so those CVEs are not exposure for the image and buried its real findings. The packages stay in the inventory and one scan note per scan gives their count and the reason; real kernel packages (`linux-image-*`, `kernel`) are still matched. `redis:7.0.11` from a clean install: 8,161 CVEs, 198 s and 722 MB peak memory before; 290 CVEs, 23 s and 229 MB after, with outcome `complete`.
- Go modules found in container image binaries are now looked up. They were tagged ecosystem `golang`, which the OSV query skipped as unknown; they are now `go`, like every other Go source. The Go release that built the binary is read from its build information (Go 1.18 and later) and scanned as the `stdlib` package, so standard-library CVEs are reported.
- Skill and instruction-file package extraction stops at shell operators. `&&`, `||`, `;`, `|`, `&`, redirects such as `> out.txt` and `2>&1`, command substitutions and environment assignments no longer become packages, so the skill audit no longer reports "Unknown package: '&&'", and `pip install foo; echo done` reports `foo` rather than `echo`.
- Install commands in skill and instruction files are now parsed per command. Options that take a value consume it, so `pip install -r requirements.txt` no longer reports a package named `requirements.txt` or an "Unknown package" finding; the file is listed under the new per-file `manifest_references` in `skills scan` JSON. Every install on a line is read (`pip install a || pip install b` reports both). `npm i`/`add`, `yarn add`, `pnpm add`, `uv add`, `uv pip install` and `pipx install` are recognised, with scoped npm names and `name@version`; local paths, git and URL specs are not packages. Jupyter `%pip install` lines use the same parser, so a flag no longer swallows the next package and packages whose names start with `http` (`httpx`) are no longer dropped.
- `agent-bom image --help` and the image docs now describe where an image is read from. In `auto` mode a reference with no registry host (`nginx:latest`) that the local Docker daemon already holds is scanned from the daemon, so it never resolves to a same-named Docker Hub image; the help previously said the daemon was used only with `--pull-mode daemon` or after a failed registry pull. The `linux/amd64` default applies to registry pulls; without `--platform` the daemon path scans the daemon's stored copy, usually the host's architecture.
- Placeholder examples in Python docstrings are no longer reported as secrets: a connection string whose password is a plain word or whose host is a reserved documentation name (`user:pass@cluster.example.com`), or an assignment whose value is a plain word or phrase (`SECRET_KEY = 'development key'`). The same values in executable code, and values with digits, mixed case or symbols inside a docstring, still report.
- Personal data found in translation catalogs (files under `i18n/`, `locale/`, `locales/`, `l10n/`, `lang/` or `translations/`) is reported at `low` with a `downgrade_reason`, since those strings are usually UI text or sample values.
- A fresh-install scan of a Debian image no longer reports every distro advisory as `unknown`. An unrated `DSA-`/`DLA-` advisory now takes the strongest rating among the CVEs it links, looked up from `api.osv.dev` in bounded, cached batches. On `nginx:1.21.6`, unknown advisories fell from 81 of 81 to 7 of 81; the other 7 link no CVE. Offline, or when a lookup fails, the finding stays `unknown`, the scan says so and no grade is given.
- `severity_source`, `severity_linked_cve` and `severity_note` state which CVE supplied a severity and why one is partial or absent, in JSON, SARIF, CycloneDX, the API, the CLI and the Findings drawer. `severity_basis` gains the value `linked_cve`. A linked score does not count when the Debian tracker marks that CVE fixed or not affected for the package and release.
- Collapsing a Debian rollup into its CVE records no longer drops the rollup's severity, CVSS score or KEV status.
- The README, PyPI page, first-run guide and docs quick start no longer pass `--no-discover`, which silently turned off the project's secret, personal-data, skill, dataset and training-pipeline scanning. `agent-bom scan .` already stays inside the repository; the docs now say what it covers.
- API memory sizing no longer claims a push costs about 8x its body. The chart values, sizing guide and Helm guide carry peaks measured by `scripts/qualification/performance.py`: 730 MB (SQLite) and 789 MB (Postgres) at 1,000 agents, 2.4 GB and 4.1 GB at 5,000 agents (18 MB body, 116,907 package instances), where a 2Gi pod was OOM-killed. The `2Gi` default fits about 1,000 agents per push; set at least `3Gi` on SQLite or `5Gi` on Postgres for about 5,000, or split pushes.
- A result where every vulnerability finding is `unknown` now grades `N/A` instead of a letter. Mixed results state how many findings have no severity.
- `agent-bom scan .` in a repository no longer prints "-1 agents". The console headline subtracted projects from an agent count that already excluded them; it now shows the report's own agent count and the projects it left out.
- Hints that said "use --verbose" now name a flag the running command accepts. `image`, `fs`, `iac`, `sbom` and `cloud snowflake|databricks|huggingface|ollama|posture` gain `--verbose`/`-v`, passed through to the scan. Commands without it are pointed at `AGENT_BOM_LOG_LEVEL=DEBUG`, which also prints the traceback for an unexpected error.
- Under `--log-json` (or `AGENT_BOM_LOG_JSON=1`), `agent-bom api` and `agent-bom serve` write only JSON lines, including the startup summary, startup notices, Uvicorn server messages and the access log, in single and multi-worker mode. The access log carries method, path without its query, and status as fields. `AGENT_BOM_LOG_JSON` was ignored by every command with a `--log-json` flag; it is now honoured.
- A healthy single-process local start no longer warns about the per-process rate-limit key or audit HMAC key; both are logged at info. They still warn in production or clustered mode, and the audit key still warns when a durable audit store is configured.
- The compose files no longer pin `container_name`, so a second copy of a stack can run under another project name; services already reach each other by service name. Host ports still need to differ.
### Changed
- **Behaviour change:** `skills scan` exits 2 and reports `scan_failed` when it could not scan a file, rather than exiting 0.
- **Behaviour change:** skill audit context rules are stricter: more skill files that previously read as documentation now report at full severity.
- **Behaviour change:** only a presented credential that failed verification now counts toward authentication backoff, marked by the handler that verified it. An expired session, a missing credential, a logged-out `/v1/auth/me` and a valid key missing a report token no longer lock out a source.
- **Behaviour change:** enabling `controlPlane.ingress` in Helm now fails rendering unless `controlPlane.api.trustedProxy` declares the proxy, or `sharedSourceAccepted` records that all clients share one authentication failure budget. The hosted Compose overlay sets the trusted proxy settings, and the API warns once when a peer is outside the configured networks, when only `X-Real-IP` arrives, and on a unix socket.
- **Behaviour change:** Compose secret files are now mode 0640 with your primary group, read by services through `group_add`, so `make platform-up` works on Linux without world-readable secrets. For a direct `docker compose up`, set `AGENT_BOM_SECRETS_GID`.
- **Behaviour change:** audit integrity over its entry budget now reports `verified: null`, `status: not_verified_over_budget`, `total_entries` and `verification_limit`, in the API, the export and the dashboard, instead of zero verified.
- **Behaviour change:** `/`, `/version`, `/docs`, `/redoc` and `/openapi.json` no longer depend on authentication state, so a source's backoff or an auth-store outage does not refuse them. See `docs/RUNTIME_FAIL_MODES.md`.
- **Behaviour change:** a typosquat or dependency-confusion match now reads as suspected, not as malware. `check` and MCP report `suspected_malicious` in `verdict`, `canonical_verdict` and `status`, and say what the flag rests on and how to confirm it. It still exits `1` and `is_malicious` stays `true`. A new `malicious_basis` field (`known_malicious`, `name_similarity`, `dependency_confusion`, `unspecified`) appears in check results, reports, SARIF, CycloneDX, SPDX, CSV and Parquet. Findings, remediation text, alerts and the console use the same wording. A script that matches only `verdict == "malicious"` should also match `suspected_malicious`.
- `agent-bom fs` on a directory with no packages or evaluated IaC no longer grades `A`: `posture_grade` is `N/A`, `scan_run.outcome` is `partial` and a `nothing_to_scan` issue names the directory. It still exits `0`. Any scan carrier with no packages, such as an image or SBOM, also grades `N/A`.
- `agent-bom fs <dir>` auto-detects IaC in that directory rather than in the working directory.
- **Behaviour change:** an API scan whose every requested target failed, such as an unreachable image, now ends as job `status: failed` with `error` set and `result.status: failed`, instead of `done` and `no_agents_found`.
- **Behaviour change:** NVIDIA CSAF bulletins apply to a PyPI package only when a CVE's own product identifies that package, and the fix comes from that product's entry. `torch`, `torchvision`, `jax`, `tritonclient`, `openai` and other packages that bundle or call a product no longer receive these findings, and `Upgrade torch to 26.07` no longer appears.
- **Behaviour change:** `agent-bom mcp audit` and every scan now report what a configured launch recipe says: `remote_code_launcher` for a launcher that fetches and runs code, `plaintext_secret` for a literal secret in `env`, and `unscoped_filesystem` for a filesystem server rooted at the host root. Each is high or critical, so the audit exits 1. Blocked servers are included, and commands are read, never run. See `docs/MCP_AUDIT.md`.
- **Behaviour change:** notebooks and CI workflows are no longer counted as agents or MCP servers. They have their own `notebook` and `ci-workflow` surfaces. A repository with no MCP configuration and no agent code reports 0 agents and 0 MCP servers, draws as a repository in the graph, and raises no agent-reaches-credential findings. Packages and CVEs are unchanged; findings on those packages now carry source `SBOM` and a package asset instead of `MCP_SCAN` and an MCP server asset. Saved reports keep the counts they were written with.
- **Behaviour change:** secret scanning no longer reports an assignment from a function call, variable or environment lookup as a hardcoded secret, or translated UI labels as passwords. Personal-data patterns no longer match inside URLs or asset paths.
- **Behaviour change:** an AI dismissal is held as `needs_review` when the recorded fix is on a different release line from the installed version. On the recorded held-out set, `qwen2.5:3b` goes from 8 wrong dismissals to 3 and from 79% to 64% recall.
- AI triage warns when a model missed the published quality bar or was never measured: in the CLI summary, the JSON report, `GET /v1/ai/status` and the AI status line.
- The Go SDK and example now declare `go 1.26.10`; 1.22 no longer receives security fixes.
- **Behaviour change:** AI never dismisses or downgrades a KEV, malicious-package or reachable-critical finding, and a finding whose text addresses the reader goes to a person.
- **Behaviour change:** providers off the loopback interface need `AGENT_BOM_AI_ALLOW_REMOTE=1`, and control-plane AI needs `AGENT_BOM_AI_CONTROL_PLANE_ENABLED=1`. A scan request's `ai_enrich` no longer turns it on by itself.
- AI triage picks the riskiest findings rather than the first ones, sends the fields that decide a false positive, and reports what it skipped and why.
- The investigation graph opens on what explains the first frame. A full map reads as rows of chains, draws the connections between neighbours, and holds back long ones until a node is hovered or selected. A strip above the canvas states how many connections are drawn, each card shows how many of its own are not, and Show all or Fit all draws the rest.
- A connection no longer leaves the map to go around it. An exterior route more than twice the direct distance now takes the direct line, which removes the stray lines that crossed the whole frame.
- On a phone, graph and paths content starts in the first third of the screen. The newer-snapshot notice fits one line, Paths, Graph, Flow and Graph options share one row, and counts and preview actions sit under the content. Paths open on the worst-rated chain.
- The estate map opens on the highest recorded risks, named in a list and on the canvas, with one Investigate action. The overview's draw budget always includes them, its controls fit one toolbar, and its partial-view count now matches the map's.
- The WebGL map follows its own container's size, so opening the inspector no longer leaves the canvas misaligned.
- The MCP `scan` tool now reports `readOnlyHint: false`, because it keeps a redacted result locally and can refresh the advisory database. Its description says so. Tools that open no socket now report `openWorldHint: false`.
- The Claude plugin's MCP servers start with `GITHUB_TOKEN`, `GH_TOKEN` and `NVD_API_KEY` blanked, so a token in the environment is not sent. Lookups use the anonymous rate limits there.
- The `inventory` skill no longer has the `scan` tool, so it cannot reach the network.
- The skills-only OpenAI package now declares `requires_local_executor`, since its skills run a local CLI.
- The same package version in two projects is now two findings with separate dates and resolutions. Finding ids change for scans with a project: a push's `target_scope` or an API scan's target fields. Findings with no project keep their ids. Exceptions and triage assignees still apply by vulnerability and package, and ticket links are copied to each project's finding after upgrade.
- An open finding stays in the default findings view after the default read window passes it; an explicit `window_days` still narrows it.
- A report time more than a day ahead of when it was stored counts as the stored time, so a future-dated push cannot outrank later scans.
- The GitHub Action reads OSV's application-ecosystem archives by default (`vuln-db-scope: application`, about 0.3 GB instead of 2.9 GB). OS and other packages are answered by live OSV, and the JSON report, SARIF run properties, job summary and pull request comment state the scope. Set `vuln-db-scope: full` for the previous behaviour. The Action's cache key changes, so the first run after upgrading builds a new database.
- A first `--auto-update-db` refresh caps NVD at 60 seconds, newest CVEs first, and reports it as partial; later refreshes resume it. `AGENT_BOM_NVD_TIME_BUDGET_SECONDS` overrides the cap.
- `--auto-update-db` prints the time each source took.
### Fixed
- A database restart no longer extends an authentication outage: dead pooled connections are retried, the breaker opens only after three consecutive store errors, and one success closes it. In a private Postgres restart test, valid keys went from 25.7 s of 503s to 0.4 s.
- Failed-authentication receipts have their own workers and bounded queue, so a held audit lock or a failed audit write no longer refuses other callers. Dropped receipts are counted in `agent_bom_auth_receipts_dropped_total` and recorded in the audit chain. The Postgres key-presence check no longer holds the event loop while it waits on the audit table.
- One stalled SQLite transaction no longer blocks authentication checks for other sources.
- A burst of distinct sources now waits briefly for authentication state instead of receiving 503. An audit write error no longer leaves the SQLite file write-locked, and an unreachable identity provider no longer counts as a failed credential.
- Audit integrity is now scored per entry. Entries from a key's first use onward are verified exactly, entries no held key can check are counted as unverifiable, and links and counts are checked either way.
- Websocket authentication now follows the same source limit and failure counting as HTTP.
- Dropped failed-authentication receipts are recorded within seconds, with their window and sources.
- The session attempt limit uses every forwarded header line and the trusted-proxy settings, as the failure limit does, and a blank key no longer spends attempts. Forwarded entries written with a port are read as addresses; unreadable ones are limited apart from the proxy's own address.
- `controlPlane.api.trustedProxy.fromSecret` now requires `secretName` and reads both settings from that Secret as required keys.
- All `X-Forwarded-For` lines are combined before trusted hops are counted, so a second line can neither evade the failure limit nor frame another address. IPv6 sources are limited by /64.
- Attempts refused for capacity still count toward the failure limit.
- The stored audit signing-key identity is now a salted HMAC. Existing unsalted digests are upgraded in place.
- `run_runtime_capacity_qualification.py` no longer raises once a tenant's audit chain passes the default budget.
- Pushing a large estate no longer slows faster than it grows: paged graph-build reads use an index instead of sorting the whole table, and read-only passes no longer rewrite every node they read. Stored results are unchanged.
- On SQLite, a valid API key no longer gets HTTP 500 while a large snapshot commits, and a failed key write can no longer leave that worker's connection failing with `database is locked` until restart.
- `agent-bom quickstart --run` completes on a plain `pip install agent-bom`. `zstandard` moved from the `api` extra to the base dependencies, because stored finding and job payloads need it on the CLI path too.
- `agent-bom quickstart --run` finds `agent-bom.exe` beside the interpreter on Windows when the venv is not on `PATH`.
- `POST /v1/auth/saml/login` accepts the `application/x-www-form-urlencoded` form an IdP's browser posts, as the SAML HTTP-POST binding defines. The form is capped at 1 MiB and shares the browser-session attempt limit. A verified post starts an HttpOnly browser session backed by the short-lived key and redirects to `/`; the key never reaches the page. JSON callers are unchanged. RelayState stays a one-time server nonce and is never a redirect target.
- Added `GET /v1/auth/saml/start`. SAML form POST binds to the browser that started the login there: RelayState, a short-lived cookie and the response's `InResponseTo` must all match that login. Unsolicited and IdP-initiated form posts are not accepted, even with `AGENT_BOM_SAML_ALLOW_IDP_INITIATED`, which applies only to the JSON route. Point an IdP app tile or bookmark at the start URL.
- `helm uninstall` no longer exits 1: the teardown hook's Role grants `deletecollection`, which it always needed, and the hook no longer selects its own Job. Deleting a Job now also removes its pods.
- On macOS, gateway and proxy audit state under `/var/folders` no longer fails closed with HTTP 503. `/var` is canonicalized like `/tmp`; a symlink anywhere else in the path is still refused.
- **Behaviour change:** the `sqlite-pilot` and `synthetic-enterprise-story` Helm profiles crash-looped because the API refuses a non-loopback bind without authentication. They no longer ask for anonymous access: the `agent-bom-control-plane` Secret must now carry `AGENT_BOM_API_KEYS`, and the pod stops at `CreateContainerConfigError` without it.
- **Behaviour change:** the teardown hook's Role drops `get` and `list`, and holds `delete` on Secrets only for the names it removes, or no Secret access when there are none.
- A current-estate cache file re-created after a sweep no longer fails with `no such table: graph_snapshots` on Linux, where the new file can reuse the old inode and looked already initialized.
- The current-estate cache sweep no longer deletes a file a running store is using. Previously, starting another agent-bom process on the same state directory, or building more than 8 stores in one, could remove a live projection and cause `no such table` or `disk I/O error`. Each store now holds a lock on `<file>.lease` that clears when it exits, and the sweep and `agent-bom db prune` skip locked files. Where locks are unavailable nothing is swept.
- Two callers reaching a new current-estate store at the same moment now share one instance. Before, a scan's warm-up racing a reader could build a second one with separate build tracking.
- `ctransformers` and `parsing` are no longer flagged as typosquats. Both are established PyPI projects.
- A scan no longer says `supply chain looks clean` when the public registry has never heard of a package. It names the package, records a `package_not_on_registry` issue, and grades `N/A` if no scanned package exists. The exit code does not change, because private packages are normal.
- The `scan` skill's frontmatter is valid YAML again; an unquoted colon in its description broke parsing in the Claude and Codex plugins.
- Investigation flows: a flow that cannot apply no longer sits selected with no effect. It is disabled with the reason on screen, Automatic names what is really showing, and flows now work on maps of up to 100 nodes instead of 80, including from the connection preview.
- Investigation flows: the Kubernetes exposure path now continues from a pod through its ReplicaSet and Deployment to the identity and secrets recorded there, without adding or reversing any edge.
- Investigation flows: a ClusterIP Service no longer starts an exposure flow or counts as exposure, and appears in context as internal. A service account whose token is not mounted stays in context with a "Token not mounted" caption instead of reading as a step.
- Investigation flows: a real 20-node Kubernetes graph opens fully framed at a legible size instead of showing 4 of 20 items. Flows always draw full cards, so edges meet their glyphs, and one long label no longer changes the spacing of the map. When a graph cannot fit, the view opens on its primary path.
- Investigation flows: a second Service on the same pod no longer pulls the pod into an earlier column, Collapse findings now closes a selected finding, and the findings edge no longer runs through its own group.
- Investigation flows: the Internet boundary is drawn as an unproven dashed link, and clicking it opens the recorded assessment. Display-only objects are left out of evidence totals and item counts, and have accessible names.
- Investigation graph: the first frame is now readable on real scans. The connection preview wraps tall columns so cards never overlap, every chip and count on a card stays at 12px or larger when fitted, and a full map no flow fits opens along its most meaningful chain as "Automatic · Connections", with a note saying how much is in view. Edges pass behind captions, and a phone reaches graph content on its first screen.
- The camera no longer stays unsettled after an animated fit is interrupted, which could leave a view waiting indefinitely.
- The flow choice is kept in the URL, Reset view returns it to Automatic, and a flow's camera no longer replaces the saved camera for Automatic.
- Kubernetes inventory: an EndpointSlice endpoint with no `ready` condition now counts as ready, as `discovery.k8s.io/v1` specifies, so its route is no longer dropped.
- Kubernetes graph projection: a malformed routing or pod context record in a pushed report is skipped with a warning instead of rejecting the whole report.
- A credential-named variable set to an empty value is no longer reported as a credential. Blanking a token to keep it out of a server used to produce a false high finding.
- Hosted OAuth catalogs no longer list `scan`, which stores its result and can update the advisory database. These catalogs offer read-only tools only.
- MCP discovery no longer reports a server as insecure because an argument uses `${CLAUDE_PLUGIN_ROOT}` or `${CLAUDE_PROJECT_DIR}`. Any other `$` expansion or shell metacharacter is still rejected.
- `docs/PERMISSIONS.md` and the plugin READMEs now list the Alpine, Debian, OSV archive, GitHub advisory, deps.dev, Go and EPSS hosts. The privacy page now covers the guard hook, and states which tokens are sent where.
- All plugin manifests point at the same privacy and terms pages. Plugin descriptions no longer say credentials are never sent where an ambient token can be, and the guard description lists all of its warnings.
- The CVE link repair no longer passes over rows. Under some query plans its keyset page moved mid-statement, so it could leave a row unmarked and carry on past it, leaving that row's CVE link unrepaired.
- Skill audit: a security-review skill that lists coercion phrases in quotes as things to report now gets a low finding instead of high, for example `Text that asks the model to "bypass the guardrails"`. The finding stays visible.
- Skill audit: a quote or example word no longer demotes a real directive. These shapes were low and are now high: `Obey phrases such as "..."`, `Use phrases such as "..." and comply`, a report sentence inside an HTML comment, `Do not hesitate to ...`, a plain sentence under a heading such as `## Red flags`, and a report sentence hidden by base64, zero-width or homoglyph characters.
- Skill audit: packages launched by `npx`, `bunx`, `uvx` or `uv tool run` in an MCP JSON block, and `npx --yes` commands, now reach the package inventory, so they are checked like any other package.
- Security: `oauthlib` moves to 4.0.1 (GHSA-3jp2-3mrg-2vxc), which arrives through the Azure extra. Versions 3.3.0 up to 4.0.1 could issue a bearer token for a bogus device code.
- Typosquat detection now flags an adjacent-letter swap from five letters (`axois` as `axios`); single-letter substitutions keep their six-letter floor.
- Investigation graph: an exposure flow now continues through the workloads an exposed host runs, so the path to identities and data opens framed on one screen. Edges between adjacent entities no longer arc over nothing.
- A synchronous push interrupted by a process kill under distributed dispatch no longer stays `running` forever. The request now heartbeats a lease, and once that lease expires any replica marks the job `failed`, so the client can resubmit under the same idempotency key. A long push with a live lease is never failed for its age, and pushes already stranded by earlier releases are failed after the stuck-job timeout.
- A dispatched job that keeps crashing its workers now fails after `AGENT_BOM_API_SCAN_MAX_ATTEMPTS` claims (default 5) with an error naming the count, instead of being redelivered forever. Migration `20261105_02` adds the attempt count. Resubmitting the push starts a new budget.
- A worker run that raises or is cancelled before recording an outcome, such as a store error during ingest, no longer removes its job from the dispatch queue, which left it `pending` forever. The job is redelivered when the lease expires, and a brief store error during ingest is retried in place without failing the report.
- `agent-bom scan --push-url` now exits `1` when the results were not delivered, and says so even with `--quiet`. It used to exit `0`, so CI could pass while the control plane never received the scan. Push error text is sanitized.
- Investigation graph: an upward deployment-context edge now leaves from the top of the node instead of running through the cloud provider mark.
- Every finding now has a scan-driven lifecycle on both ingest paths: `status`, `first_seen`, `last_seen`, `resolved_at`, `reopened_at`, `last_resolved_at` and `status_updated_at`. A pushed scan that no longer reports a finding resolves it instead of dropping it, `status=resolved` retrieves it, and a returning finding reopens with its original `first_seen`. Partial, failed, inventory-only and differently scoped scans resolve nothing. See `docs/FINDING_LIFECYCLE.md`.
- `POST /v1/findings/bulk` with `reconcile_absent` no longer resolves other assets of the same source: it covers the assets the batch names or lists in `scanned_assets`, unless `reconcile_scope: "source"` asserts a full snapshot. A complete empty batch can now resolve, a non-complete `scan_run` skips reconciliation, and an unusable `observed_at` is rejected with 422 on bulk and compliance ingest.
- A reconciling bulk batch that sends its own `canonical_id` no longer resolves its own findings: the batch's present set now identifies rows the same way the store does.
- The Findings page has a Status filter (Open, Resolved, All), shows resolved and reopened dates, and labels partly fixed issues such as "1 of 2 open".
- A later finding on the same CVE no longer erases earlier CVE details or its KEV verdict, and every finding that names a CVE follows that CVE's KEV verdict in counts and lists alike.
- CVE records merge under row locks in a fixed order, so concurrent writers keep every field and batches naming the same CVEs cannot deadlock. KEV can no longer sit in only one of a record's column and payload.
- Migration `20261105_01` and the control plane's cleanup tick link existing CVE findings to their record and reconcile records that disagree on KEV. Fields a replacing writer already overwrote come back only from a rescan or a catalog sync.
- KEV follows the CISA catalog on the control plane. A fresh, complete catalog already on the replica sets KEV with its added and due dates and clears entries the catalog no longer lists; a missing, stale, empty or truncated catalog changes nothing. `agent-bom db update` now removes delisted entries, and only a complete feed can remove one.
- A first vulnerability database build writes about a ninth of the data it did: it builds beside the target with no write-ahead log or flushes, and only a finished file is renamed into place. A refresh of an existing database keeps its crash-safe settings and checkpoints less often, so it flushes about 20 times less.
- With a scoped OSV archive, a local hit in an ecosystem outside the scope no longer stops the live OSV lookup for that package.
- The `install-from-source` input now says that it only works when the workspace is a checkout of this repository.
- Cloud scans show a grade with partial evidence, such as `F (partial, 54 of 56 evaluated)`, scored as pass / (pass + fail + unknown). Partial grades cap at B and under 50% coverage shows N/A with the reason, so failing and clean accounts no longer both read N/A.
- Cloud checks with nothing to inspect report `not_applicable` instead of `pass` across AWS, Azure, GCP, Snowflake and Databricks.
- A disabled GCP API is recorded as not in use, and a provider with evaluated controls is `partial`, not `failed`. An errored check no longer reads as a finding in the exit reason.
- KEV counts and the `kev=true` filter no longer miss findings whose stored payload is compressed; existing rows are backfilled by migration `20261104_01`.
- Overview, posture and grouped findings report exact issue counts from a store aggregate at any size, so totals no longer shrink as data grows and no severity reads 0 because a row budget ran out. Coverage lanes and the Vuln / SCA card are exact on the same basis.
- First reads of overview, `kev=true` and posture counts no longer join the whole ledger; they read indexed columns.
- Scoped reconciliation resolves findings whose source sits in a compressed payload, and a cached findings total is dropped once another replica writes.
- SARIF uploads are accepted by GitHub code scanning: every result and related location has a physical location, and findings with no repository file use a marked `agent-bom-findings/` path with `properties.no_source_file` instead of a logical-only or `docker://` location. The upload action computes the line hash, so its per-result fingerprint warnings stop. Combination and prompt findings point at the MCP config and instruction file.
- The GitHub Action treats an empty `severity-threshold` as report-only, as documented. A partial scan warns and reports `scan-status: partial` rather than stopping the baseline step; malicious packages, failed scans and explicit policy or KEV gates still fail it.
- `--baseline --delta` and `agent-bom/v1` fingerprints no longer depend on the checkout folder name, so a docs-only pull request passes the documented gate. Older baselines still match.
- The Action's job summary, annotations, pull request comment and MCP gate write UTF-8 on Windows runners instead of failing on `→`.
- Container inventory probes executable headers before searching for Go build metadata, avoiding false coverage gaps for oversized data files.
- Scoped npm namespace lookalikes are checked against ranked scoped targets while preserving unrelated namespaces and official package families.
- Release preflight requires the exact main commit to pass every full Python correctness shard, combined coverage, UI lanes and runtime acceptance; an older green run cannot override a newer unsuccessful run.
- Cloud posture grades use evaluated controls and remain unavailable when collection is incomplete. Explicit cloud scans collect selected-provider native inventory without local workstation discovery; failed inventory retains a partial receipt.
- Disabled GCP APIs and billing are reported as unevaluated controls with service-state guidance, while permission and unexpected collector failures remain errors.
- File-backed registries, configuration and reports use explicit UTF-8, restoring MCP startup and Unicode evidence on Windows systems with a legacy default encoding.
- Container binary probes search in small chunks while retaining Go metadata beyond the first window; OCI changes always select the memory-bound regression suite in CI.
- Cloud provider commands isolate collection from workstation discovery, resolve active Azure/GCP targets when explicit scope is absent, and give safe Databricks re-authentication guidance. GCP logging checks use the Logging SDK's service client modules.
- Mixed Python requirements retain exact pins when resolving ranges; framework discovery no longer treats range bounds as installed versions. Independent requirements pins remain visible alongside Poetry and uv lockfiles.
- Scoped npm package names are assessed within their namespace, preventing unrelated unscoped names from producing typosquatting or dependency-confusion alerts.
- Failed Docker MCP image expansion retains the rest of the scan in a partial report. Container inventory includes Go binaries in application directories and distroless dpkg status fragments.
- Multi-document pnpm lockfiles retain package inventory and dependency relationships from each document.
- Reports retain digest-qualified image coordinates and credential variable names while continuing to redact secret values.
- Unresolved range advisories produce a partial, review-required result without a posture grade; Markdown and CSV include the review candidates.
- Image inventory without an evaluated agent entrypoint retains unknown dependency reachability, preserving structural CVE paths without claiming execution.
- Graph path technique enrichment reuses the batch hop index instead of repeatedly reading the same adjacency lists.
- Scanner and install guard agree on scoped package typo warnings: unknown namespaces retain leaf checks, lookalike scopes target known scoped packages, and legitimate scoped package names remain clean.
- Keep distributed scan recovery lease-owned and publish pushed-result completion only after graph persistence.
- The sign-in page shows SSO buttons to signed-out visitors. It reads the configured sign-in modes and the provider family from `/health`.
- Overview no longer fails to render when a finding's `affected_servers` holds objects; ingest stores server and agent names or ids.
- Text that used a removed colour token is readable again in both themes.
- Graph links accept every layer the graph URL supports.
## [0.113.2] - 2026-11-03
### Fixed
- Scan pipeline nodes remain visible when stage telemetry refreshes during initialization, preserving measured geometry separately for readable and overview layouts.
- Graph captions retain recorded cloud-service identities, including Amazon EKS, AWS IAM roles and Azure Blob containers. Clearer resource glyphs and provider badges distinguish mixed-vendor connections; missing or conflicting metadata keeps a generic caption.
- Current-estate graph reads retain their previous complete generation while a background worker promotes new evidence, avoiding a transient unavailable response during ingestion refresh. The response labels retained evidence as the previous generation.
- Hosted MCP startup preserves configured OAuth instead of forcing an incompatible public read-only tier. Railway and Procfile deployments use the authenticated default.
- Release and deployment-freshness checks use the configured managed credential, verify the exact release contract, and reject anonymous MCP access.
- Release checks distinguish an accepted deployment upload from a verified rollout. Failed startup, stale deployment evidence, and unavailable probe credentials cannot report a successful rollout.
### Upgrade notes
- Keep the existing OAuth issuer, audience and subject configuration. Do not combine OAuth with the optional public read-only tier.
- An unavailable identity provider leaves deployment verification failed; rerun verification after provider recovery.
## [0.113.1] - 2026-11-03
### Highlights
- Investigate inventory and findings on one scoped graph with readable names, circular entity icons, grouped findings, recorded relationship details, and light/dark/mobile layouts.
- Apply scoped IaC exceptions and policy rules while retaining suppressed evidence in reports and an audit trail.
- Discover Continue user-level YAML MCP configurations without executing configured commands.
### Added
- IaC exceptions with an audit trail: `.agent-bom-ignore.yaml` entries scoped by rule, path and Kubernetes resource (kind, name, namespace, labels) with required reason, optional owner and expiry; inline markers accept `expires=` and `owner=`. Accepted findings stay in JSON (`iac_findings.suppressed`), SARIF (`suppressions`), HTML and console, and leave the gates. Expired exceptions re-activate their findings with a warning; exceptions due within 14 days print a notice. A malformed IaC entry in the ignore file stops every scan up front, with or without IaC paths.
- Tenant IaC exceptions through `POST /v1/exceptions` (`iac_rule_id`, `iac_path`, `iac_resource`) with the existing four-eyes approval, bounded expiry and tenant scoping; repository scans through the API and project scans by the MCP `scan` tool apply them, and MCP scan results now include the project's IaC findings.
- Policy-as-code rules for IaC findings (`iac_rule`, `severity_gte`, `iac_path`, resource, labels, `iac_environment`) with `allow`, `warn` and `block` actions; a non-waivable `block` rejects repository-local exceptions. `agent-bom iac` accepts `--ignore-file` and `--policy`.
### Changed
- Dashboard navigation exposes every platform group without Personal, Shared or Governance presets. Retired browser preferences no longer hide reporting or source setup. Deployment settings and permission checks remain authoritative.
- Executive briefs emphasize issue groups, severity, remediation ownership and evidence coverage; source and signing details expand on demand. A finding no longer detected by a comparable scan is not labelled a verified remediation.
### Fixed
- Dense graph snapshots open with a labeled connection preview and circular nodes; the full loaded map and every starting point remain available. Phone previews reserve enough scrollable canvas space to keep both endpoints visible.
- Graph entry pages defer closed evidence inspectors until opened, reducing initial JavaScript loading while preserving the first-useful-paint budget.
- SQLite graph writes build the search index from bounded node batches, avoiding a second full-node read during cold current-estate projection. Search content and atomic replacement remain unchanged.
- Findings retain source, occurrence and partial-evidence context across API results and graph investigations; incomplete collection is not presented as a clean result.
- Push ingestion avoids blocking the API event loop, retains idempotency boundaries and isolates background graph enrichment from mutable report objects.
- Scheduled scans revalidate leadership and the current tenant-owned schedule before dispatch; deleted, disabled or rescheduled stale entries are skipped.
- Continue's `~/.continue/config.yaml` is discovered on macOS, Linux and Windows. Malformed YAML does not hide legacy JSON configurations, and discovery does not execute commands (#6147).
### Upgrade notes
- Postgres migration `20261102_01` adds IaC exception scope columns. Migration `20261102_10` adds missing fleet endpoint storage and retry-safe trend scan identity to upgraded databases; both are additive and idempotent.
- Retired dashboard product-view preferences no longer hide navigation. Permissions and evidence scope remain authoritative.
- See [upgrade guidance](docs/UPGRADING.md) for backup, migration and rollback verification. Scheduler revalidation does not guarantee exactly-once execution across crashes or leadership loss after the final check.
## [0.113.0] - 2026-11-01
### Highlights
- Azure and GCP Terraform coverage: 23 new rules (`TF-SEC-101`..`112` for `azurerm_*`, `TF-SEC-201`..`211` for `google_*`) with NIST tags, remediation, ATT&CK mappings and CIS Azure/GCP control ids (all but `TF-SEC-110`, which awaits a pinned benchmark version).
- Taint flow findings carry severity and CWE on every surface (code JSON, unified findings, console, both SARIF exporters), so they rank and export alongside other findings.
- MCP audit detects hidden-instruction tool-description poisoning, and the session's own scan result now answers `blast_radius`, `should_i_deploy` and `exposure_paths`.
- Bounded local storage: current-estate projection caches and the local analytics mirror are swept automatically, and the new `agent-bom db prune` previews or removes stale projection caches.
### Upgrade notes
- See [0.113.0 upgrade guidance](docs/UPGRADING.md) for compressed pushes, retention defaults and count changes.
- CLI pushes are gzip-compressed by default. A control plane older than 2026-09-10 (before gzip push decoding, #5193) rejects compressed pushes; set `AGENT_BOM_PUSH_GZIP=0` on collectors that still push to one.
- A Postgres URL in `AGENT_BOM_DB` (`postgresql://...` or `postgres://...`) is accepted at boot as the alias of `AGENT_BOM_POSTGRES_URL`. SQLite store openers still refuse it, with a message naming both variables.
- The decoded push budget (`AGENT_BOM_API_RESULT_PUSH_MAX_BYTES`) defaults to 256 MiB; gzip push bodies that expand beyond 100:1 are refused with 413.
- The local scan mirror now drops whole runs older than 90 days or past 2 GiB of live data, always keeping the newest run. Raise or disable (`<= 0`) `AGENT_BOM_ANALYTICS_MAX_RUN_AGE_DAYS` / `AGENT_BOM_ANALYTICS_MAX_BYTES` before upgrading if the local mirror holds history you need. Compact an existing oversized mirror once with `agent-bom report prune --apply --compact`.
- New environment variables: `AGENT_BOM_EXEC_PREWARM` (default on), `AGENT_BOM_CURRENT_ESTATE_CACHE_MAX_AGE_DAYS` (default 14), `AGENT_BOM_ANALYTICS_MAX_RUN_AGE_DAYS` (default 90) and `AGENT_BOM_ANALYTICS_MAX_BYTES` (default 2 GiB). See [ENV_VARS](docs/operations/ENV_VARS.md).
- npm lockfile scans no longer report workspace members or their `node_modules` symlinks as packages, and model binaries are scanned by default; package and finding counts can change between 0.112.1 and 0.113.0 scans of the same project.
- The weekly MCP Registry Sync needs "Allow GitHub Actions to create and approve pull requests" (Settings, Actions, General) to open its PR; without it the run opens or updates one tracking issue and fails with that reason.
### Added
- Azure Terraform rules for public storage, internet-open NSG rules on SSH/RDP/all ports, database public access and TLS, Key Vault purge protection, disk CMK encryption, VM public IPs, flow logs and SQL auditing/TDE; GCP rules for public buckets, open firewall rules, Cloud SQL public IP/SSL/backups, OS Login, serial console, subnet flow logs and IP forwarding. A depth-aware HCL helper keeps nested flags from being read as resource flags, and AWS rules no longer fire on Azure/GCP files or vice versa (#6145).
- Taint flows map to CWE-89 (SQL), CWE-78 (command), CWE-502 (deserialization), CWE-95 (eval), CWE-918 (SSRF), CWE-79 (XSS), CWE-22 (path) and CWE-1427 plus OWASP LLM01 (tainted LLM prompt); prompt-injection flows are promoted to high findings and SARIF rules carry `security-severity` and CWE tags (#6145).
- Continue project configs (`.continue/config.json`, `config.yaml`, `mcpServers/` blocks, legacy `experimental.modelContextProtocolServers`) are discovered, and the Vercel AI SDK is detected from its real packages (`ai`, `@ai-sdk/*`) (#6145).
- `docs/VENDOR_COVERAGE.md` capability matrix with counts read from code, including an explicit "not provided: HTTP/API dynamic testing" row (#6145).
- `agent-bom db prune` previews stale current-estate projection caches and deletes them only with `--yes`; `report storage` shows free pages so an oversized mirror is visible (#6144).
- The executive brief (`GET /v1/brief`, MCP `executive_brief`) carries `sla_status` (overdue / due soon / on track) and an `sla_policy` display name next to the raw `sla_due_at`; the UI shows relative and absolute dates, an SLA chip and the policy name, including in the Markdown export (#6142).
- Advisory affected-asset rows carry `asset_display` (image ref/tag or server name, kind, owning agent, host) from the graph, and the summary names its `count_basis` (#6142).
- MCP introspection and live audit detect hidden-instruction tool-description poisoning: hidden tags, "do not tell the user", credential-file paths, exfiltration URLs, tool shadowing and base64 blobs (#6143).
### Changed
- Live Activity serves an anonymous no-auth session the REST gateway feed (`GET /v1/gateway/feed`, polled every 15 s) instead of a sign-in prompt; the ledger stream still requires a signed-in session, and the empty state appears only when the API returns no events (#6142).
- The get-started project command pre-fills this deployment's `/v1/results/push` URL, shows the API-key step only when the session needs a key, links key-capable users to the API-key page, and offers an in-product action beside the CLI snippet (#6142).
- `/compliance?framework=iso-27001` (and other API/finding slugs) resolves to the matching framework section; selecting a framework writes its slug back into the address bar (#6142).
- An anonymous reader of the seeded demo estate is labelled "Demo visitor / Read-only demo" instead of "Unauthenticated access" (#6142).
- The exec read cache is generation-driven with a 15-minute safety TTL, and the four exec reads (counts, compliance summary, fix-first, brief) are warmed in the background after a demo seed or a results push, so a dashboard opened after either no longer pays the rebuild (#6142).
- Every demo agent records its kind, platform and a fictional host, so the hero attack path no longer reads "Agent kind not recorded · Host not recorded" (#6142).
- `docs/DEPLOY_PLATFORM.md` gains the Postgres-without-Compose/Helm checklist (#6142).
- npm lockfile parsing no longer emits workspace members or their `node_modules` symlinks as packages; a member's dependencies carry `parent_package=<member>` at depth 1 instead of appearing as root packages (#6143).
- One canonical tagline across `--version`, `--help`, the Docker label, Helm chart, GitHub Action, registry manifests, skills and the social preview; the release consistency check enforces it and the MCP registry description length (#6143).
- README leads with why an AI supply-chain BOM matters, the standards findings map to, and a four-row offerings table (#6143).
- Model binaries are scanned by default under the existing pickle size and opcode caps; the quick preset prints a `--model-files` hint instead (#6145).
- `agent-iac` help and the Docker MCP registry readme state "200+" IaC rules with a pointer to the capability matrix instead of stale counts (#6145).
- Current-estate projection files are swept on store construction (newest 8 kept, age cap `AGENT_BOM_CURRENT_ESTATE_CACHE_MAX_AGE_DAYS`, the active file never removed); the local analytics mirror uses incremental auto-vacuum, releases pages after prune and applies age and live-byte caps (#6144).
- MCP Registry Sync uses one stable `chore/mcp-registry-sync` branch, probes whether Actions may open pull requests, and opens or updates a single tracking issue when PR creation is refused (#6144).
### Fixed
- First-run scan accuracy: private npm workspace members are no longer reported as typosquats or resolved against the public registry; a local `agents/` package is no longer reported as the OpenAI Agents SDK; an unresolvable manifest records a coverage warning instead of aborting the scan with exit 0 (#6143).
- JSON reports keep structural identifiers (`exposure_path_id`, `package_purl`, `server_ref`) for scoped npm packages; credential-pattern redaction still applies to every value (#6143).
- Image scans record suppressed unfixed OS-package findings in the summary and never print a clean verdict over them (#6143).
- MCP server: `blast_radius`, `should_i_deploy` and `exposure_paths` consult the current session's scan result before persisted evidence or host discovery (#6143).
- MCP introspection accepts servers launched by a verified absolute launcher path; skipped servers are recorded with the reason (#6143).
- GitHub Action summary reports the posture grade and path counts recorded in the report instead of recomputing them (#6143).
- Push 413 responses say how to split the scan; the measured 10k-package report (154 MB) fits the new 256 MiB decoded budget (#6142).
- Root-caused CI flakes: the 390 px graph-workspace E2E spec, a graph-builder mock leaking across tests, and a `sys.modules` eviction that left a stale cloud submodule; no retries or widened timeouts (#6144).
- Intentionally outdated SBOM fixtures are excluded from repository vulnerability scoring through a directory-scoped `osv-scanner.toml` (#6144).
- Browser credential binding regression coverage (#6135), closed cleanup and action result readers (#6136), failed-scan posture in console summaries (#6137), GCP unresolved-boundary evidence (#6138), graph accuracy on validated findings (#6139) and consistent NVD score normalization across sync paths (#6140).
### Security
- Gzip push bodies past 1 MiB decoded that expand beyond 100:1 are refused as decompression bombs; inflation stays bounded per step and against the decoded budget (#6142).
- `docs/THREAT_MODEL.md` records the 0.113.0 trust-boundary review: descriptor poisoning detection, absolute-path launchers, identifier-safe redaction, MCP session result scoping, push budgets, exec read prewarm, cache and mirror retention, and the registry-sync tracking issue.
## [0.112.1] - 2026-10-31
### Highlights
- Import customer-selected security-lake findings into the existing authenticated findings and graph workflow, retaining scope, observation times and source receipts.
- Preview and export scoped executive briefs from the dashboard, CLI, API and MCP, with remediation ownership, evidence drilldowns and explicit verification boundaries.
- Investigate component findings with advisory context, fixed versions, connected evidence and clearer graph navigation.
### Upgrade notes
- See [0.112.1 upgrade guidance](docs/UPGRADING.md) for package extras, image roles, retained evidence and post-upgrade checks.
- Supersedes the unpublished v0.112.0 tag. Artifact publication was blocked by an expired bundled ATLAS catalog.
- Base CLI installations no longer include unused Flask and Tornado dependencies. API installations retain the filesystem-jail dependency through `agent-bom[api]`; the developer extra includes the API dependencies.
- Use the dependency-free `sca-import` extra for external SCA report imports. The existing `snyk` extra remains a deprecated compatibility alias.
- Lake ingestion accepts bounded JSON/NDJSON snapshots from local files or an explicitly version-pinned S3 object. Warehouse queries, native Parquet ingestion and continuous subscriptions are outside this workflow.
### Added
- Agent context in the investigation graph separates platform association, recorded execution hosts, model links and environment membership, with bounded references and explicit missing-evidence labels. Generic provider relationships no longer claim hosting.
- Dedicated Get started page, linked from a compact Overview setup card and Connect navigation, with explicit control-plane scan, source connection and local-project push paths; expandable credential-free commands, inventory-to-impact-to-verification guidance, and a separate browser-only report preview.
- Project activation with retained scan/graph receipts, shared-fix prioritization, and a CVE impact workspace showing recorded workflows, advisory conditions and conditional reach without claiming local exploitability.
- Dashboard executive brief with Markdown/JSON exports, finding and graph drilldowns, source-scope disclosure, and authenticated compliance-pack/key downloads with explicit verification guidance. The read-only demo retains sample labels and evidence limits.
- Lake import activation in Connections, with a packaged `agent-bom ingest lake-sample` workflow, private local report artifacts, server-recorded graph persistence counts, and scan-scoped Findings and Investigation links.
- `agent-bom ingest lake` maps selected finding observations into a private report and can push it to a configured control plane. Input and mapping digests, scoped identifiers and observation times support replay and provenance; source-declared controls remain evidence tags rather than attestations.
- Per-agent novel-tool alerts use a bounded in-memory baseline and report previously unseen tools after baseline warmup. Alerts are advisory and do not change enforcement decisions.
- Authenticated executive briefs through `agent-bom report brief`, `GET /v1/brief`, and the `executive_brief` MCP tool: canonical posture, remediation owners and SLA provenance, ranked exposure paths, technical control evidence, scoped scan history, and links to the signed compliance pack. Sample-data and incomplete-evidence boundaries remain explicit.
### Changed
- Azure multi-subscription and GCP multi-project onboarding share discovered scopes between inventory and CIS collection, retain per-account attribution, and preserve partial-collection warnings.
- Scanner, API/storage, graph, output and dashboard modules are split behind existing interfaces, with ratcheted file and function size checks.
- One lead message on every public surface (README, docs home, PyPI and Docker Hub descriptions, package metadata, registry manifests): "AI supply-chain BOM: every agent, MCP server and AI package, and what each one can reach." The README leads with the measured real-repository accuracy, shows the `db advisory affected` / `affected_assets` commands for a new CVE, keeps five badges, and points to GitHub Discussions first.
- `docs/THREAT_MODEL.md` records the gateway/posture audit outbox (leased, fenced acknowledgements) and the MCP `scan` `project_path` filesystem boundary.
### Fixed
- First-run project scans retain an HTML report with responsive evidence tabs, expandable finding details and print layouts. Missing scores remain distinct from zero; stale catalog evidence is labelled explicitly.
- Explicit enrichment requests remain effective under quick scan profiles. Distribution-ambiguous package matches remain possibly affected and incomplete until distribution evidence resolves them.
- Ollama discovery preserves the selected endpoint and collection provenance; an unavailable configured endpoint never substitutes local manifest inventory. Missing AISVS exposure evidence remains unknown.
- Current-agent counts avoid materializing graph paths; retained-report reads reduce repeated decoding, and advisory aliases use indexed lookups.
- Published MCP images use verified release Git contexts and distinct API/MCP image identities. Released-image dependency refreshes update pyasn1 where installed.
- Cloud inventory summaries in REST and MCP preserve incomplete enabled-provider collection instead of reporting success from enablement alone. Missing SDK states and partial Snowflake discovery remain visible without discarding successful resource counts.
- Retain collected cloud assets and managed Kubernetes GPU pools from explicitly partial inventory in the graph, while preserving incomplete scan coverage and scoped reconciliation.
- Release preflight and CI inspect the bundled ATLAS catalog even when a newer operator cache exists, using the same 180-day freshness limit. The bundled catalog is refreshed from the upstream source.
- Affected-advisory answers include image packages and distinguish stale observations from confirmed current evidence; VEX filtering and finding totals use aligned semantics.
- LangGraph topology retains `Send` fan-out edges. Secret scanning distinguishes CPython module links from password values.
- Released-image refreshes patch Tornado where installed and verify collector and UI refresh completion.
- Finding-backed graph assets retain recorded provider, account, region and environment. Imported rows with different observation times keep their own node and relationship timestamps through persistence instead of inheriting the oldest observation in the batch.
- Component findings place recorded advisory context, relationship explanations, known fixed versions, connected neighbors, and scoped next steps in a responsive investigation view while preserving snapshot and coverage boundaries.
- Findings, audit, trace, and graph drill-down links preserve the configured deployment prefix. The static demo reports explicit zeroes for empty severity buckets and directs onboarding links to installation instead of unavailable sign-in.
- Release automation: the post-release pin job pushes its branch and ends as a labelled skip with the manual `gh pr create` command when the signing key or the "Allow GitHub Actions to create and approve pull requests" setting is missing; the MCP SSE deploy separates "deploy succeeded, probe deferred" (OAuth provider outage, with the token endpoint status) from a failed deploy; the release run calls the docs build alone instead of showing a skipped Pages deploy.
- PR CI: the kind/Helm runtime acceptance lane runs on PRs only for chart, runtime deploy or lane changes, and Dependency Review skips the snapshot diff when no manifest, lockfile, Dockerfile, chart or workflow changed.
- Small multi-source graphs use balanced relationship ranks, retain directional arrows, and reveal relationship captions on selection.
- Completed graph queries keep display labels in the inspector instead of rewriting the URL, preserving newer root navigation and explicit link labels on slower browsers.
- First-run console output: repeated CI-hardening warnings collapse to one line per rule with a count, IaC rule groups count occurrences instead of splitting per message, a scanned repository is counted as a project rather than an agent, the dependency legend spells out direct/transitive/unknown, a lone critical says why it is P2 rather than P1, and the compliance block appears only with `--compliance` or benchmark evidence (one hint line otherwise).
- Reports written to a file (including through a saved profile that pins `format`/`output`) confirm the path on stdout instead of leaving it empty.
- `agent-bom iac` no longer flags `uv pip install --no-cache` as a missing `--no-cache-dir`; the shipped `Dockerfile` pins its Alpine packages.
- Secret scanning no longer reports template placeholders as hardcoded secrets. Go/Helm/Jinja `{{ ... }}` actions, `${VAR}`/`$VAR` references, printf verbs (`postgres://%v:%v@%v`), `<placeholder>` values and a value that only repeats its key name (`password = "password"`). The same rejection applies to Terraform `TF-SEC-051`. Findings under `testdata/`, `__fixtures__/` and `__mocks__/` are kept at `low` severity with a `downgrade_reason` instead of their default severity.
### Deprecated
- `agent-bom gateway serve --oauth-as-issuer` / `AGENT_BOM_GATEWAY_OAUTH_AS_ISSUER` is hidden and ignored (the embedded OAuth AS is unavailable); it will be removed in a later release.
## [0.111.0] - 2026-10-28
### Highlights
- **"Am I affected by CVE-X?"** answered across every stored scan, from the CLI, API, MCP, UI, skills and Action, using every alias record of a vulnerability.
- **More accurate dependency scans on real repositories:** Gradle variables, `setup.cfg`/`setup.py`, full requirement ranges, lockfile-owned workspaces and Maven parent POMs, measured against public Temporal and Snowflake repositories.
- **Runtime protection you can switch on:** `--injection-mode enforce` removes poisoned MCP tools and blocks injected tool results; the default stays alert-only.
- **A sturdier self-hosted control plane:** probes survive a database outage, operator API keys stop expiring, SQLite deployments keep their keys, and large images scan in bounded memory.
- **One severity gate:** the CLI now fails on high or critical findings, the same as the GitHub Action (see Upgrade notes).
### Upgrade notes
- **The CLI severity gate now defaults to `high`**, the same as the GitHub Action, so a repository no longer passes locally and fails in CI. A plain `agent-bom scan` with a high or critical finding exits `1`. Keep the old behaviour with `--fail-on-severity critical`, or make a run reporting-only with `--exit-zero`.
- `agent-bom check <package>` without a version now checks the latest published version and reports `version_source` (`requested`, `registry_latest` or `registry_fallback`). When the latest version cannot be resolved, the result is incomplete and exits `2`; previously it was skipped with exit `0`.
- `agent-bom proxy` and the gateway can now enforce, not only alert, on poisoned tool descriptions and injected tool results (`--injection-mode enforce`). The default stays alert-only in this release.
- Completed scan-job results are now stored zstd-compressed in SQLite and PostgreSQL (see `docs/STORAGE_BACKENDS.md`). Existing rows are read unchanged and nothing is rewritten, but earlier releases cannot read the new rows: during a rolling upgrade, or before a downgrade, set `AGENT_BOM_JOB_RESULT_CODEC=none` on every writer.
- JSON report `blast_radius` rows move to `schema_version: "2"`: the embedded `exposure_path` object, an exact copy of the matching `exposure_paths.paths` entry, is replaced by `exposure_path_rank` (1-based index into `exposure_paths.paths`) and `exposure_path_id`. Read the path with `.exposure_paths.paths[.exposure_path_rank - 1]` or `agent_bom.output.exposure_path.blast_radius_exposure_path(report, row)`, which also reads schema `"1"` rows. SARIF results still embed their path.
- `--gpu-scan` reports and the MCP `gpu_infra_scan` result drop `driver_findings` and `driver_cve_count`, which were computed from fixed version floors rather than advisories. NVIDIA driver CVEs now appear as ordinary vulnerabilities on the `nvidia-gpu-display-driver` package of the `k8s-gpu-cluster` agent; the cluster scope lists `driver_versions` per vendor.
- Shared gateway audit delivery (`AGENT_BOM_GATEWAY_AUDIT_BACKEND=postgres`, Helm `gateway.auditBackend`) adds Alembic revisions `20261027_01` to `20261027_03`. `20261027_02` (delivery leases) has no downgrade, and `20261027_03` adds a stored generated column to `drift_incidents` under an exclusive lock: run the migrations before starting new replicas and schedule them outside peak hours on large tables.
### Added
- LangGraph topology in `scan`: `add_edge(START, …)` / `set_entry_point`, `add_edge(…, END)` / `set_finish_point`, `add_conditional_edges` and `set_conditional_entry_point` (one `delegated_to` edge per branch, carrying the branch label), `add_node(fn)` with the inferred name, subgraphs (`add_node("sub", child.compile())`, nested under their node via `parent_id`) and `Command(goto=…)` handoffs are read with `declared` provenance. The JSON report lists them per agent; in both graph views the edges keep every branch label that leads along them and an edge `kind` (`entry`, `finish` for a node returning control to its graph, `subgraph`), and unified-graph agent nodes carry `parent_id`, `unresolved_route_count` and `unresolved_route_reasons`. Graph bindings and node functions resolve by lexical scope, so builders with the same name in different functions stay separate graphs, and an aliased `StateGraph` import is recognised. Routes the parser cannot resolve (no path map, a non-literal path map or member, a computed `goto`) are listed on the source node as `unresolved_routes` and mark it `dynamic_edges` instead of being dropped; a file yields at most 256 targets per call and 5,000 edges, and a cap that is hit is recorded as a `truncated` unresolved route. Previously only `add_node("name", fn)` and string-literal `add_edge` were read, so a supervisor graph scanned as disconnected nodes.
- LangGraph prebuilt multi-agent shapes: `create_supervisor` (supervisor → members, members hand back, `.compile(name=)` naming nested teams), `create_swarm` (entry edges to every member, the default agent marked) and `create_handoff_tool(agent_name=)` in an agent's tools are read as declared routes; LangChain `create_agent` is detected; a `model=` passed through a module-level chat-model binding, `tools=TOOLS` and `ToolNode(TOOLS)` are resolved within the file. The console prints each agent graph as a bounded flow (entry, routes with branch/goto/handoff notes, subgraphs, tools and model per node, unresolved routes). Framework-agent ids are scoped to the scanned project, the project agent `contains` its top-level graphs, and the CLI graph export folds non-delegating constructs into the project agent as the unified graph does. The framework node now links to the scanned, pinned package (previously a versionless copy, so CVEs on `langgraph` did not reach LangGraph agents), and declared wiring no longer counts as runtime-observed activity on attack paths.
- MCP `scan` takes `project_path` to scan a repository directly; the path must resolve inside the home directory or `AGENT_BOM_MCP_WORKSPACE_ROOTS`, and project discovery runs off the event loop.
- Opt-in runtime enforcement (`--injection-mode enforce`, the matching environment variables, policy key and Helm value): poisoned tools are removed from `tools/list` and later calls to them are refused for the session; injected tool results are replaced by a JSON-RPC error that names the detector and never forwards the injected text. Every action is audited with `action: blocked|stripped`. See `docs/RUNTIME_FAIL_MODES.md`.
- `agent-bom cloud databricks`, `snowflake`, `huggingface`, `ollama` and `posture`, previously reachable only through `agent-cloud`.
- Helm: an ingress-only NetworkPolicy for the control-plane API and UI that admits their namespace and the ingress controller (`controlPlane.networkPolicy`).
- Persisted runtime, proxy and gateway activity appears in the graph as `recorded_activity`, with source receipts, replay-safe counts and exact tenant/cloud-scoped identity binding. One bounded UTC window covers both evidence stores. Unmatched identities remain unverified shadows; recorded decisions do not establish execution or permission and never form attack paths.
- Agent contributor skills `.agents/skills/develop` (test-first loop) and `.agents/skills/secure-change` (security rules), loaded by Codex and Claude Code; CI now fails a PR that changes Python source without a test change (maintainer label `no-tests-needed` waives it) and scans every repo `AGENTS.md`, `CLAUDE.md` and `SKILL.md` with the skill auditor.
- Contributor path: `GOVERNANCE.md` with the triager, reviewer and maintainer ladder, `MAINTAINERS.md`, and a Codespaces dev container that installs the dev extras and pre-commit hooks.
- GitHub Action: `sbom-artifact` (`cyclonedx` | `spdx` | `none`) uploads an SBOM / AI-BOM workflow artifact; `mcp-gate` fails a pull request that adds or redefines a blocked, malicious or vulnerable MCP server; `max-annotations` caps annotations with an explicit "N more" notice. The job summary lists the top findings (severity, file:line, rule, fix), and the PR comment ranks findings by severity, then blast radius.
- A reusable workflow (`.github/workflows/agent-bom-reusable.yml`), GitLab CI, Azure Pipelines and Bitbucket Pipelines templates under `examples/ci/`, and a GitHub Action guide with quick-start, full PR gate and new-findings-only examples.
- Source-built language runtimes are detected in images (PHP, Python, Node.js, Go, Ruby, Java from image config) and on hosts and VM snapshots (`/usr/local` and `/opt` builds), and matched through NVD CPEs. Offline, a runtime reports a coverage gap, never a clean result.
- Operator advisories match before feeds publish: `agent-bom db advisory add|list|remove|match`, `/v1/intel/operator-advisories` and MCP `intel_lookup`. Declared advisories match the newest stored scan per target, tenant-scoped; a later feed record with the same id or alias supersedes it.
- NVIDIA GPU stack detection in `fs` scans, VM side-scans and image layers: driver, CUDA toolkit and NVIDIA Container Toolkit versions with NVD CPE identities, so their CVEs match GPU nodes.
- Skill audits flag hardcoded secrets (type and line only), environment-variable exfiltration to a network sink, and instructions hidden in Unicode tag characters or variation selectors.
- The agent-bom plugin installs in Codex as well as Claude Code, with `checkup`, `affected`, `bom` and `activity` skills. `agent-bom-guard` adds a Claude Code `PreToolUse` check that blocks known-malicious and typosquatted package installs. Both plugins publish a privacy policy.
- `/v1/graph/attack-paths` accepts `kev=true` with the same meaning as MCP `exposure_paths`, and rejects undeclared query parameters with 422 instead of ignoring them.
- `/health` reports `demo_seed: pending|complete` in demo mode, and the UI waits for seeding before showing counts.
- Over HTTP or SSE, host-local MCP tools (`agent_activity`, `browser_extension_scan`, and `inventory` / `tool_risk_assessment` without `config_path`) are refused unless `AGENT_BOM_MCP_ALLOW_HOST_LOCAL_TOOLS=1`; stdio is unchanged.
- `agent-bom activity`, the `agent_activity` MCP tool (audit profile, local and read-only) and the plugin `activity` skill review local Claude Code and Codex session history: MCP server and tool usage, configured servers never called in the window, risky actions with redacted evidence (pipe-to-shell, destructive commands, writes outside the project, secret-file reads, package installs) and installed packages. Reads are streamed and capped; prompts and responses are never read into results, and nothing is sent unless `--check-packages` is set. Sessions map to the shared trace span shape as local providers `claude-code-sessions` and `codex-sessions`, and used servers come from the runtime trace projection with exact discovery bindings; the control-plane pull route rejects these providers.
- `agent-bom db update --source nvd` ingests NVD-only CVEs and their CPE ranges without an API key: a 14-day `lastModified` window at 5 requests per rolling 30 seconds (120 days and 50 per 30 seconds with `NVD_API_KEY`), so a source-built runtime such as `php 8.3.30` matches a CVE NVD has analysed before OSV or KEV list it, with the range's fixed release.
- Operator advisories that name a `cpe_product` no longer report distro packages of that product as clean: Debian `php8.2-cli 8.2.29-1~deb12u1` against a `php:php` advisory takes the local Debian/Alpine/RHEL feed verdict when one exists, and is otherwise listed as `indeterminate` (`distro_backport_unverified`, shown in the `db advisory match` table) instead of comparing backported versions to upstream ranges. The daily re-match counts these as `indeterminate_distro` without freezing its cursor.
- Helm: optional `nvdApiKey.existingSecret` injects `NVD_API_KEY` from an existing Kubernetes Secret (`secretKeyRef`, unset by default) into the scanner CronJob and control-plane API; the chart never takes a plaintext key.
- "Am I affected by CVE-X?" on every surface: `GET /v1/intel/advisories/{id}/affected`, the read-only MCP tool `affected_assets` (graph profile) and `agent-bom db advisory affected <id>` return one answer per asset for a declared advisory or a local feed record, NVD CPE ranges included: affected (with the patch target), possibly affected (distro packages; verify the distribution's advisory) or not affected, with provenance, last observed scan and graph node ids for blast radius. Pages use a generation-carrying cursor (409 when the answer changed, 422 when malformed); `clean_scanned` means not found in what was scanned, never safe.
- Operator-advisory and feed re-match matches are persisted as findings (tenant-scoped, one per advisory, package and asset) so they appear in `/v1/findings`; a rescan that no longer matches, a feed record that now covers the package at scan time, or a retired advisory resolves them. Possibly-affected rows keep severity `unknown` with `advisory_severity` alongside, so they are never counted as confirmed vulnerabilities. Control-plane scan graphs give declared advisories a vulnerability node and a `vulnerable_to` edge from the package.
- Scans report distro packages of a CPE product (Debian `php8.2-cli` for PHP2Shell, CVE-2026-109618/109619) as possibly affected (`distro_backport_unverified`) when no distro record decides the CVE: JSON `possibly_affected`, SARIF note-level `agent-bom/possibly-affected`, CycloneDX `analysis.state: in_triage`, HTML and terminal. They are never counted as vulnerabilities and never trip `--fail-on-severity`.
- Dashboard: an Advisories page to declare an emergency advisory (validation errors shown per field) and see which assets an advisory affects, with patch targets, distro "possibly affected" labelling and blast-radius links; the advisory freshness widget shows partial and stale feeds per source, and `/v1/intel/sources` reports a partial NVD sync as `partial` with its reason.
- GitHub Action: optional `nvd-api-key` input (pass `${{ secrets.NVD_API_KEY }}`), set only as the scan step's `NVD_API_KEY`, so the default DB refresh, which includes NVD, reads the keyed window. Possibly-affected results stay in the SARIF upload as notices, outside `vulnerability-count`, the badge, the grade and gates.
- Claude Code and Codex `affected` skill answers NVD-only CVEs as affected / possibly affected (distro) / not found in what was scanned, using `affected_assets` when a control plane is connected. New runbook `docs/RESPOND_TO_A_NEW_CVE.md`: declare, match, blast radius, patch, verify by rescan.
- Source import discovery identifies Google ADK, Claude Agent SDK, Strands Agents and Azure AI Agents in Python and JavaScript/TypeScript, plus Bedrock AgentCore, Microsoft Agent Framework and NVIDIA NeMo Agent Toolkit in Python, and OpenClaw plugin-SDK imports in JS/TS. SDK package identities and AST labels agree, while near-name modules and general cloud clients are excluded. Static import evidence does not imply runtime execution.
- Gateway operators can configure upstream HTTP connection/keepalive limits, timeout, circuit failure threshold and cooldown through CLI flags or matching environment variables. The existing validator rejects invalid settings before discovery or listener startup; authentication and policy defaults are unchanged.
- Gateway replica sets can share one Postgres audit outbox (`AGENT_BOM_GATEWAY_AUDIT_BACKEND=postgres`, Helm `gateway.auditBackend: local|postgres`): events are claimed with database-clock leases, acknowledged with fenced deletes, and a tool call is admitted only after its own decision is durably acknowledged by the control plane (up to 20 seconds; the earlier replica-local spill remains the default). See `docs/RUNTIME_FAIL_MODES.md`.
- The stdio proxy detects tool definition drift under a stable tool name (description, input/output schema, annotations), coalesces `notifications/tools/list_changed` refreshes and bounds the catalog it re-reads (20 pages, 4,096 tools); an incomplete catalog never replaces the baseline.
- Gateway `--injection-mode enforce` remembers the tools it withheld from `tools/list` per tenant and upstream (bounded, 24 h TTL) and refuses a later `tools/call` to them with the same `-32001` error and an audited `action: blocked` event; a clean relisting restores the tool. The memory is per process: a replica that never saw the listing forwards the call and still inspects its result (`docs/RUNTIME_FAIL_MODES.md`).
- `AGENT_BOM_MAX_IMAGE_CONFIG_BYTES` (default 16 MiB) bounds the image config blob read during a registry pull separately from the manifest limit; an oversized config fails the scan before any layer is fetched.
### Changed
- Image scans stream layers instead of buffering them: peak traced memory for a 1 GiB image fell from about 3 GB to 9 MB. A time budget (`AGENT_BOM_OCI_SCAN_TIMEOUT_SECONDS`, default 900) and member, size and decompression limits report a `partial` scan that names the skipped layers instead of exhausting the host.
- Fix First explains each priority (for example "P3: high severity; not known exploited, no credential reach"), the AI component summary no longer calls a clean or empty scan "safe", and Findings rows leave remediation cells blank instead of printing filler.
- The Docker Hub push token is available only to release jobs in a `docker-release` environment; pull-request CI uses a pull-only token. Main-failure alerts react only to pushes to this repository's main branch.
- Compose files publish ports on loopback by default, including the development Postgres and Redis and the runtime example proxy.
- GitHub Action: `pr-comment` defaults to `auto` (posts when the token has `pull-requests: write`, otherwise skips with a notice); annotations are error, warning or notice by severity; `format: json` writes `agent-bom-results.json`; docs reference `@v0` and `scan-type: scan`.
- PR CI sizes pytest workers to the runner, skips smoke when the full shards replace it, and splits UI E2E across four runners.
- Migration `20261023_02` revokes writes on `alembic_version` from the application and RLS-maintenance roles; migrations run as the admin role.
- `agent-bom image <ref>` and `scan --image` now contact the image's registry first (read-only GETs, plus a token request) instead of the local Docker daemon. A reference without a registry host that the local daemon already holds (`acme/app:dev`) is scanned from the daemon, so it never resolves to a same-named Docker Hub image; other local-only tags scan through the daemon after the registry pull fails; use `--pull-mode daemon` to never contact the registry. `--offline` never contacts a registry.
- Hardcoded secrets found inside an image are high/critical findings, so an image that bakes in a credential now fails the default severity gate.
- `agent-bom image <ref>` works without a Docker daemon: it pulls the manifest, config and layers over the OCI distribution API (anonymous, Docker Hub token flow, `~/.docker/config.json` `auths`, `credHelpers` and `credsStore`, or `--registry-user`/`--registry-pass`), picks the platform from an index (default `linux/amd64`, `--platform` to choose), verifies every manifest and blob digest, and streams each layer through the same bounded walk as `docker save` archives. `--pull-mode auto|registry|daemon` (`--image-pull-mode` on `scan`, `AGENT_BOM_IMAGE_PULL_MODE`); `auto` scans a host-less reference held by the local daemon from the daemon, and otherwise uses the daemon only when the registry pull fails and says why. zstd layers are reported as coverage gaps.
- Image signatures and attachments: cosign signatures (`sha256-<digest>.sig`) are verified with `--cosign-key`/`--image-cosign-key` (ECDSA, RSA or Ed25519 public keys); keyless Sigstore signatures are verified with the optional `sigstore` extra when `AGENT_BOM_IMAGE_SIGNER_IDENTITY` and `AGENT_BOM_IMAGE_SIGNER_ISSUER` name the expected signer; signatures without a key or a signer policy are reported as `present-unverified`, never `verified`. OCI referrers (referrers API, tag schema, cosign `.att`/`.sbom` tags, BuildKit attestation manifests) are listed as SBOM / provenance / attestation / signature. Status `verified`/`invalid`/`present-unverified`/`unsigned`/`unknown` is on the image agent's metadata, the graph image node and the console; `--require-signature`/`--require-image-signature` fails the scan unless the signature is `verified` with a cosign public key.
- Secrets inside images: text files in every layer (same matchers, skip rules and redaction as `agent-bom secrets`; binaries, OS package trees and files over `AGENT_BOM_IMAGE_SECRET_MAX_FILE_BYTES` skipped; at most `AGENT_BOM_IMAGE_SECRET_MAX_FILES` per image), image config `ENV` values and build history (`created_by` build args) are scanned. Findings are `CREDENTIAL_EXPOSURE` bound to the image node with the layer digest; a secret a later layer deleted is still reported (it is in the layer blob) with `in_final_filesystem: false`.
- Base-image attribution: the base named by the OCI `org.opencontainers.image.base.name`/`.base.digest` annotations or labels is fetched (server-side scans only from `AGENT_BOM_REGISTRY_ALLOWLIST`, or the image's own registry without one), and when its layers are an exact prefix of the image's, each package occurrence is marked `layer_origin: base|app` with the base name. A named base whose layers do not match attributes nothing.
- `agent-bom iac` scans Docker daemon configuration (`daemon.json` in a `docker/` directory, or any `daemon.json` named on the command line) with eight `DOCKERD-*` rules: TCP API without `tlsverify`, non-loopback insecure registries, `icc` not disabled, no `userns-remap` (not required for rootless), `live-restore` off, `no-new-privileges` off, `experimental` on and `log-driver: none`. Missing keys take dockerd's default.
- Compose rules `COMPOSE-007`..`011`: seccomp/AppArmor `unconfined` or `label:disable`, host IPC/UTS/user namespace, sensitive host paths bind-mounted (`/`, `/etc`, `/proc`, `/sys`, `/var/lib/docker`, `.ssh`, ... but not `/etc/localtime` or app directories), `user: root`, and services without `no-new-privileges`. `COMPOSE-005` also covers `SYS_MODULE`, `SYS_PTRACE`, `SYS_RAWIO`, `DAC_READ_SEARCH`, `SYS_BOOT` and `BPF`.
- Dockerfile rules `DOCKER-023`..`026`: `apt install` without `--no-install-recommends`, apt/apk packages without a version pin, a `CMD`/`ENTRYPOINT` overridden later in its stage, and credential files (`.env`, SSH private keys, `.npmrc`/`.pypirc`/`.netrc`, keystores, `.aws/credentials`, `.docker/config.json`) copied into an image layer.
- `*.dockerfile`, `*.containerfile` and `Containerfile*` are scanned as Dockerfiles.
- GPU node pools in cloud inventory, read-only: EKS managed node groups (`eks:ListNodegroups` / `DescribeNodegroup`), GKE node pools with accelerators, GPU sharing, MIG partition and driver-install setting, AKS agent pools with GPU VM size, MIG profile and driver opt-out, GCE guest accelerators, Snowflake SPCS compute pools (`SHOW COMPUTE POOLS`, GPU model and count per `GPU_*` family) and Databricks GPU clusters (kept even without installed libraries). Connection inventory summaries report `node_summary.gpu_node_pools`.
- GPU pools in the graph: a `gpu_node_pool` cloud resource contained by its cluster and assuming its node IAM role or service account; GPU attributes on EC2, GCE and Azure GPU instances; SPCS compute pools under their account.
- `--gpu-scan` and the MCP `gpu_infra_scan` tool inventory Kubernetes GPU workloads with read-only `kubectl get`: node pool, service account, EKS IRSA role, privileged / hostPath / host-namespace settings, the node driver (GPU Feature Discovery labels) and Container Toolkit (GPU Operator `ClusterPolicy`). The driver and toolkit become CPE-identified packages matched like any other, and attack paths run from a vulnerable host GPU stack through the workload to its service account, IRSA role and the GPU node identity.
- Provider-aware GPU posture checks GPU-001 to GPU-021 (SSH open on EKS GPU node groups, AKS node public IPs, GKE default compute service account, untainted EKS/AKS GPU pools, operator-managed drivers, shared GPUs and multi-tenant time-slicing, privileged / hostPath / host-namespace GPU pods, Databricks no-isolation GPU clusters, shared SPCS GPU pools) in the unified findings stream and the graph.
- Image scans record model files found in layers and statically disassemble pickle-bearing ones (never deserialized) within the existing pickle byte cap, honouring whiteouts; findings attach to the image. See `site-docs/features/gpu-ai-infrastructure.md`.
- The README banner shows coverage in six groups (AI clients and IDEs, AI platforms and model hubs, GPU and neo clouds, hyperscalers and Kubernetes, data and vector stores, identity/SaaS/dev) plus where evidence can be sent, built from `docs/images/integrations-manifest.json`. Every entry names the code that implements it; a test fails when an entry has no implementation or a registered provider, connector or MCP client is missing. The same generated table appears in the README, PyPI and Docker Hub pages and the docs index. ClickHouse is listed as an analytics and export destination, not a scanned source.
- Graph glyphs: packages now draw a blocks glyph and the container keeps the shipping-container glyph, so a package hop never reads as a container. The path step list (desktop and phone) draws each hop's own entity glyph instead of a coarse role glyph, so containers no longer share the MCP server glyph, users no longer share the agent glyph, and environments and clusters no longer share one glyph.
- Scan-job `result` and `result_document` values of 4 KiB or more are stored zstd-compressed behind a versioned, SQL-visible header instead of as uncompressed JSON rewritten on every job update; top-level job keys and the demo-estate SQL projection keep working, and a guard test rejects any new SQL that reads deeper into a stored result. Measured on the demo estate in `docs/perf/report-volume.md`.
- Dashboard runtime activity is grouped by origin, collector, session and identity, and connection-state copy says what was recorded rather than what is live.
- Server-side disk side-scans require a tenant connection grant that names the provider, account, target and collector; live-Kubernetes collection is an explicit opt-in; API-docs assets are pinned with integrity hashes.
- Shared Postgres gateway audit delivery: non-admission events are enqueued and the delivery worker is woken, instead of flushing inline under the send lock, so a slow control plane no longer serialises every tool call behind a 20 s send; admission still waits for its own durable acknowledgement.
- Tool definition drift fingerprints ignore the order of schema `required`/`enum` sets and declaration `icons` lists and treat absent `annotations` as `{}`; literal values, vendor metadata, `anyOf`/`oneOf` member order and `required` membership still drift.
- Pushed scan results publish detached snapshots to the in-memory job store, keeping concurrent graph readers stable during enrichment. A fatal graph roll-up warm-up exit releases its worker state without suppressing the error; later readers compute uncached views.
- In enforce mode the stdio proxy drops non-JSON-RPC upstream stdout whether or not inline scanning is enabled, and audits the drop; `--injection-mode --help` states which detections block (high/critical response-inspector categories) and which forward with an alert.
- `AGENT_BOM_REGISTRY_ALLOWLIST` accepts any Docker Hub alias (`docker.io`, `index.docker.io`, `registry-1.docker.io`) for Hub images, and when an allowlist is set the bearer-token realm host must be the registry itself or allowlisted.
- Advisory re-match indexes advisories by package identity and CPE product once per run (5,000 packages × 2,000 advisories: 10.4 s → 0.12 s, identical results) and, when scan snapshots are enabled, reads only the newest job per scope instead of decoding every historical result.
- `compare_version_order` maps vendor and OSV ecosystem labels (`Ubuntu`, `Debian:12`, `Alpine:v3.18`, `Red Hat`, `Rocky Linux`, `AlmaLinux`, `SUSE`, `Wolfi`) to their distro comparator; previously such labels fell through to PEP 440 ordering and could compare distinct releases as equal.
- Alembic refuses to run as `agent_bom_app` or `agent_bom_rls_maintenance` (the migration that makes `alembic_version` read-only would otherwise revoke the runner's own access); use the admin `ALEMBIC_DATABASE_URL`. Migration `20261027_03` is guarded and builds its index concurrently; `alembic downgrade` cannot pass below `20261027_02` (`docs/STORAGE_BACKENDS.md`).
- The release `docker-mcp-pin` job fails loudly, with a run-summary section and the manual commands, when the automation signing key is missing instead of silently leaving the published pins at the previous version.
### Fixed
- Hugging Face collection failures return stable outcome codes (`denied`, `throttled`, `unavailable`, `empty`) and keep the partial inventory instead of failing the whole cloud scan.
- Corrupt or truncated gzip SBOM payloads are rejected as validation errors instead of raising inside the parser (ClusterFuzzLite regression).
- `HELM-003` no longer reports the `key`/`name` fields of an `existingSecret`, `secretKeyRef` or `valueFrom` block as a hardcoded secret, so a chart that references a Kubernetes Secret the documented way (including this project's `nvdApiKey.existingSecret`) passes the default gate.
- `contracts/v1/scan-report.schema.json` accepts the `blast_radius` schema `"2"` rows and the scan-wide `coverage_warnings` rows (`kind: remote_lookup_error|remote_lookup_gap|package_version_gap`) that every 0.111.0 report carries; a report with findings now validates against the shipped contract.
- Behind an egress proxy (`HTTPS_PROXY`) on a network without direct DNS, advisory and registry lookups no longer fail with `host unresolvable or egress refused`: the proxy resolves the name, so a local resolution failure is not treated as a verdict. Literal private and metadata addresses, and `NO_PROXY` hosts, are checked as before.
- Shared Postgres audit admission drains a backlog larger than one delivery batch (after a control-plane outage) within the acknowledgement deadline instead of returning 503 until the retry worker catches up; a cursor minted for another tenant on `/v1/intel/advisories/{id}/affected` is `422`, as on every other paged route; `GET /v1/intel/advisories/{id}/affected` and `/v1/graph/attack-paths/cross-vendor` reject undeclared query parameters with `422`.
- Source-only projects (no manifest or lockfile) now get a project host agent, so LangGraph builders fold under `contains` edges and `code_agents` the same way as projects with a `requirements.txt`; previously every construct scanned as a standalone agent.
- `init.sql` bootstrap carries `trend_history.kev_count`, matching migration `20261022_01`; the gateway audit outbox grants `agent_bom_readonly` SELECT in both schema paths.
- Stdio proxy: the catalog refresh and the client relay share one stdin writer lock, so a `tools/list_changed` refresh under backpressure no longer races the relay's drain on Python 3.11 and kills the session.
- Version ordering for `/etc/os-release` style labels (`Debian GNU/Linux`, `Alpine Linux`, `Red Hat Enterprise Linux`, `Ubuntu 22.04`) routes to the distro comparator; previously such labels fell through to PEP 440 and could compare distinct revisions as equal.
- Migration `20261027_03` drops and rebuilds its partial index when an earlier `CONCURRENTLY` build was cancelled and left it `INVALID`, instead of keeping the invalid index and marking the schema version as upgraded.
- Retain package/ecosystem identity and credential-name redaction in the dedicated MCP AI inventory tool.
- Preserve validated sub-second upstream timeouts, disabled keepalive pools, and zero circuit cooldown in the gateway relay.
- Enable AI SDK inventory automatically for supported non-Python source trees, retaining package identity and source locations across project and repository scans.
- Graph times report when evidence was recorded, never when it was read. Governance overlay nodes and edges (managed identities, JIT grants, conditional-access policies, blueprints, drift incidents) take their times from their own records instead of the request clock, a graph loaded from Neptune keeps its snapshot time, and a scoped consequence graph keeps its source graph's time.
- On Python 3.11, registry-latest lookups no longer start a second request before the first npm 429 opens the cooldown: the 3 s lookup deadline runs in the caller's task (`asyncio.timeout`) on every supported Python.
- The same scan now builds the same graph. A node or edge built without its own time takes the scan's recorded time (the report's `generated_at`, or the stored snapshot's time) instead of the clock at the moment it was built or read, so two builds of one report are identical and no time claims an observation at build time. Finding nodes keep the first observation their findings recorded, audit-observed edges keep the aggregate's `last_seen`, a node seen by several sources spans the earliest first and latest last observation (previously the last source overwrote `last_seen`), and the demo estate stamps its snapshot time. Graphs carry the scan's time as `observed_at` (used for node and edge times and for edge freshness), while a stored snapshot's `created_at` stays the time it was stored, so retention never prunes a freshly imported report whose scan is old. A guard fails CI when a graph container is constructed without its observed or created time.
- Building the graph for a server shared by many agents no longer re-sorts the server's agent and alias lists once per agent: 2,000 agents sharing one MCP endpoint build in about 1 s instead of 3.6 s, with identical output.
- Version ordering no longer reports installed versions as past a fix they have not reached, which hid real CVE matches in four ecosystems. RPM compares `[epoch:]version` before the release as `rpmvercmp` does, so `2.4-12.el8` is affected by a `2.4.1-1.el8` fix (RHEL, Rocky, Alma, SUSE). crates.io/Cargo, Pub, Hex and Swift packages use SemVer 2.0 precedence instead of PEP 440, so `1.0.0-test.1 < 1.0.0` and `canary.1 < canary.2`. Go ignores `+incompatible` as build metadata (`v2.0.0+incompatible` equals `v2.0.0`) and orders pseudo-versions as the pre-releases they are (`v1.5.0-0.<ts>-<sha> < v1.5.0-rc.1`). PyPI dash-spelled `-dev1` / `-post1` releases normalise as PEP 440 requires (`4.2.0-dev1 < 4.2.0`), and local labels such as `+r1` are left untouched.
- Tenant isolation: the legacy SQLite backfills for agent identities and fleet agents rewrote rows by id alone, so two tenants sharing an agent id could overwrite each other's agent data. Both are tenant-scoped, and a guard fails CI on any SQLite write to a tenant-keyed table that filters by id without the tenant.
- `.csproj` parsing refuses XML entity declarations instead of expanding them, and `defusedxml` is a declared dependency.
- "Am I affected by CVE-X" answers from every alias record of a vulnerability. A CVE row that carries only distribution ranges no longer hides the PyPI, npm or Maven range on its GHSA alias, GHSA ids match regardless of case, and the advisory takes the worst severity across its records. Packages the scan itself flags (urllib3 for CVE-2023-43804, django for CVE-2022-34265, pillow for CVE-2023-4863) are now reported affected instead of clean.
- Dependency accuracy on real repositories: Gradle versions given as `$var`/`${var}` resolve from `gradle.properties` and `ext` blocks, and builds without a lockfile report partial coverage; `setup.cfg` and static `setup.py` dependencies are read (setup.py is parsed, never executed); requirement specifiers keep the whole range and environment markers; members of a lockfile-owned pnpm/npm/yarn workspace are no longer re-resolved to invented versions; Maven modules inherit local parent POM dependencies and managed versions; go.sum checksum verification reads the checksum database format and case-encodes module paths; go.mod `replace` blocks parse correctly; OSV records that enumerate git-tag versions no longer produce unverified matches.
- Health probes stay up when Postgres is down or stalled: probes bypass the shared rate limiter, the limiter falls back to memory when its store errors or is slow, database outages return a JSON 503, `/readyz` gives up after 3 s, and every chart probe sets `timeoutSeconds`. A request waits at most `AGENT_BOM_POSTGRES_POOL_TIMEOUT_SECONDS` (default 10) for a pooled connection instead of psycopg_pool's 30 s per attempt.
- API keys configured by the operator (`AGENT_BOM_API_KEYS`, the key file, `configure_api`) no longer expire after 30 days and no longer add a row on every boot; removing one from configuration revokes it. Without Postgres, keys minted through the API persist in the SQLite database instead of process memory, and mint, rotate and invitation responses report whether the key is durable.
- `agent-bom cloud aws|azure|gcp` without the provider extra stops before any discovery and prints the one install command.
- Databricks: model-serving endpoints and cluster libraries are inventory, not unauthenticated MCP servers (no more HIGH authentication findings on platform-managed endpoints); best-practice findings carry their workspace, so two workspaces no longer merge into one asset; audit-log delivery reports no data, not a critical error, when only workspace credentials are available.
- Light theme: notices that used near-white text are readable, and a guard forbids pale text without a dark-theme variant. The MCP catalog shows 25 servers per page.
- Registry documentation identifies temporary cloud resources created by opt-in disk side-scans and links the required secret bootstrap before production Helm installation. The docs landing page uses the verified reference lab instead of an unverified illustrative package/fix pair.
- Persisted tool-drift baselines are scoped to an explicit server identity and written atomically. Sessions without that identity keep their baseline in memory; legacy shared files are not assigned to an inferred owner.
- Release preparation keeps OpenClaw pull and signature-verification commands on the published version until artifacts are available; repeated version generation and validation agree.
- Runtime evidence retains bounded actor/session/cloud attribution, preserves available evidence when another store fails, and orders timestamp offsets in UTC. Structured graph IDs survive redaction while embedded credentials and shortened identities cannot bind.
- Databricks discovery retains resolved workspace scope and serializes incomplete security evidence without converting collection errors into passes. Workspace-user observations do not establish account-wide coverage.
- Malicious skill findings fail the default CLI gate; explicit audit mode retains a non-blocking review path.
- `agent-bom db update` shows NVD progress live: each `lastModified` slice with its date, the running record count and every rate-limit wait. Keyless NVD runs stop after a wall-clock budget of 8 minutes (`--nvd-time-budget SECONDS`, `AGENT_BOM_NVD_TIME_BUDGET_SECONDS`, `0` = no limit; keyed runs have none by default) as `PARTIAL` with `partial_reason: time_budget`, keeping the window checkpoint, and the summary says how to resume. The start-of-run note now describes the progress each selected source actually reports instead of saying "live progress follows" only for OSV.
- Keyless `agent-bom db update` no longer spends up to about 100 minutes on the NVD CVSS/CWE backfill: it makes at most 50 per-CVE requests per run, and rows whose CVE the `lastModified` window already scored take that score locally without a request. Keyed runs are unchanged.
- `/v1/graph` `stats.attack_path_count` now equals the `/v1/graph/attack-paths` total for topology-only snapshots instead of reporting 0. The node page reads the snapshot path index (one COUNT/MAX query, cached per tenant, scan and snapshot generation) and never loads the graph; a snapshot without a current index reports the queue's derived total once the queue has been read, and `stats.attack_path_count_basis` says which count (`queue_total` or `persisted_paths`) is shown.
- `agent-bom db update` now includes NVD by default (newest first, incremental), so CVEs published only in NVD, such as PHP2Shell (CVE-2026-109618/109619), reach the local database without `--source nvd`.
- `agent-bom db update --source nvd` with `NVD_API_KEY` read only the oldest 1,000 records of a 120-day window (about 91,000 modified CVEs) and printed `complete`, so the newest CVEs never landed. The window is now walked newest first in day slices, keyed runs cover the whole window by default (keyless runs read up to 60,000 records), the checkpoint records covered ranges so later runs fill the older tail without re-reading, and a capped, rate-limited or timed-out run shows `PARTIAL` in `db update`, `db status` and `db freshness`.
- The SSE/HTTP proxy (`agent-bom proxy --url`) now reads every page of the upstream `tools/list` before applying `--block-undeclared`; tools declared after the first page were blocked as undeclared.
- Proxy tool-drift detection reads a paginated `tools/list` (`nextCursor`) as one catalog instead of comparing each page with the first, so a multi-page server no longer raises false "new tools" and "tools removed" alerts. A listing that stops early (20-page cap, repeated or invalid cursor) is marked incomplete: it never becomes the baseline and never reports removals.
- Gateway drift enforcement looks up open incidents for the caller's own blueprint in the store (SQLite and Postgres, with a new index and Alembic revision `20261024_01`). Previously a tenant with 200 or more open incidents on any blueprints got `unavailable` for every lookup, which denied every call in secured enforce mode and dropped to audit-only otherwise.
- The `ai-enrich` extra requires `litellm>=1.96.2`, the first release clear of every LiteLLM advisory on OSV, including CVE-2026-84377. The lockfile already used 1.100.1; the floor stops a pip resolve from landing an affected version.
- Graph UI: a selected path title wraps before an advisory id (CVE, GHSA, OSV) instead of splitting it across lines; relationship labels stay inside the canvas after the node stepper or a pan moves the view, never cut at its edge; and the snapshot selector shows its full label when there is room, otherwise an ellipsis with the full label on hover.
- IaC findings can be accepted one at a time with an inline `agent-bom: ignore[RULE-ID] <reason>` marker on the offending line or the line above; a marker without a reason suppresses nothing, and accepted findings stay in the scan result's `suppressed` list. The reusable workflow uses one for its deliberate `@v0` action reference, so the repository's own scan passes.
- GitHub Action: the default scan covers the checkout, so Terraform, Kubernetes, Helm, CloudFormation and workflow files are scanned and the runner's home-directory MCP configs no longer leak into CI reports.
- GitHub Action: `scan-type` `image`, `fs`, `sbom`, `iac`, `secrets` and `code` no longer exit 2 with default inputs; only flags the selected command accepts are passed, and gates it cannot enforce fail with an error.
- GitHub Action: `baseline` passes `--delta` instead of the nonexistent `--delta-mode`, and `--baseline --delta` now suppresses pre-existing IaC, secret, skill and other non-CVE findings, matched without line numbers.
- GitHub Action: the vulnerability database cache uses the real path (`~/.agent-bom/db/vulns.db`) with a daily key, `setup-python` no longer fails on repositories without Python manifests, and inputs reach run scripts only through `env`.
- Terraform IAM wildcards are detected in `jsonencode` policies, `aws_iam_policy_document` data sources and the `"*:*"` form, at the offending line; Deny statements are not flagged. TF-SEC-010 now covers service-wide grants such as `s3:*`.
- Terraform public S3 detection reads `aws_s3_bucket_acl` and flags an `aws_s3_bucket_public_access_block` with any flag set to false; a block for another bucket no longer covers every bucket. TF-SEC-001 accepts a separate `aws_s3_bucket_server_side_encryption_configuration`.
- Docker Compose files are scanned (privileged, Docker socket mount, host network or PID, ALL/SYS_ADMIN capabilities, literal secrets in environment by variable name only). A named file no rule module evaluates now reads "not evaluated" instead of "no misconfigurations".
- GitHub Actions hardening gaps are real findings with rule ids, lines, SARIF results and the severity gate: `pull_request_target` checking out the PR head (GHA-001), actions not pinned to a commit SHA (GHA-002), untrusted `${{ github.event.* }}` input in `run:` (GHA-003) and `write-all` or missing top-level permissions (GHA-004).
- Kubernetes findings report the line of the offending key (hostPath, image, missing limits, probes, env secrets, capabilities) instead of line 1, including in multi-document manifests.
- Helm chart templates are evaluated: rendered with `helm template` and default values when `helm` is on PATH, otherwise scanned textually for privileged and host-namespace keys, with coverage stating that rendering was skipped. Templates no longer count as a clean Kubernetes scan.
- Duplicate rules no longer report one issue twice: TF-SEC-008 and CFN-013 are retired, DOCKER-004 covers only local `ADD` and DOCKER-010 only tagged images without a digest. Probe, HEALTHCHECK, logging, retention and versioning rules are rated low.
- Display redaction no longer hides boolean settings such as `automountServiceAccountToken: true.`.
- SARIF results for skill and instruction files use the real relative path and line instead of `<path:CLAUDE.md>`, a redaction label is never emitted as an artifact URI, and SARIF tool links point at the current repository. The skill-audit report detail no longer includes the absolute host path.
- `scan -p` still scans the project when `--image` or code paths are also given, including the default run with Semgrep installed.
- The hub partition runway job logs the expected DDL permission denial as one line instead of a traceback.
- Graph exports and graphs rebuilt from a report name a cloud asset with the node id and entity type its finding row declares (`node_id` with `entity_type`), in both the finding and the CIS-benchmark projections, instead of re-deriving `cloud_resource:<provider>:<identifier>`. The showcase job's graph export now joins the showcase snapshot on 727 cloud-resource and data-store nodes (previously 3) and on 1,233 nodes once the roles, clusters, service accounts and principals, models, agents and servers the rows declare are counted (previously 18). That includes the 12 multi-source compute VMs under their merged `cloud_resource:aws:aws_instance:…` id rather than the posture row's ARN, which stays the node's `resource_id`. Demo-estate findings now record their asset node's entity type. Buckets, databases and roles therefore no longer gain a `data_store:cloud_resource:…` companion in the export, and the ASPM overlay no longer attaches an asset whose kind the relationship contract does not let belong to an application.
- The demo-estate showcase names its 5 agents, 10 MCP servers, 26 tools, 15 credentials and its provider with the ids a scan derives (`agent:cfg-<hash>:cursor`, `server:cfg-<hash>:cursor:<uuid>`), so the showcase snapshot, `/v1/scan/{job_id}/graph-export` of the showcase job and `agent-bom scan --demo` join on every agent, server, tool, credential, provider, package and vulnerability id. The former short ids (`agent:cursor`, `server:shell-runner-server`, `cred:aws-secret`) resolve as node aliases; exposure-path ids through these nodes change. `scan --demo` no longer edits the shared demo inventory in place or derives agent ids from `$HOME`.
- Snapshot diffs (`/v1/graph/diff`, `/v1/graph/changes`, MCP `graph_diff`) compare a node whose id scheme changed under its new id when the new snapshot records the old id as an alias. It is unchanged, or changed with a `previous_id`, instead of removed plus added; edges follow their endpoints. The demo showcase diff reports 3 removed nodes instead of 15.
- While the demo estate is seeding, an unpinned `GET /v1/graph` returns 503 `current_estate_seeding` with `Retry-After` instead of an empty graph marked complete whose generation changes between reads. Pinned reads and the seed's own reads are unaffected.
- Exposure-path ids include the per-hop relationships, identically on REST, MCP and the path payload, so two stored paths over the same hops through different relationships no longer share an id. Paths recorded without relationships keep their hop id.
- `GET /v1/graph` and `/v1/graph/attack-paths` follow one cursor rule: the cursor carries the snapshot generation, so `snapshot_generation` is optional on a continuation and a mismatching one returns 409. `/v1/graph` no longer returns 422 for a cursor without the token, and attack paths no longer return 400 when a matching token is sent.
- Graph exports join REST and MCP: `/v1/scan/{job_id}/graph-export`, `agent-bom graph` and `scan -f graph` use the canonical node id (`pkg:<ecosystem>:<name>@<version>`, `vuln:<id>`, `agent:…`, `server:…`, `misconfig:…`) and an `entity_type` from the graph enum on every node. Display kinds such as `pkg_vuln` and `server_cred` stay in `kind` / `type`; the old package and advisory spellings stay in `attributes.legacy_id`.
- An unpinned `GET /v1/graph` whose evidence changes during assembly rebuilds a bounded number of times, then returns 503 with `Retry-After` instead of 409. A pinned generation that changed still returns 409.
- Feed re-match pages on (modified, id), so records sharing a timestamp are no longer skipped, honours an advisory's `limit_version` through the same range check as scan time, and marks a run `incomplete` when tenant or job caps are reached.
- MCP `intel_lookup` reads advisories declared through the API when a store is configured.
- Cross-vendor paths never report `has_more` with an empty cursor. Search ranks an exact id, id suffix or label first.
- Every path-taking MCP tool returns the same validation error for a path outside the allowed roots, naming `AGENT_BOM_MCP_WORKSPACE_ROOTS`, instead of "An internal error occurred". `inventory` now applies the same check. Tool calls that return an error are logged and counted as failures.
- The standalone `agent-bom mcp server` in demo mode reads the demo database. Image tarball scans no longer include the host's agents.
- The overview vulnerability card counts with the query its link opens, so the card and the findings list agree. Demo trust scores use the 0–100 scale the scorer and UI use. Mixed-version issue titles read "ws · 2 versions".
- Credential counts say what they count on every surface: held by the server, or exposed by the advisory's impact.
- Graph nodes keep their canonical type, layer and evidence when types share a renderer; recorded evidence is no longer shown as runtime; advisory ids are never clipped.
- Persona journeys: the remediation link no longer 404s, overview counts no longer change after first load, and a queue row no longer mixes two package versions.
- CIS results come from the graph, and a control passes only with a recorded passing result; missing results are `not_evaluated` and excluded from the score.
- Graph change exposures page their top rows, and removed paths link to the previous snapshot.
- NVIDIA CSAF advisories attach only to the release branch they fix, and NVD ranges scoped to another platform are skipped.
- Documentation and shipped manifests reference only CLI commands that exist (the proxy bootstrap bundles and the Kubernetes DaemonSet called removed commands); the check now verifies every word of a cited command. Version bumps update the OpenClaw, first-run and CloudFormation image pins.
- `agent-bom serve` and `agent-bom api` exit when the Postgres control plane cannot initialize, instead of logging "lifespan appears unsupported" and answering every request with 500. The error names the missing migration or runtime-role grant.
- `agent-bom scan -p <project> --sbom <file>` scans the project's manifests and MCP configs as well as the SBOM. `-p` with `--no-discover` still discovers the project's own MCP configs, as MCP `scan` does with `config_path`.
- Demo package nodes carry their name, ecosystem, version and purl, and no longer carry cloud exposure flags. Each demo scan's summary counts its own active findings, as the findings API does. Demo AWS accounts use only AWS documentation example ids.
- zod updated to 4.6.7 for GHSA-mrhx-c7xc-m3w5 (development dependency).
- MCP `scan`, `where`, `compliance`, `policy_check`, `runtime_correlate`, live `context_graph` and the local `blast_radius` fallback no longer read the server host's agent configs over HTTP or SSE without `config_path`; they return `AGENTBOM_MCP_AUTH_FORBIDDEN` unless `AGENT_BOM_MCP_ALLOW_HOST_LOCAL_TOOLS=1`. A `scan` of an explicit image, SBOM or package skips host discovery with a warning.
- Cloud best-practice results imported from an external scanner are cloud posture, not vulnerabilities, so `domain=vuln` equals the overview Vuln / SCA card (the demo listed 788 against the card's 785). Rows stored under the old lane read as cloud posture too.
- Advisory re-match past its tenant cap serves the tenants with the oldest attempt first, so every tenant is reached within ceil(tenants / cap) runs instead of the same alphabetical tail being skipped every run.
- The remediation plan counts the credentials the affected servers hold (names only) beside the credentials exposed by the advisory's impact, in the API (`server_credentials`, `server_credential_count`; `null` when a finding did not record them), the remediation page, the scan result, console and JSON output.
- `GET /v1/graph` bounds a page's stack and containment context at `completeness.context_limit` (`max(limit, 500)`), keeping the agent stack first, then ancestors from the top of the spine down; `context_omitted` and `context_truncated` report the cut. Every context node carries `context: true`, so ranked nodes are told apart; each node is ranked on exactly one page of a cursor walk, and context may repeat across pages by contract. On the demo, the default `limit=500` page drops from 1,548 to 1,321 nodes and keeps its org and account spine, and the first `limit=1000` page drops from 2,571 to 2,328. The UI's default 50-node page is unchanged.
- The dashboard keys attack paths by the backend `exposure_path_id` (falling back to hops plus per-hop relationships), so two paths over the same hops through different relationships no longer share a selection, card or queue row. Graph change highlights carry `exposure_path_id`, their "Investigate" links select that exact path, and a path that differs from the previous snapshot only by relationship is reported as new.
- While the demo estate is seeding, every unpinned current-estate read refuses with 503 `current_estate_seeding` and `Retry-After`, not only `GET /v1/graph`: attack and exposure paths, search, agents, rollup, node and impact reads, `POST /v1/graph/query`, `should-i-deploy`, `/v1/inventory/assets` and `/v1/inventory/summary`, plus MCP graph tools in the same process (`AGENTBOM_MCP_UPSTREAM_UNAVAILABLE` with `details.reason`). Previously 22 of these 23 reads answered 0 items with `complete: true` during the seed. Reads pinned to a recorded scan are still served, and the seed's own reads are never refused.
- A graph read whose `scan_id` (or `scan`) names no snapshot of the caller's tenant returns 404 instead of 200 with no nodes and `complete: true`. This covers `/v1/graph` and the 14 sibling reads that served an empty page (agents, attack and exposure paths, clusters, governance, rollup, scoped, node neighbors, incident edges, fix-first, `should-i-deploy` and more); MCP `exposure_paths` and `should_i_deploy` return `AGENTBOM_MCP_NOT_FOUND_RESOURCE`.
- `/v1/graph`, `/graph/search`, `/graph/agents` and `/v1/inventory/assets` reject a `cursor` sent with a non-zero `offset` with 400, as the attack-path queues do, instead of serving the cursor's page and echoing the ignored offset.
- A cursor that does not decode, or that belongs to another tenant, query or replaced incident-edge page, is a 422 on every graph and inventory route. `/v1/graph`, the attack-path queues, incident edges and inventory assets returned 400, and `/graph/agents` returned 500.
- Graph exports of an `--sbom` input or a project's repository inventory name them as the graph does (`source_file:sbom:…`, `directory:repository:…`, `directory:manifest:…`) instead of an agent, an MCP server and a `provider:sbom` / `provider:project` node the graph never draws. 6 export ids that joined nothing in a scan with both inputs now join.
- The showcase snapshot places each demo code finding on the file it names (`source_file:` / `config_file:`, the ids a repository scan gives those files), and a finding row's declared `finding_node_id` names its node in report-built graphs. The showcase job's graph export now joins the showcase snapshot on all but 10 of 2,481 nodes (previously 45): the 8 data-store companions of Snowflake warehouses and the 2 tool-drift rows the showcase draws as `drift_incident` governance nodes.
- AI source scans apply the shared placeholder policy to credential matches while retaining masked findings for non-placeholder values.
- AI source scans detect OpenAI Agents SDK imports as `openai-agents` and reject similarly named Python modules such as `llama_index_helpers`.
- CycloneDX vulnerability ingest into the compliance hub resolves `affects[].ref` against nested components and the metadata component, so a vulnerability on a nested library keeps its package purl instead of falling back to the bare vulnerability id.
- Kubernetes manifest scans report a container as root (K8S-005) when `runAsUser: 0` or `runAsNonRoot: false` is set only in the pod-level `securityContext`, as live posture (K8S-LIVE-011) already did; a container-level value still overrides the pod's. Privileged containers (K8S-001) now carry the ATT&CK T1611 escape technique like K8S-LIVE-007, and live posture no longer reads `runAsUser: false` as UID 0. Both scanners share one set of securityContext checks, and the live pod health checks (K8S-LIVE-001..005) moved next to the other live posture checks with unchanged output.
- The MCP `scan` tool's `image` argument builds the same image carrier as `scan --image` and API scans, so its agent and server ids now match theirs (previously the server was named `image:<ref>` with an unknown transport and the agent had no `docker://` config path). The image scan also runs off the MCP event loop. Filesystem, OS-package and image-archive carriers come from the same builders on every surface.
- Image references with both a tag and a digest (`ghcr.io/org/app:1.2@sha256:…`) are split the same way everywhere: the container image SBOM no longer reads the repository as `app:1.2@sha256` and the digest as the tag, and cloud (AWS, GCP, Kubernetes GPU), `docker run` MCP and Docker Compose packages name the image without its tag. Compose images pinned by digest now carry the full digest as their version instead of its first 12 characters, and `localhost/<name>` is read as a local registry.
- Image layer provenance (`created_by` on package occurrences) redacts credentials passed as build args or `ENV` values instead of copying them into reports.
- A `--policy` result no longer clears an earlier failed gate (image signature or model provenance policy) in the scan exit code.
- Dockerfile rules evaluate whole instructions: continuation lines (including a `# escape=` character) are joined and reported at the first line, and heredoc bodies stay with their `RUN`. A package install, `curl | sh` or `chmod 777` on a continuation line was missed, and cache cleanup on a later continuation line was ignored. `yum`/`dnf`/`microdnf`/`zypper` installs and their `clean` commands and BuildKit cache mounts are recognised.
- `DOCKER-002` judges the final build stage: a root builder stage, or `USER root` followed by a non-root `USER`, is no longer flagged; a final stage without `USER` is flagged even when an earlier stage set one (`FROM <stage>` inheritance is followed, `:nonroot` base tags are honoured). `FROM --platform=...`, stage-alias references and `ARG`-defaulted base images no longer produce wrong `DOCKER-001`/`DOCKER-010` findings, and an unresolvable `FROM $VAR` is not guessed.
- `DOCKER-003` checks every `KEY=VALUE` pair on an `ENV` line and no longer flags `*_FILE`, absolute-path or `${VAR}` values; `WORKDIR $HOME/app` is not reported as relative. `Dockerfile.md`/`Dockerfile.txt` and other look-alike docs are not parsed as Dockerfiles, and a file without `FROM` gets no findings.
- Dockerfile ATT&CK tags match what each rule detects (for example `DOCKER-001` was tagged Escape to Host and `DOCKER-006` Exploit Public-Facing Application); Compose and daemon rules are now tagged.
- Findings reachability joins derive each attack path's identifiers once per snapshot index instead of once per finding row. This reduces repeated work during facet aggregation; requests that reach the existing deadline still report partial counts as lower bounds.
- The AI BOM (`/v1/agent-bom/manifest`) no longer shows a blank transport for an MCP server whose endpoint sync omitted it: it reports the newest transport any observation of that server recorded, and `unknown` when none did.
- `agent-bom iac <path> --format console` lists the findings instead of one count per path: a severity-sorted table of severity, rule id, `file:line` and title, capped at 25 rows with a `+N more finding(s)` line pointing at `-f json` / `-f sarif`, folded to fit 80 columns. `--verbose` lists every finding and `--quiet` omits the table. The per-path and live-cluster summaries count every severity band instead of only critical and high.
- SBOM import sets an OS package's distro from its purl, as an image scan does from `/etc/os-release`: the namespace (`pkg:rpm/redhat/…`, `rocky`, `almalinux`, `amzn`, `opensuse`, `pkg:deb/debian/…`, `ubuntu`, `pkg:apk/alpine/…`, `wolfi`) and the `distro` qualifier (`rhel-9.4`, `el9`, `debian-12`, `3.19.1`). RHEL, Rocky, AlmaLinux and openSUSE RPMs from an SBOM now match their distro advisories instead of reporting `rpm-unknown-distro`, and Debian, Ubuntu and Alpine packages match their own release instead of every release. Codename-only releases are left unset rather than guessed.
- Explicit options on `image`, `fs`, `sbom`, `iac`, `cloud` and the AI-platform commands now win over the active profile. These commands forward to `scan`, where every forwarded value looked like a default, so a `format = "json"` profile turned `agent-bom image … --format console` into a JSON report file.
- NVIDIA GPU driver CVEs on Kubernetes nodes were claimed from a fixed `< 555.52` floor that ignored driver branches (every patched 535/550 LTS driver was flagged), and AMD/Intel driver CVEs from similar floors. Node drivers are now CPE-identified packages matched by the NVIDIA security bulletins (branch-aware fixed versions) and NVD, like drivers found on hosts and images.
- `serve --cors-allow-all` no longer sets `AGENT_BOM_CORS_ALL`, which nothing read; CORS is configured from the flag directly. A guard test fails when the runtime writes an `AGENT_BOM_*` variable that nothing reads.
- SPDX 2 tag-value export now carries every field of the JSON rendering the grammar can express (`PrimaryPackagePurpose`, `PackageComment`, `PackageHomePage`, summary/description) and writes annotations as spec `Annotator`/`SPDXREF` blocks instead of repeated comment tags, so agent and server context records are no longer read back as software packages. The cloud-inventory extension moved from `DocumentComment` to a document annotation.
- A GKE pod's node VM and the GCE inventory or side-scan VM are now one graph node, so an OS CVE on the node disk is an attack path through the pod.
- RPM packages from a distro without a supported advisory feed (Amazon Linux, Oracle Linux, SLES, Fedora, Azure Linux, any unrecognised distro, or a supported distro whose release is unknown) were matched against OSV's upstream `Linux` kernel ecosystem, so a distro `kernel` with backported fixes reported upstream CVEs and upstream fix versions. They are no longer matched against any ecosystem; every scan surface instead reports an `advisory_source_unavailable` coverage warning naming the distro release, even for a single package, so the result never reads as clean. The intel package-match API no longer routes `rpm` coordinates to `Linux` either.
- Terraform TF-SEC-007 (hardcoded SSH key) and TF-SEC-051 (hardcoded secret) apply to every provider's resources but tagged all of them with `CIS-AWS-1.14`; the CIS AWS control is now attached only to `aws_` resources, and Azure, GCP and other resources carry the provider-neutral NIST controls.
- TF-SEC-051 detects Azure's `administrator_login_password`, and TF-SEC-007 detects SSH keys in GCP instance `metadata` `ssh-keys`.
- SARIF IaC rules no longer copy the first finding's message, which named one resource, into the rule `fullDescription`, or its compliance IDs into the rule: the rule carries its title and the compliance IDs shared by every result, and each result carries its own `properties.compliance`.
- Kubernetes GPU nodes read the full driver version from GPU Feature Discovery's `nvidia.com/cuda.driver-version.full` label instead of a `major.minor` prefix.
- Each CVE on a package now carries only its own `ai_risk_context`. The context was extended in place across a package's CVEs, so the Nth CVE repeated the attack summaries of all before it; on `php:8.3.30-cli` that built about 0.9 GB of text for `linux-libc-dev` and dominated JSON export time.
- Image rescans distinguish actionable vulnerabilities from incomplete scan coverage. Daemon signature lookup uses a qualified observed repository digest; automated image issues close only with complete successful scan evidence.
- Python AST framework discovery labels OpenAI Agents SDK imports and retains LlamaIndex detection.
### Security
- `--require-signature`/`--require-image-signature` now needs a cosign public key or a Sigstore signer identity and issuer (`sigstore` extra) (`--cosign-key`, `--image-cosign-key` or `AGENT_BOM_IMAGE_COSIGN_KEY`): without one the gate fails with "a cosign public key is required to enforce signatures", because a signature tag anyone can push proves nothing. A signature that was found but not verified is reported as `present-unverified` (it was `signed`).
- Server-side image scans (API scan jobs and the MCP `scan` tool) no longer act on a tenant-supplied reference with the server's own network and credentials: HTTPS only; loopback, private, link-local, CGNAT and cloud-metadata registries are refused (address literals up front, DNS answers at connect time); `~/.docker/config.json`, credential helpers and `AGENT_BOM_REGISTRY_USER`/`PASS` are never used (anonymous pulls only); `AGENT_BOM_REGISTRY_ALLOWLIST` (comma-separated hosts) limits which registries may be contacted; a label-named base image is fetched only from an allowed registry; the Docker daemon is not used unless the operator sets `AGENT_BOM_IMAGE_PULL_MODE=daemon`; and errors no longer carry the upstream HTTP status. The local CLI is unchanged.
- In `auto` pull mode a reference without a registry host that exists as a local Docker image is scanned from the local image instead of a same-named (possibly squatted) Docker Hub image.
- Registry client: manifest and referrers bodies stop reading at the 8 MB cap instead of being read whole first; a 401 from a redirect host no longer receives the registry's credentials; `sha512` manifest digests are verified and unknown digest algorithms rejected.
- Image labels and annotations go through the secret scanner: credentials in them are redacted and reported as findings, and MCP image scan results carry an `untrusted_content_notice`.
- Regular expressions in the Dockerfile rules, registry auth-challenge parsing and image config secret scanning no longer backtrack catastrophically on hostile input (a crafted `apt-get` line took 21.8 s at 11 option tokens); a guard test checks every pattern in the IaC, image-evidence and registry modules.
- The `ai-enrich` extra excludes litellm 1.107.0, whose wheels were a compromised release (startup-hook credential stealer, 2026-10-21), so installs without the lockfile cannot resolve it. The lockfile already pinned 1.100.1. A guard test fails if any dependency range or the lockfile admits a known-compromised release, and `agent-bom check litellm@1.107.0` reports it as malicious (MAL-2026-2144).
- Image scans (`--image`, `--image-tar`, `--k8s`, the MCP `scan` tool, API scans) project as container image nodes keyed by digest instead of agents with MCP servers; earlier node ids are kept as aliases.
- The packaged scanner reader role (Helm and `deploy/k8s/rbac.yaml`) grants get/list on workloads, service accounts, role bindings and admission objects so in-cluster inventories are complete; it still grants no Secrets and no mutation verbs.
- `-o report.json.gz`, `report.sarif.gz`, `bom.cdx.json.gz` (and `.spdx.json.gz`) write a deterministic gzip stream; `report diff` and `--sbom` read gzip input (SBOM inflation is capped at 10 MiB). CycloneDX, SARIF and SPDX exports now use the JSON report's streaming writer, which encodes each item of a top-level list separately and publishes by atomic replacement, instead of building one string for the whole document.
- `GET /v1/scan/{job_id}/result` returns a completed scan's summary and an index of its large sections with item counts, and `GET /v1/scan/{job_id}/result/{section}?offset=&limit=` returns one section, paged when it is a list, redacted exactly as the full result. `GET /v1/scan/{job_id}` is unchanged for existing clients; its redaction and JSON encoding now run in a worker thread, and the Starlette floor is raised to 1.4.1 so responses of 128 KiB or more are gzip-compressed off the event loop.
- SBOM import reads CycloneDX XML (1.2-1.7) and SPDX 2.x tag-value in every intake (`scan --sbom`, the API and MCP scan paths, `report diff`). The format is detected from content; both are re-shaped onto the JSON document form and read by the same parser, so each yields exactly the packages, hierarchy, vulnerabilities and cloud-inventory evidence of its JSON twin. XML goes through `defusedxml` with DTDs forbidden (no entity expansion, no external entity or DTD retrieval) and bounded nesting; every import keeps the 10 MiB cap. Previously both were rejected.
- Console output groups kernel-header CVEs into one block per image distro release (Debian/Ubuntu `linux-libc-dev` and `linux-headers-*`, RHEL-family `kernel-headers`/`kernel-devel`, SUSE `linux-glibc-devel`/`kernel-*-devel`, Alpine `linux-headers`), with the full severity breakdown; the scan verdict says how many of the CVEs they are. KEV and critical kernel-header CVEs are named on the group line and stay as their own rows, and JSON, SARIF and CycloneDX still carry every CVE. Recognition is scoped to the ecosystem and `os-release` family, so a same-named package elsewhere is never grouped.
- The `docker export` fallback of `agent-bom image` scans the exported filesystem with the same bounded walk and extractors as image layers: the `AGENT_BOM_OCI_*` member, size and time limits apply, and a crossed limit is a coverage gap under `docker-export` (a partial scan) rather than an unbounded read.
- Image layers emit the canonical purl for scoped npm packages (`pkg:npm/%40scope/name@…`), matching every other scan path.
- Pull request CI runs the affected tests instead of the full suite: the changed-domain map plus every test that directly imports, loads or references a changed module, helper, `conftest.py` or fixture (`scripts/pytest_ci_affected.py`), with the contract smoke, in 1–12 duration-sized `Python affected tests (shard N/M)` jobs bounded at 15 minutes. Shared or unmapped inputs no longer start the full 4-shard suite on a PR; the full four-version matrix, full musl suite and four UI E2E shards run after merge and nightly as before. PRs run the musl subset only for dependency/build/workflow inputs and one `UI E2E smoke` job (every packaged route plus accessibility) only when UI sources change. Test jobs report the 50 slowest tests. `docs/operations/CI_RUNBOOK.md` lists the eight required checks and the merge-queue prerequisites.
- GPU discovery no longer runs `kubectl exec` into the cluster (`nvidia-smi`, `rocm-smi`, i915 sysfs reads in a pod) when GPU Feature Discovery labels are missing; the driver version stays unknown instead. A guard test fails when any collector builds a mutating or exec-style `kubectl` command (`exec`, `cp`, `apply`, `delete`, `patch`, `create`, …).
- Repository scans reject file symlinks across the checkout boundary, enforce clone resource budgets during execution, and bound serverless archive downloads and expansion. Coverage gaps remain explicit.
- Credential URI matching in dbt configuration avoids super-linear backtracking on hostile scheme input.
- Runtime tool-definition drift includes bounded fingerprints of annotations, output schemas, titles, execution metadata, icons and extension fields; invalid or oversized declarations preserve the previous baseline.
- Tenant-scoped disk side-scans require an authorized `client_factory`; the ambient provider-credential fallback is reachable only from the local CLI (`allow_ambient`). API, scheduler and MCP callers already passed the tenant-bound factory.
- Daemon-mode image scans on the server apply the server registry policy (allowlist, pinned DNS, no local credential helpers) to signature, referrer and base-image lookups, as registry-mode scans already did.
- cosign ECDSA verification selects the digest by curve (P-384 → SHA-384, P-521 → SHA-512), so valid P-384/P-521 signatures are reported `verified` instead of `invalid`.
### Performance
- JSON reports (`-o report.json` and `--format json` on stdout) are redacted and encoded section by section instead of building a redacted copy of the whole report and encoding each section in one call. A local 40,000-package offline scan measured 718.5 MiB peak RSS and 77.9 seconds with all 200 seeded advisories retained; streaming output matches the existing encoder byte for byte.
- `agent-bom image` on `php:8.3.30-cli` (4,424 findings) peaks at 413 MiB for console and 758 MiB for JSON output instead of about 2.5 GiB, and JSON output uses 52 s instead of 277 s of CPU, with an identical finding set (`docs/perf/image-scan-end-to-end.md`). Text redaction is memoized for the life of one scan (bounded at 8M input characters and cleared when the scan ends), and the inventory snapshot, graph-store compliance stats and typosquat check no longer do quadratic work over large package lists. A 40,000-package offline manifest scan is measured after every merge against a 1,000 MiB peak-RSS budget.
- Kubernetes workload graph from live clusters (`scan --k8s`) and manifests/Helm charts (`--iac`, API repository scans): `cluster → namespace → workload → image → package → CVE`, with each workload's service account, its RBAC grants (scope and Secret access) and the Secrets it references. Live pods collapse into their owning controller and images are scanned at the running digest. A new attack-path family follows a vulnerable workload through the service account it runs as to the Secrets and scopes it can reach; a pod with no mounted token never starts one. The report carries a `kubernetes_inventory` section (each container with its image's scan result), the console prints a most-vulnerable-first workload table, and the new read-only `kspm_workload_inventory` MCP tool returns the same inventory. Secrets are never listed.
- Read-only admission posture in live KSPM: namespaces running workloads without a `baseline`/`restricted` Pod Security enforce level, audit-only ValidatingAdmissionPolicy bindings, fail-open policies and webhooks, and a cluster with no policy control mechanism (CIS Kubernetes 5.2.1). Manifest rules `K8S-037`/`K8S-038` cover Namespace documents.
- Disk side-scan packages and CVEs project onto the VM's own inventory node (EC2 instance, Azure VM, GCE instance) and reach attack paths; `POST /v1/cloud/side-scan` returns per-package/CVE detail and stores it in the tenant graph, and `agent-bom cloud side-scan -f json` and the MCP tool return the same records. A `side_scan_results` report section carries them.
- Machine-image side-scan: AWS AMIs (from the AMI's root snapshot, never modified or deleted) and GCE images. Azure managed images are refused with the reason in the CLI, API and MCP. `side-scan-capabilities` states image support per provider and that every side-scan creates and deletes temporary disks.
## [0.110.0] - 2026-10-21
### Changed
- **Outdated local vulnerability data now fails closed.** Applies when nothing is at or above the gate, the local DB is past the age `agent-bom db freshness` warns about (or its freshness can't be trusted), and the run was offline or live OSV did not answer. `check` then exits `2` (incomplete), `scan` exits `1` with a `stale_local_data` issue in `scan_run`, and MCP `check` / `POST /v1/scan/check` return `status: "incomplete"`. Real findings keep their exit codes, and `--exit-zero` does not turn this into a pass. Air-gapped installs that accept old local data pass `--allow-stale-db` or set `AGENT_BOM_ALLOW_STALE_DB=1`. `check` now honours `--require-fresh-db` / `AGENT_BOM_REQUIRE_FRESH_DB` (exit `3`) like `scan`.
### Added
- Scoped Supabase project and organization inventory, Vercel project/deployment and team inventory, and Modal/NGC metadata exports. Supabase and Vercel directory graphs retain users and configured roles without inferring effective access; incomplete collection preserves the previous local graph.
- Findings queues separate evidence lanes, and scoped HTML impact briefs provide expandable supporting evidence. Installed Python-package metadata can be compared against a retained advisory with scoped remediation receipts; this does not verify a running workload.
- Claude Code plugin (`integrations/claude-code`, marketplace at `.claude-plugin/marketplace.json`): bundles the MCP server, pinned to the published PyPI release through `uvx`, plus `/agent-bom:scan`, `/agent-bom:check` and `/agent-bom:fix` skills. `scripts/check_version_alignment.py` keeps the plugin version and the `uvx` pin on the published release.
- Exploit story for a CVE occurrence: `GET /v1/graph/exploit-story` and the MCP `finding_detail` `exploit_story` block say what the vulnerable package puts at risk (agents, MCP servers, tools, credential names, data stores, cloud resources and identities, each with its hop-by-hop path and evidence tier) and how it could be exploited (decoded CVSS 3.x/4.0 vector, CWE attacker capability, KEV, EPSS, reachability, fix), with a narrative built only from recorded fields. The finding drawer and the investigation path show it, and the path canvas can draw the at-risk hops past the package.
### Fixed
- Graph and finding identities retain repository, account, tenant and occurrence boundaries; current-estate reads preserve snapshot scope and relationship selection. Shared image inventory remains distinct from workload instances, and configured identity scope does not imply tool access.
- The terminal demo recording keeps complete finding rows when the current table exceeds its viewport and waits for the first scene before recording. Refreshed product screenshots and the recording from current source, corrected directory collection guidance, and moved the Claude Code plugin entry out of the already-published v0.109.0 history.
- `agent-bom check` no longer reports results from an outdated local vulnerability DB as complete. Whenever `agent-bom db freshness` would warn (past the configured max age or the 7-day danger age), online checks also query OSV live and merge by advisory ID and alias. Previously a 7-day danger age above a raised `AGENT_BOM_VULN_DB_MAX_AGE_HOURS` skipped OSV and missed newer CVEs. Offline checks and failed live lookups say plainly that results may be incomplete and how old the local data is, and local hits from outdated data no longer print a check mark. CLI JSON, MCP `check` and `POST /v1/scan/check` add `vuln_data_freshness` and `completeness`, and the MCP `scan` summary now includes `vuln_data_freshness`.
- Vulnerability-data freshness fails closed. A local DB written by a newer agent-bom schema, or one whose sync metadata cannot be read, is no longer treated as fresh: `db freshness` warns, `check` also queries OSV live, and offline results are marked incomplete. `vuln_data_freshness.integrity_issue` names the problem. A freshness probe that errors now counts as outdated instead of failing the check.
- `check` on an outdated local DB reports `complete` only when OSV actually answered for the package in that run (a completed OSV batch or a response-cache hit). Before, it was inferred from the absence of errors. When OSV did not answer, the result is `incomplete` with "live OSV did not answer for this package".
- Advisory merging no longer folds distinct advisories into one finding through a shared alias. A local DB that kept a stale upstream alias group (for example CVE-2021-23337/GHSA-35jh fused with CVE-2026-4800/GHSA-r5fr) made `check` report 3 lodash@4.17.20 findings instead of 5, and showed CVE-2021-23337 as fixed in 4.18.0. Now a cluster never holds two different primary CVEs, a record's own CVE id outranks a CVE alias, and a stored record whose aliases name several CVEs is matched by its own ID only, with a warning to run `agent-bom db update`. Live OSV alias sets reconnect each advisory to its GHSA, and every finding keeps its own fix version.
- EPSS and KEV from the local vulnerability DB no longer cross a stale fused alias group. A `CVE-…` row takes only its own scores. Other rows take a single CVE alias, plus their upstream CVEs as before. A row whose aliases name several CVEs takes none, so CVE-2021-23337 no longer inherits CVE-2026-4800's EPSS or KEV.
- A merged finding keeps the fix version of an alias record when the record chosen for severity has none. For example, a local CVE-2020-28500 row with no fix merged with the live GHSA-29mw-wpgm-hmr9 record now reports `fixed_version` 4.17.21 instead of none. The fix only ever comes from a record for the same package. For Debian, Alpine and RPM packages an empty fix still means no fix for that release.
- `GET /v1/graph/node/{node_id}` and `/v1/graph/node/{node_id}/neighbors` accept node ids that contain `/` (cloud resource ARNs, file paths, scoped packages), raw or `%2F`-encoded, instead of returning 404.
- `GraphRollupResponse.collapsed` is typed as a list of rows in the OpenAPI contract and the generated UI types, matching what the attack-path roll-up returns.
- CVEs found on VMs roll up under their instance instead of appearing as graph orphans, carry their occurrence verdict and effective reach on the advisory node, and no longer count their own instance as a source that reaches them.
- MCP `blast_radius` reads its holders from the same graph walk as `GET /v1/graph/impact` (a `holders` block, with per-row servers and agents taken from it) and returns each row's derived `reachability`, so MCP and REST no longer report different holders for one advisory.
- Findings, the `reachability` filter and facets, attack paths and MCP read one reachability verdict derived from the stamped graph evidence, so a finding reached by a provenance-backed path reads `likely` (never `confirmed` without function-level or runtime evidence) and `?reachability=reachable` returns only `likely`/`confirmed` rows.
- `GET /v1/findings` cursor pages report the same exact `total` as the first page (`total_kind: exact`) instead of `null`/`unknown`; a cold count cache runs one COUNT, approximate reads still skip it, and scope-filtered walks keep the honest marker.
- The MCP `context_graph` tool builds from the tenant's persisted scan snapshot (or `scan_id`) by default and labels its `source` and `scope`; live discovery of the MCP host's own agent configurations runs only with `live=true` or a `config_path`.
- One count per metric across pages. An issue's severity is now its worst open occurrence, so the findings page (sorted by reach) and the overview no longer disagree by one high or medium issue. An explicit `CVE` finding reported by a cloud source is classified as a vulnerability, so the overview's open-CVE tile matches the posture dimension. KEV is counted in issues beside the issue tiles and in the exec summary (`issues.kev`; occurrences remain in `finding_counts.kev`). Remediation reads the findings default window and returns it as `window`.
- Units and scopes are named wherever two numbers could be read as one: overview security areas say "occurrences", the agents page says "Agents on this host" with the estate count beside it and counts unique credentials, the posture dimension says "vulnerability occurrences", the compliance page counts control frameworks and risk mappings with the overview's definition, and the NIST SP 800-53 panel leads with the framework list's pass/fail and labels the catalog rescoring as a separate basis.
- Agent and server names come from one resolver: a recorded display name, otherwise one canonical formatting of the identifier; exposure paths no longer append "service" or "connector".
- Native trace imports expose bounded provider retry timing while retaining the last committed checkpoint on failure.
- Bulk findings require a JSON boolean for absent-finding reconciliation; invalid CVSS errors omit submitted values.
- JSON file exports publish through atomic replacement so encoding or write failures preserve an existing report.
- Credential binding projection retains only referenced identity IDs, preserving ambiguous matches without loading the entire store-backed graph into memory.
- The main CI regression tracker reconciles on cancelled head runs, ignores non-decisive runs, and closes with one comment citing the green run instead of overwriting the failure evidence.
- Distribution freshness reports provider-side verification gaps in the job summary and no longer opens or reopens the drift issue for them; stale, unreachable and unmonitored surfaces still alert.
## [0.109.0] - 2026-10-15
### Changed
- Trace pull, trace collection and trace findings (Langfuse, LangSmith, OTel) are experimental and **off by default**. Enable them with `AGENT_BOM_EXPERIMENTAL=traces`. While disabled, `GET /v1/traces/connectors`, `POST /v1/traces/connectors/{provider}/pull`, the trace collection routes and `POST /v1/traces/attack-paths` return `404` with the enable hint; `POST /v1/traces` still accepts OTLP for runtime and cost telemetry but records no trace findings; graph snapshots do not overlay trace events or emit `SHADOW_AI` trace findings; the MCP `runtime_correlate` tool returns the hint for `otel_trace`; and the dashboard labels Platform traces *Experimental* and shows the hint instead of the pull form. Cortex agent coverage is unchanged.
### Added
- One experimental-feature registry (`agent_bom.core.experimental`) lists every experimental feature with its surfaces, how it is enabled and its doc. `docs/EXPERIMENTAL.md` is generated from it (`scripts/generate_experimental_docs.py --check` runs in CI and `make preflight`), storage tiers read their experimental backends from it, `agent-bom doctor` prints an Experimental features section (and `experimental` in agent-mode JSON), and `GET /v1/system/health` reports `experimental_features` with live enablement.
- `HOSTED_ON` (workload → host, a dependency relationship) records where an agent, MCP server or container runs. A cloud-hosted agent is hosted on its cloud resource (the account and provider `HOSTS` rollup is unchanged), and a Kubernetes-discovered MCP server is hosted on its pod and the pod on the node VM named by the node's provider id (AWS, Azure, GCE); discovery records the pod uid, namespace, node and provider id. Component paths walk these edges, so a CVE on a VM reaches the agent whose server runs on it, and a compute instance with a CVE and nothing hosted gets its own path. Graph exports map the edge to `HOSTED_ON`.
- One graph node per real-world asset across sources. Observations that share a strong provider-native key (qualified ARN, AWS instance id with account and region, Azure ARM id case-insensitively, GCP self link, Kubernetes uid, image digest, principal id, Snowflake account-qualified name) merge onto a deterministic canonical id derived from that key, before attack-path analysis and again after finding rows name new assets. The merged node records `attributes.identity` (canonical key, basis, replaced ids, one observation per source node and data source, conflicting fields) and the union of data sources; conflicting strong keys never merge and are counted. Host names, private addresses, MAC addresses and names within an account never merge: they add a non-traversable `POSSIBLY_CORRELATES_WITH` candidate with matched signals, confidence and sources, suppressed when both sides carry distinct native ids. CloudTrail principals are typed from the recorded caller identity (an assumed-role session is the role). Snapshot correlation joins on the same keys (`scoped-identity.v5`). Replaced ids persist in the tenant-scoped `graph_node_aliases` table (SQLite, and Postgres with forced row-level security via Alembic `20261014_01`); imported-trace inventory bindings resolve through it. The demo estate adds a compute lane of 12 VMs seen by inventory, CloudTrail, a side scan and a posture finding, plus 3 host-name-only sensor findings that stay separate candidates.
- Agents and MCP servers get one graph id from shared helpers in every producer (graph builder, export graph, exposure paths, demo estate). A remote MCP server is one node per tenant, a launched server with arguments one node per host, and a name-only server stays under its agent; shared server nodes list every declaring agent, and a credential slot is reachable only from the agents that configured it. Exposure-path hops, tools and credentials in JSON and SARIF are graph node ids with a `nodeLabels` map for display. Runtime incidents bind to one agent by graph id or source scope plus name; an ambiguous bare name binds to none and is counted in the `runtime_incident_feedback` analysis status.
- `GET /v1/graph/diff` is bounded: the default `view=summary` returns counts per change kind by entity type and relationship plus the most significant changes (new critical findings, new credential reach, removed controls, new attack paths) and compares the latest two snapshots when no IDs are given; `view=detail` pages change rows with filters and a cursor; `view=full` keeps the legacy lists under a row and byte cap with `truncated`/`complete` flags and omits node attribute blobs. On the showcase estate the default answer drops from about 8 MB to under 16 KB. The new read-only MCP tool `graph_diff` answers from the same function (94 tools).
- Imported Langfuse, LangSmith and OTLP JSON traces normalize into one canonical metadata-only event and become `runtime_observed` graph edges aggregated per source, relationship and target, bound only by exact inventory or canonical IDs. Attack-path ranking and credential reach read them; unmatched and name-only spans are counted in the `runtime_trace_projection` analysis status. Screened trace signals, vulnerable tool calls and unlisted agents or tools become unified findings, including the new `SHADOW_AI` finding type. Langfuse pulls now request model and usage fields and LangSmith pulls token and cost columns.
- Graph relationship inspectors and recorded rows surface receipt evidence types and source observation times separately from graph-record timestamps and traversal eligibility. Missing and mixed evidence stay explicit.
- Graph relationship details and recorded-link rows explain agent/server use, available tools, dependencies and findings in plain language. Recorded source, basis and package fields remain separate from relationship meaning; conservative tool-impact mappings and missing source receipts stay explicit.
- Public first-run and investigation feedback forms capture setup friction and decision context. Inaccurate-finding reports now capture directed graph identity, scope, provenance, freshness and incomplete evidence, with contribution and support routing through the active repository.
- Published-package first-run verification checks the offline report, persisted sample snapshot and rendered graph on Linux/macOS, with desktop/mobile light/dark captures, separate timings and retained failure receipts.
- A shorter release-focused README leads from a repository scan to the persisted sample graph, explains the server handoff, and links the full source-version product tour.
- Add opt-in snapshot finding reads with per-read differential verification and fallback to current findings on mismatch or unavailable snapshots. Qualification preserves live enrichment and retention semantics; optimized metadata-only reads remain deferred.
- Opt-in scan finding snapshots persist completed job evidence in tenant-scoped memory, SQLite or PostgreSQL stores. Backfill and purge commands manage retained jobs; deletion and expiry remove snapshots even after writes are disabled. Reads require the independent, default-off qualification flag.
- Direct compromise assessment is available through the authenticated graph API, `graph-paths compromise`, the graph-profile MCP tool, and the graph entity drawer. Requests require explicit assumptions and return a pinned snapshot revision with action-scoped historical permission receipts, denials, and unknowns. Partial storage reads fail closed; this does not establish current access, successful execution, or multi-hop compromise.
- Storage backends now carry a declared support tier: PostgreSQL (server system of record) and SQLite (local, CLI and pilot) are supported, ClickHouse is an analytics sink only, and the Snowflake control-plane stores and the Neptune graph backend are experimental. When an experimental backend is selected, the API logs one warning at startup that names the backend, its tier and `docs/STORAGE_BACKENDS.md`. In a deployment that looks multi-tenant, the warning states that tenant isolation for that backend is not proven. Startup continues by default. `AGENT_BOM_REQUIRE_SUPPORTED_STORAGE=1` refuses to start with an experimental backend. `agent-bom doctor` lists the tier of each selected store under Platform integrations; experimental rows count as warnings.
- `--external-scan`, `findings push` and the MCP `ingest_external_scan` tool now import Prowler v4/v5 JSON-OCSF and AWS Security Hub ASFF reports. FAIL and MANUAL Prowler results and active Security Hub findings become cloud posture findings with account, region, resource ARN, vendor compliance references and tool provenance; Security Hub vulnerability records become CVE findings. PASS, muted, archived, suppressed and resolved records are skipped and counted in a scan notice. Imports are file-based: no credentials, network access or tool execution.
- `docs/VENDOR_COVERAGE.md` and `docs/VENDOR_COVERAGE.json` give a generated provider × capability matrix (connect, API connection, scheduling, discovery, inventory, benchmark posture, identity, data, side-scan, change events, runtime evidence), integrations by category, and a list of provider lists that disagree. Each cell cites its source constant. `scripts/generate_vendor_coverage.py --check` runs in `make preflight`.
- Scanner importer contract (`agent_bom.parsers.importers`) with a published transparency manifest per importer and an opt-in `agent_bom.importers` entry-point group for third-party importers. See `docs/IMPORTERS.md`.
- A Python direct compromise assessment contract classifies scoped authorization receipts from a selected graph node or an explicitly assumed affected component. It preserves action-specific denials, uncertainty, original edge direction and collection timestamps. This read-only foundation does not establish current access, successful exploitation or multi-hop reachability.
### Changed
- An operator-supplied VEX document (`--vex`, or the API scan request `vex` field) now suppresses findings it marks `not_affected` or `fixed` for a matching vulnerability and product: they stay visible with the VEX justification but leave totals, risk scores, SARIF active results and exit-code gates. Findings record `vex_source` (`operator` or `discovered`); VEX discovered inside scanned artifacts remains evidence only, and `under_investigation`/`affected` never suppress. VEX statements that name products now apply only to those packages.
- Browser-extension findings use the `BROWSER_EXT` finding type and source in JSON output instead of the non-enum `BROWSER_EXTENSION_RISK` / `BROWSER_EXTENSION_SCAN` values; stored rows with the old values still load as `BROWSER_EXT`.
- Cloud discovery for AWS (AI services, organizations, inventory), Azure, GCP (inventory and authorization), Snowflake, Databricks, CoreWeave, Nebius, Lambda Labs, RunPod, local GPU infrastructure and the NVIDIA advisory check is split into named stages. Each stage keeps its own error isolation and warning text, so discovery output, warnings and API calls are unchanged. Kubernetes GPU parsing shared by CoreWeave and Nebius moves to `cloud/k8s_gpu_common.py`; GCP authorization record normalizers move to `cloud/gcp_authorization_records.py`; Snowflake notebook discovery moves to `cloud/snowflake_notebooks.py`. 35 architecture debt entries are retired.
- Managed-node EKS bootstrap no longer requests an unused Auto Mode policy or a standalone cluster-encryption policy. Encryption permissions remain scoped to the cluster's KMS key through an inline role policy; the cluster-first bootstrap command includes that policy.
- The top-level module guard (`scripts/check_package_layout.py`) now names a likely subpackage for each new `src/agent_bom/*.py` file and points to the new `docs/CODE_MAP.md`, which maps common changes to their owning packages, canonical models and layering rules. The guard has its own regression test.
- `CONTRIBUTING.md` adds a "first PR in 30 minutes" path, good first contribution areas and maintainer response expectations. A maintainer-facing `Good First Issue` template is added.
- The deployment overview classifies each `deploy/` target as Supported, Preview, Reference or Community. No deployment files changed.
- `--external-scan` reports are now read with the shared parser size limit (`AGENT_BOM_MAX_MANIFEST_BYTES`) in the CLI and API scan paths.
- The scan API route module is split into focused modules with no change to routes or responses: the local-path scan jail (`api/scan_path_jail.py`), AI-scan execution (`api/ai_scan_runtime.py`), bulk-ingest request models (`api/bulk_findings_ingest.py`), finding row identity and legacy row builders (`api/finding_row_shapes.py`), job response payloads (`api/scan_job_views.py`), correlation cohort ids (`api/scan_cohorts.py`) and findings list helpers (`api/finding_list_helpers.py`). The dedicated AI-asset scan endpoints (`/v1/scan/dataset-cards`, `training-pipelines`, `browser-extensions`, `model-provenance`, `prompt-scan`, `model-files`) move to `api/routes/ai_scans.py`. Existing imports from `agent_bom.api.routes.scan` keep working.
- The `/v1/findings` read path is decomposed into named stages with no change to responses: query validation, the scan-findings half, page strategy (scoped keyset, merged scan + hub keyset, hub-only, in-memory), facets and envelope assembly. The view-filter and issue-grouping handlers, the bounded facet walk and the scan + hub merge walk get the same treatment. A read that pages through many rows (view filters, issue grouping) now folds, filters and sorts the scan-findings half once per read instead of once per internal page.
- The largest scanner engines are decomposed into named stages with no change to results: package vulnerability scanning (`scan_packages`), the cloned repository tree scan, the MCP `scan` tool body and the live Kubernetes posture scan. Each now reads as a short orchestrator over input preparation, version resolution, advisory lookups, enrichment and result assembly.
### Fixed
- Alias merging no longer lets a namespace-default `medium` (an advisory with no score or rating, such as a PYSEC record) outrank a rated alias: CVE-2024-12797 in `cryptography` now takes the GHSA `low` rating. Console and HTML severity badges show "(unscored)" when the band is only that default.
- Whole-queue path searches (the `should_i_deploy` gate and filtered `exposure_paths`, including `/v1/graph/exposure-paths`) read every stored path instead of the first rank window while reporting a complete search; beyond the search cap the answer is a labelled lower bound and the gate never allows. `/v1/graph/attack-paths` marks totals beyond the rank window as lower bounds. The deploy gate rechecks the pinned snapshot generation and returns a retryable error when it changed mid-read.
- `AGENT_BOM_GRAPH_CONTRACT=enforce` no longer reports a clean contract after dropping edges: the snapshot's `relationship_contract` analysis records `rejected` counts per relationship and entity pair with a bounded sample of edge ids.
- Graph analysis limits are reported only when they hide something. Credential reach no longer marks a scan `limited` (and therefore `partial`) when a depth-bounded route only leads to a credential already reached by a shorter route. A path budget keeps any earlier agent-budget reason instead of replacing it. The current-estate view now carries the worst analysis status of its source scans, so a limited source is no longer shown as clean.
- Compliance control tags stay consistent across findings, the graph and exports. Toxic-combination findings no longer put OWASP Web Top 10 IDs (`A06:2021`, `A01:2021`) or `LLM06:2025` into the OWASP LLM field; they now carry catalog-backed NIST SP 800-53, SOC 2 and (for agents reaching privileged tools) OWASP LLM `LLM06` / Agentic `ASI03` controls, which survive report rehydration. Graph vulnerability nodes keep NIST 800-53, FedRAMP, CMMC and PCI DSS tags. CWE lookups are case-insensitive in every tagger. Framework-qualified export tags no longer treat a colon inside a control ID as a framework prefix. A dead SAST CWE-to-CMMC lookup that could never resolve was removed.
- Compliance controls are no longer passed or failed on evidence that never ran. "Scan for vulnerabilities" and "component inventory" controls (NIST RA-5 / CM-8, FedRAMP RA-5 / CM-8, CMMC `RA.L2-3.11.2`, CIS `CIS-02.1` / `CIS-07.1` / `CIS-07.5`, SOC 2 `CC7.1`, NIST CSF `ID.RA-01` / `ID.RA-02` / `DE.CM-09`) pass only on a scan whose inventory or vulnerability lane actually ran, scored from that lane's latest scan. IaC-only, browser-only, secrets-only, cloud-benchmark-only and `--no-scan` runs now read `not_assessed` (reason `detective_evidence_not_collected`) instead of `pass`, and a partial vulnerability lane reads `not_assessed` (reason `vulnerability_coverage_incomplete`). `--no-scan` records a `vulnerability_scan_skipped` receipt in `scan_run`. NIST 800-53 catalog controls are evaluated only by a finding's own evidencing checks, so SR-3 and SI-4 no longer fail on every CVE. CMMC no longer tags `RA.L2-3.11.2` or `CM.L2-3.4.3` onto CVEs.
- ISO/IEC 27001 findings reached through NIST's SP 800-53 crosswalk (for example `A.5.17` and `A.5.16` on committed credentials) were tagged but missing from the ISO catalog, so `/v1/compliance` and the evidence export dropped them. The bundled ISO catalog now lists all 25 Annex A controls a tagger can emit, by identifier with agent-bom's own short descriptors. A test enumerates every tagger and fails on any emitted ID its framework's catalog does not contain.
- `/v1/compliance` and the evidence export scored MITRE ATT&CK against the bundled catalog, while the taggers and CLI narrative used the active (synced) catalog. All surfaces now use one resolver, so a technique that exists only in a synced catalog appears everywhere. The documented technique count still comes from the bundled catalog.
- A finding's `applicable_frameworks` now always includes every framework it carries a control tag for, in addition to the frameworks the source/asset/finding-type selection table assigns. Hub posture counts by `applicable_frameworks` and `/v1/compliance` counts by tags, so the hub no longer omits a finding that `/v1/compliance` counts. This now includes graph toxic-combination findings, which were tagged after classification, and survives report rehydration.
- `/v1/findings?framework=X` now matches a scan finding only when it carries an X control code that exists in X's scoring catalog, the same rule `/v1/compliance/X` counts by. An IaC rule declaring an 800-53 control outside the bundled catalog (for example `CP-9`) no longer lists its finding under NIST 800-53 while the NIST line counts nothing; the exact `control=` drill is unchanged. Falling back to `applicable_frameworks` listed every SBOM CVE under PCI DSS while the PCI DSS line counted none. Bulk-ingested findings, which carry only the producer's applicability, still match on it.
- IaC findings now carry the NIST SP 800-53 controls their rule declares (for example `AC-3`, `AU-2`) in `nist_800_53_tags`, and the FedRAMP Moderate subset in `fedramp_tags`, so the NIST 800-53 and FedRAMP lines count them. Only controls in the bundled catalog are tagged, and detective controls (RA-5, CM-8) never are. CIS AWS Foundations Benchmark IDs are not CIS Controls v8 safeguards and stay out of `cis_tags`.
- The CIS Foundations Benchmark scorecard line no longer counts Databricks security checks, which are vendor best practice (Databricks publishes no CIS benchmark). They remain listed by `/v1/cis/checks`.
### Security
- API local-path scans: a set-but-blank `AGENT_BOM_API_LOCAL_PATH_SCANS` no longer enables local filesystem scans. A blank value is treated as unset and falls through to `AGENT_BOM_ENABLE_LOCAL_PATH_SCANS`, then to the disabled default.
## [0.108.4] - 2026-10-09
### Fixed
- `doctor` warns when its selected OSV scan cache is unreadable, locked or incompatible instead of reporting it as healthy. Diagnostics reuse the scanner path resolver, open the cache read-only with a bounded lock wait, and give a repair/recheck hint without exposing paths or exceptions.
- Align documentation-site links, package metadata and Action examples with the transferred repository; accept release signatures from its current and historical owners while rejecting unrelated workflows.
- Managed PostgreSQL initialization validates existing maintenance-role attributes without reapplying superuser-only flags. Unsafe existing roles fail initialization; pre-provisioned safe roles remain usable on RDS.
- ClickHouse analytics now creates its database before selecting it for HTTP queries. Connecting to a fresh server no longer fails with `UNKNOWN_DATABASE`; bootstrap retains credentials and stops on authorization errors.
- Corrected the ClickHouse benchmark example to use an HTTP endpoint and the registered `report analytics` command. Deployment guidance now explains optional ClickHouse analytics alongside PostgreSQL/SQLite and the bounded analytics queue's durability limits.
- Helm API shared-state safeguards now account for autoscaler maxima and KEDA fallback replicas, including deployments starting at one replica. API/UI disruption budgets cover configured scale-out, and extra API environment entries cannot override the chart-derived replica safety setting. Deployment guidance distinguishes single-replica SQLite, shared-state PostgreSQL API scaling, and single-writer gateways. PostgreSQL table partitioning is not automatic database sharding; live load, failover, and HA need qualification in the target deployment.
- Label CIS benchmark checks separately from cloud inventory in scan pipelines; preserve unknown check outcomes and explain target-dependent scan scope.
- `serve --demo-estate` and `api --demo-estate` now keep demo scan jobs in the same SQLite file as the demo graph (`<demo dir>/control-plane.db`) unless `--persist` or another backend is configured. A restart no longer shows the 6,803-node graph next to zero jobs and re-runs the curated scan; jobs are available on the first response. Existing demo installs reseed once after upgrading.
- `/v1/overview`, and `/v1/posture` when it has no data yet, report `demo_estate_seeding` while the startup seed runs, and the dashboard shows a seeding status instead of the first-scan guide.
- The spend forecast no longer turns minutes of history into a daily burn rate. On the demo estate, $6,851 recorded over about 50 minutes had shown $356,507.91/day and an $8,236,481.61 projected period. A burn rate now needs at least 6 hours of recorded calls, and `history_span_hours` reports how much history there is. AI Spend explains the gap, shows large amounts compactly ($8.24M) and no longer overflows its forecast cards.
- The Scan Jobs pipeline no longer shows more critical findings than total findings ("30 findings · 103 critical"). Both counts now come from the unified finding summary. Older summaries show a critical count only when it is no larger than the total. Jobs without stage events hide the stage-timing rows and the zero wall clock instead of listing "Unavailable" for every stage.
- On the demo estate, the sidebar no longer links Cloud Governance and Control Plane Security, because the backend does not serve them there. The UI now points to `agent-bom serve` (or `serve --no-ui` for the API only) instead of the retired `agent-bom api` command.
- The Agent Activity header now counts the stream events listed below it, with the runtime events for the window counted separately. Before, it said "0 events across the last 30 days" above a populated stream. The stream header and event details still say when the producer is unknown, but rows no longer repeat it.
- Inventory now shows an error with a Retry button when a request fails (including 429 rate limits), instead of staying on its loading skeleton.
- The AI BOM counts packages, cloud assets and findings from the same inventory summary that Inventory uses, instead of showing "Not collected".
- KPI strip labels and hints now wrap instead of being cut off, and finding rows show readable detection labels ("Cloud security", "CIS fail") instead of raw enums.
- Credential patterns no longer fire inside ordinary identifiers. The OpenAI key pattern matched the `sk-` in `task-`, `risk-` and `disk-`, so CSS class names and schema ids such as `risk-campaign-evidence-label` became CRITICAL "OpenAI API Key" findings. Vendor-prefixed patterns (OpenAI, Anthropic, AWS, GitHub, GitLab, Slack, Stripe, Google, Twilio, SendGrid, Mailgun, Square, npm, PyPI, Discord, Telegram, JWT, Shopify, Databricks) now require a token boundary before the prefix. An OpenAI key body must also contain a digit and must not be a lowercase hyphenated slug, and `sk-ant-` keys report as Anthropic only. A `postgresql://${USER:-default}@host` shell default is no longer read as a password. On this repository, `agent-bom secrets .` drops from 39 to 12 credential findings (OpenAI 26 to 1, connection strings 8 to 6). Real-format `sk-...` and `sk-proj-...` keys are still detected, and pattern cost on a 200 KB response is unchanged.
- Distro fix advisories (Debian `DLA-`/`DSA-`, Ubuntu `USN-`, AlmaLinux `ALSA-`, Red Hat `RHSA-` and similar) no longer report `unknown` severity when their upstream CVEs are scored. Such an advisory takes the highest CVSS score, or else the worst label, of the CVEs it fixes. The scores come from records already in the scan and from the local vulnerability DB, with no extra network request. `severity_source` names the CVE (`upstream_cve:CVE-…`), and EPSS and KEV attach through the same CVEs. The advisory id is no longer projected into `cve_ids`. An advisory with no scored upstream CVE stays `unknown` and still fails the severity gate closed; the exit-code reference now documents this.
- The "SQL query is built through string interpolation" rule honors `# nosec`, `# nosec B608` and `# noqa: S608` anywhere on the call or on the assignment that built the query. It no longer flags queries whose interpolated values are only bind placeholders (`?`, `%s`, a separator `join` of them) or module-level UPPER_CASE constants the function does not rebind. A `# nosec` naming another rule, or any interpolated parameter, is still reported. On this repository, `agent-bom scan -p .` drops from 202 to 29 such findings.
- CPython runtime findings now carry `fixed_version` when OSV publishes release-numbered fix windows (for example `3.9.24`). Only the window that contains the installed release supplies a fix, so no cross-branch upgrade is suggested.
- The console summary, Agents table, dependency tree, HTML agent cards and graph summary now count only real MCP servers. A plain repository scan no longer shows "0 servers" in the summary and "Servers 1" in the Agents table.
- Project scan auto-detect no longer stops at 4,000 files per probe. A capped probe both missed notebooks, Terraform and IaC deeper in a normal-sized repository and recorded a `discovery_file_limit` coverage gap that marked the whole scan `partial`.
- Compliance framework mappings were corrected, so **control tags differ from those in earlier reports**. Re-scan before comparing history, because stored findings keep the tags they were written with. Changes by framework:
- **OWASP LLM Top 10**: every ID now uses the 2025 numbering. Before, 2023 and 2025 IDs were mixed. Vulnerable packages are now LLM03 Supply Chain (was LLM05). Exposed credentials and readable data are LLM02 Sensitive Information Disclosure (was LLM06 or LLM07). Exec tools and injection CWEs are LLM05 Improper Output Handling (was LLM02). Excessive agency is LLM06 (was LLM08). Training-data packages are LLM04 Data and Model Poisoning (was LLM03 and LLM04). LLM08 Vector and Embedding Weaknesses now fires for vector-store packages. A CVE in an AI framework alone is no longer tagged poisoning. CWE-287, CWE-444, CWE-639 and CWE-942 no longer map to an OWASP LLM risk.
- **EU AI Act**: Articles 5 (prohibited practices) and 6 (high-risk classification) are no longer tagged and are removed from the catalog. Neither can be inferred from dependencies or tools. Article 10 is evidenced only by dataset findings. New tags are Article 14 (human oversight) for exec-capable tools and Article 12 (record-keeping) for logging weaknesses. Tags express technical relevance, not a legal classification.
- **SOC 2**: CC7.1 is now labeled as vulnerability and configuration-change detection. It is evidenced by a fresh scan instead of failing on every CVE. CC7.2 (anomaly monitoring) is no longer tagged from dependency CVEs. CC6.8 (malicious software) now requires a known-malicious package instead of any HIGH+ CVE.
- **NIST AI RMF**: subcategory IDs now match their AI RMF 1.0 meaning. Third-party and supply-chain risk is GOVERN 6.1 / MAP 4.1 (was GOVERN 1.7 / MAP 3.5). Human oversight is MAP 3.5. Impact is MAP 5.1. Security evaluation is MEASURE 2.7 (was MEASURE 2.5). Risk response is MANAGE 1.3 (was MEASURE 2.9). The MAP 1.6, MANAGE 2.2 and MANAGE 2.4 rules were dropped because no subcategory with that meaning fits them.
- **CIS Controls v8**: library allowlisting is CIS 2.6 (was 2.7) and hardening templates are CIS 16.7 (was 16.11).
- **NIST CSF 2.0**: containment is RS.MI-01 (was RS.MI-02), and the DE.CM-09 title now matches CSF 2.0.
- **OWASP Agentic Top 10**: ASI01, ASI08 and ASI10 now carry their 2026 titles. ASI01 (Agent Goal Hijack) and ASI09 (Human-Agent Trust Exploitation) are no longer applied to every dependency CVE.
### Performance
- The retained-findings fold computes each job's scope key once instead of three times per finding. A cold `/v1/posture` on the demo estate drops from 17.5M to 7.6M function calls (38.2 s to 25.7 s under cProfile on a loaded host).
- `agent-bom scan -p .` on this repository uses 33-39% less CPU (140-141 s to 86-95 s user CPU over two runs each, peak RSS 335-395 MB to 307-325 MB) with identical findings (941) and packages. The AI component scanner skips any regex whose required literal text is absent from a file (66 to 20 CPU-seconds for that pass, same 601 components); AST source selection walks the project once instead of once per language; project auto-detect walks it once instead of once per surface; and Python entrypoint discovery walks each module once instead of three times.
- `agent-bom --version` answers without loading the command tree: 1.2-1.7 s down to under 0.15 s. Every other command also imports less at startup (output formatters, the MITRE catalog fetcher, the warehouse graph builder and fleet API models now load on first use).
- Dashboard page loads no longer freeze the API. Page reads already ran on worker threads, but about ten CPU-bound aggregate reads at once (overview, findings, compliance, posture counts, graph) left the event-loop thread waiting on the GIL. `/health` and `/_next/static` chunks queued behind them for tens of seconds. Those reads now compute two at a time (`AGENT_BOM_HEAVY_READ_CONCURRENCY`, default 2). Waiters beyond the worker-thread ceiling get 429 instead of an unbounded queue. Concurrent reads of the same job payload share one parse instead of each parsing it. Hashed `/_next/static` assets are served from memory without worker-thread hops and marked `immutable`, and the schedule poll moved off the event loop. On `serve --demo-estate` with SQLite, after seeding settles, firing ten page reads at once with static and `/health` probes running alongside: max event-loop stall drops from 31.7 s to 2.6 s, max static-asset latency from 38.0 s to 4.6 s and max `/health` latency from 37.9 s to 2.9 s. Cold page reads drop from 35-96 s to 7-17 s, and warm ones from 19-35 s to 4-12 s. Measured on a 10-core dev host at load average ~35, so absolute numbers are high.
### Changed
- Project scans no longer stop the secret scan at 1,000 files or AST analysis at 500 source files. The budgets are now 10,000 and 5,000, so `agent-bom scan -p .` on this repository covers all 2,634 secret-scan files and all 1,804 AST source files instead of reporting both as coverage gaps. Findings rise from 739 to 862 (secret/PII 26 to 119, SAST 31 to 61). Full coverage costs more: user CPU goes from 97-105 s to 205-266 s over two runs each, peak RSS from 433-552 MB to 1.17-1.46 GB, and the JSON report from 39 MB to 95 MB. Most of the extra CPU comes from AST analysis running twice per scan (agent prompt discovery and source analysis each call it). The scan outcome on this repository is still `partial` for reasons these budgets do not cover: 12 TypeScript files the bundled tree-sitter grammar cannot fully parse, 7 CloudFormation templates with unresolved values, and 7 packages with unresolved versions.
- The gateway `/mcp/{server}` relay now runs as an ordered pipeline of named, typed stages (request admission, identity, runtime profile, edge/spend/fleet admission, layered tool policy, forward) that pass explicit request state instead of sharing one 1,464-line closure. Status codes, response bodies, headers, audit records and metrics are unchanged.
### Security
- The REST API now validates the `Host` header before routing or authentication.
A loopback `agent-bom serve` / `agent-bom api` answers only loopback host names,
so a web page can no longer reach a local control plane through DNS rebinding and
obtain the zero-config dashboard session. Non-loopback deployments can restrict
accepted host names with the new `AGENT_BOM_API_ALLOWED_HOSTS`. Without it they
keep accepting any Host and log a startup warning. Malformed configured host
entries fail startup instead of silently disabling the allowlist.
- Cookie-authenticated state-changing requests now also require a trusted `Origin`
(or `Sec-Fetch-Site: same-origin`). API-key and bearer clients are unaffected.
- Webhook signing secrets are encrypted at rest with the connection at-rest key
when one is configured. Existing plaintext rows stay readable and are encrypted
on their next write.
- The Cloud Run demo deploy workflow passes the triggering run's branch name to
the shell through an environment variable and validates it as an image tag.
## [0.108.3] - 2026-10-08
### Performance
- Postgres job reads no longer transfer and decode every job payload on each call. A read sends the row versions this process already holds, and the database returns a payload only for rows that changed, so another replica's write is always seen. Payloads are cached as JSON text per tenant, with a byte limit (`AGENT_BOM_POSTGRES_JOB_PAYLOAD_CACHE_MB`, default 256). Reads outside one request's aggregate read still parse a fresh job.
- A cold `/v1/posture` now parses each job payload once instead of three to four times. Latest-scan selection, the overview fold, the current-graph resolver and the finding spine share one parse of each exact payload within the request, on Postgres and SQLite. A changed row is new payload text and is parsed again, so another replica's write is still seen. Latest-scan selection also moved off the event loop. On the demo estate (one 43 MB job), the first `/v1/posture` after a restart drops from 2.6 s to 1.7 s on Postgres and from 2.4 s to 1.7 s on SQLite (median of 3 restarts); the first `/v1/overview` and `/v1/findings` also drop by 0.3-0.7 s.
- Campaign reconciliation now reaches its checkpoint, so it stops re-walking and re-redacting every finding at each boot and every maintenance tick while evidence is unchanged. A campaign walk reuses one read context across its pages, and the background walk is no longer cut off by the 10 s request bound.
### Fixed
- A nested package-vulnerability row now folds onto the finding on its own MCP server when other assets share the same package version, instead of surfacing as a duplicate row with no finding identity. The demo estate reports 2,711 findings, not 2,719. A nested row that still matches no finding gets a deterministic `canonical_id`. Campaigns still treat a row without an identity as an incomplete collection.
- Campaign collections no longer stay incomplete on Postgres. Resumed cursor pages intentionally omit the total, so only the first page's exact total binds the walk, and any later page reporting a different total still fails the walk.
## [0.108.2] - 2026-10-08
### Changed
- Move the PyPI and Docker Hub front doors to `docs/registry/`, the repository map to `docs/PROJECT_STRUCTURE.md`, the image exception register to `docs/security/image-exceptions.yaml`, the project-config template to `examples/agent-bom.yaml.example`, and the fuzz harnesses to `.clusterfuzzlite/fuzz/`.
### Removed
- Remove the legacy Streamlit dashboard, the `agent-bom report dashboard` command and the `dashboard` install extra (which also left `agent-bom[all]`). Use `agent-bom serve` for the bundled dashboard.
### Performance
- The first `/v1/posture` and `/v1/graph` after a restart no longer compete with background work. Campaign reconciliation and the demo story prewarm wait until no request is in flight and a quiet window has passed, and they run after at most 60 s even under steady traffic. Current-estate graph reads now run concurrently. Rebuilding or retiring a generation still waits for active readers, and the 409 retired-generation contract is unchanged.
### Fixed
- Match RHEL/UBI advisories and fixes to applicable product streams. Keep unknown fix status and incomplete assessments explicit after suppression.
- Retain available current-estate evidence when a scan has no graph, preserve unrelated inventory and authoritative clean replacements, and represent valid empty estates without masking storage failures.
- Preserve validated package identities and PURLs through redaction. Use ecosystem-appropriate Go and NuGet lookup spelling and invalidate older OSV cache entries that may contain incomplete matches.
- Persist configured SQLite exception approvals across restart and reject remote database URLs before SQLite can create local files. Postgres support for remaining governance stores is separate work.
- Show “No assessment evidence” when an otherwise finding-free report has neither packages nor evaluated controls.
- Bundle the cloud benchmark inventory, verify compiled dashboard installation pins against the wheel version, and retain the requested image platform through Docker save/export.
- Resolve Maven properties from local parent POMs, retain Swift repository aliases for advisory lookup, and preserve supplied SBOM scope, package identity and recorded dependency evidence.
- Keep repository IaC and filesystem coverage gaps explicit, retain Django URL reachability evidence, and inventory the CPython runtime in container images with applicable advisory checks.
- Bound campaign reads, persist Postgres campaign evidence and keep missing evidence distinct from verification results.
- Preserve graph investigation scope and navigation, keep loaded evidence readable in light and dark themes, and retain the selected mobile path position when updating shareable URLs.
- Update service-image runtimes and honor CPython tarfile filter rejection while retaining runtime remediation evidence.
- Update the pinned Python Alpine image and dashboard icon, accessibility and table dependencies.
- Record OSV response-cache use and the default unfixed OS-advisory suppression count in scan JSON.
- Require executed, successful dashboard validation, export and browser jobs on the exact main release commit; run those UI lanes on every main push.
- Verify campaigns on default single-node deployments where jobs and hub findings are in memory and campaign workflows use SQLite.
- Keep SPDX 2 and SPDX 3 packages marked APPLICATION in scan inventory instead of omitting them.
- Keep letter-suffixed Debian, Alpine and RPM fix versions such as tzdata `2025b-0+deb11u2`; they were misread as prereleases and their advisories hidden as unfixed.
- Return compact `/v1/findings` list rows with `controls_count`; request full control mappings with `?include=controls` (CLI `--include-controls`, SDK `include_controls`). Reuse scan rows within posture reads to cut cold posture and remediation latency.
- Treat Glama listing lag within 72 hours of a release as pending provider sync, and report the underlying Smithery catalog error with a longer convergence window.
- Build the graph scale-evidence server image from the current source tree.
- Reuse the persisted current-estate graph projection across server restarts when evidence is unchanged, instead of rebuilding it in every process.
### Security
- Allow valid bearer access-token reuse while retaining replay protection for one-time OIDC exchanges. Honor explicit role claims, including viewer, and reject non-string tenant claims.
- Remove request queries from both API launchers' access logs, redact short bearer tokens and control-plane keys, and suppress interpreter warnings that echo scanned source or filenames.
- Reject malformed compliance ingestion bodies before writes and require an explicit JSON boolean for absent-finding reconciliation.
- Sign the exact audit response bytes and reject malformed, past or timezone-less explicit exception expiries without changing separate approval requirements.
- Pin webhook socket connections to validated DNS answers, block cloud metadata destinations, and require both tenant and operator opt-in for private subscription destinations. Disable automatic redirects and ambient proxies for inline delivery.
- Keep `GET /v1/agents` read-only and scope demo agents to the showcase tenant; live host discovery stays behind the operator binding.
- Return 503 from `POST /v1/auth/saml/relay-state` when SAML is not configured instead of recording a nonce.
- Bound suppression expiry (`AGENT_BOM_EXCEPTION_MAX_EXPIRY_DAYS`, default 365) and reject approval by the requester or by the author of the suppressing triage or VEX decision unless `AGENT_BOM_EXCEPTION_ALLOW_SELF_APPROVAL` is set. Postgres deployments need migration `20261007_03`.
## [0.108.1] - 2026-10-06
### Fixed
- Reconcile default inventory and graph reads with the current tenant estate across REST, MCP and the dashboard. Retain clean assets, unrelated targets and partial evidence; preserve explicit historical views and reject stale pagination generations.
- Load configured persisted scan evidence in standalone MCP before API startup, including base installations without API extras. Propagate configured storage failures instead of silently returning an empty estate.
- Redact bearer credentials and short JWTs through pushed and persisted evidence. Retain safe IaC file/line labels, classify repository IaC as application evidence and apply only approved, active suppressions to finding filters.
- Preserve imported server identities and validate actual HTTP report and graph exports against their published schemas.
- Replace the permanent inventory detail panel with a keyboard-accessible drawer, collapse secondary metadata and improve mobile and light-theme readability. Explain graph connectivity separately from severity and derive freshness banners from displayed evidence.
- Reduce repeated finding decoding and reconciliation, bound remediation projections and defer collapsed campaign requests. Refresh desktop and mobile product captures at documented viewport sizes.
- Keep console verdicts coverage-aware for incomplete imported inventories and lookup warnings. Correct the bounded GitHub Action Dogfood fixture and ensure fixture changes run its workflow.
- Preserve SARIF source locations, CycloneDX nested components and distinct imported server identities. Keep suppression state and recorded reachability evidence explicit in findings.
- Restore inventory empty-state handling, persistent dashboard theme selection and remediation across current scan targets.
- Refresh output characterization contracts and bundle framework catalogs in frozen fuzz targets.
- Preserve OSV upstream CVE relationships through local database sync, scanning, EPSS/KEV enrichment, findings and SBOM round trips without merging those links as aliases.
- Align Ruby artifact identity regressions and changed-domain CI selection with recognized native-platform normalization.
- Bound gateway and proxy policy regex execution; reject unsafe pattern structures and fail closed on oversized inputs or exhausted match budgets, with explicit audit-mode receipts.
- Match recognized native Ruby artifact versions using the underlying gem version, retaining platform evidence. Resolve uv workspace members from their shared lockfile; missing or malformed locks remain incomplete instead of falling back to registry-selected versions.
- Require separate authenticated admin approval, exact finding/package scope and a future timezone-aware expiry for suppression. Feedback, triage, MCP and imported VEX requests remain pending; legacy automatic approvals require reapproval. Retain VEX assertions as evidence without silently changing risk or exit gates.
- Report configured filesystem exclusions separately from collection failures. Git metadata, ignored paths and duplicate worktrees remain visible scope receipts without forcing partial scans or secret-scan error exits; unreadable paths, symlinks and exhausted budgets remain incomplete.
- Restrict new-tenant invitations to platform-operator admins whose scope ceiling permits the delegated key. Require attributable authorization receipts in both tenants before provisioning; reject customer-admin recursion and scoped-key escalation without creating tenants or keys.
- Reject malformed and oversized gateway policy regexes before writes. Existing invalid blocking rules fail closed, audit rules expose the invalid policy explicitly, and standalone gateway and proxy evaluation share the same validation.
- Cache OSV results only after every package query succeeds. Failed, malformed, truncated, and incomplete alias/ecosystem lookups remain retryable; a new cache-key generation excludes older potentially incomplete results. Incomplete scans no longer print a clean or complete-report claim.
- Keep pushed findings scoped to the producer and explicit scan target. Complete rescans replace only their own target, partial scans preserve prior evidence, and older unscoped pushes remain independent observations with a coverage-neutral warning.
- Run redaction traversal and generated-metrics contracts on relevant pull requests. Verify Postgres collated-index readiness separately from the planner's choice between eligible indexes.
- Preserve the stable, redacted CLI input-error message for malformed SBOM JSON, duplicate keys and non-finite values.
- Restore the cloud-inventory extension when importing CycloneDX or SPDX JSON through CLI, API and MCP scans, including cloud-only documents. Retain imported provenance and collection gaps, reject malformed or ambiguous extensions, and preserve imports alongside fresh cloud observations.
- Retain collected cloud inventory, native scope, environment tags and collection warnings alongside CycloneDX and SPDX software BOMs in a redacted, versioned evidence extension. Include these artifacts in the offline connected-BOM example.
- Preserve recognized Azure ARM IDs, AWS ARNs and GCP resource coordinates across structured report and graph redaction, including resource-reference lists. Continue redacting embedded or encoded credentials and mask local Windows paths in identifier fields.
- Preserve recorded package-to-package dependency relationships in CycloneDX and SPDX exports. Resolve parents within the same server and ecosystem, keep ambiguous or missing lineage explicit, and retain distinct package URL identities.
### Security
- Upgrade locked Mako, multidict, Werkzeug and source-map-js dependencies to versions that passed the repository advisory gate; raise the Werkzeug dependency floor.
## [0.108.0] - 2026-10-03
### Added
- Inspect component hierarchy, recorded relationships, collector names and timestamps through bounded REST/MCP inventory pages and the dashboard. Snapshot-generation checks reject stale continuations; page completeness remains distinct from source collection coverage.
- Navigate from inventory to exact component findings and control evidence without losing the retained snapshot. Export loaded evidence with source records and completeness receipts.
- Distinguish control mappings from explicit failed benchmark checks, including resource versus account scope. Missing results and supporting detail remain visible; account results are not inherited by child resources.
- Reproduce dependency parsing, component investigation and a changed-input rescan with `scripts/prove_connected_bom.py`. The credential-free example retains SQLite snapshots and compares authenticated REST and MCP evidence in tests; modeled cloud topology is explicitly synthetic.
### Fixed
- Accept slash-containing native cloud identifiers and scoped package identifiers in inventory detail URLs.
- Map known Apache Maven namespaces to the NVD Apache vendor and compare OpenSSL alphabetic release suffixes in affected-version ranges. Correct the bounded Apache HTTP Server regression fixture.
- Keep the experimental Neptune unsupported-operation test aligned with the bounded inventory reader and run it in targeted graph/inventory checks.
### Security
- Remove the vulnerable braces/micromatch dependency chain from Next ESLint root discovery using a scoped maintained glob replacement. Preserve advisory enforcement and test directory discovery and the actual lint rule.
## [0.107.2] - 2026-10-01
Version 0.107.1 was tagged but not published; its prepared changes are included here.
### Fixed
- Query release CI by the candidate commit so unrelated workflow history cannot hide its successful main run; retain exact-main and completed-success checks.
- Queue same-process SQLite graph mutations in arrival order before schema setup, preserving bounded contention errors, failed-producer rollback and concurrent initialized readers.
- Keep SQLite graph startup metadata repairs indexed after migration, and reinitialize read schema state when a database is replaced at the same path. Late legacy relationship metadata remains repairable.
### Security
- Validate both published container architectures by immutable image identity, retain vulnerability reports before enforcing release gates, and archive original daily-rescan SARIF evidence.
- Require Tornado 6.5.10, including the 6.5.9 security fixes and follow-up static-file compatibility repair.
- Preserve installed-package self-scan findings in GitHub uploads by anchoring them to retained generated evidence; keep the original SARIF and the severity gate unchanged.
### Added
- Map bounded, explicitly configured warehouse-export rows into versioned graph evidence through `agent-bom ingest warehouse`, preserving tenant-scoped identities, recorded context and observation times with unknown collection coverage.
- Add independent-process graph storage qualification, content-verified restart receipts and an authenticated public-contract consumer probe with bounded, revision-pinned evidence pages.
- Version the graph interchange and node-evidence provenance contracts. Preserve recorded source and observation metadata across graph JSON, REST, MCP and entity details without treating collector names as successful execution.
- Export a selected graph entity's loaded recorded relationships as an investigation JSON bundle. Preserve the snapshot generation, canonical entity/finding identifiers, relationship evidence, page completeness and a return link; disclose unknown collection coverage and unsigned local-export provenance.
- Saved graph views notify when a newer snapshot of the same kind is available. Open it explicitly or keep the current investigation; background metadata checks preserve the pinned snapshot, selection and viewport.
### Fixed
- Preserve structured SQLite failure diagnostics and verify identical page-content contracts in thread and process graph qualification.
- Keep blast-radius counts, bounded canvases and entity details on one committed snapshot revision. Preserve recorded context relationships and disclose traversal lower bounds separately from the visible map.
- Pin graph, search, agent-selector, attack-path and rollup continuation to committed read revisions, separately from write ownership. Reject replaced snapshots and preserve evidence-first queue order in the investigation UI.
- Backfill committed graph read revisions through PostgreSQL bootstrap as well as migrations; preserve tokens on replay. Count only authoritative queue paths in investigation headings and retain actionable guidance for unsupported backend paging.
- Serialize SQLite graph schema inspection and backfill with other writers to prevent startup lock-upgrade races during concurrent ingestion.
- Avoid repeated normalization of absent exposure attributes during cold graph rollups while retaining aggregate counts and filter semantics.
- Keep routine snapshot polling out of the graph layout; show update controls when a newer snapshot or a failed check needs attention.
- Keep filtered attack-path investigations pageable when no loaded path matches. Retain loaded evidence after a failed page request and reject pages from a different tenant or snapshot.
- Use finding severity for investigation filters independently of path priority, and preserve recorded bidirectional witnesses when an attack path traverses them in reverse.
## [0.107.0] - 2026-09-30
### Added
- Guided first scans connect an empty workspace to scan and connection actions, with browser-local report previews and an explicit return to live evidence. CLI quickstart prints the saved report, package-CVE coverage boundary and exact graph snapshot link.
- Context Map supports Auto, Horizontal and Vertical layouts, bounded recorded-relationship inspection, and separate Fit all and Readable view controls. Collapse preserves independently loaded branches and reframes the remaining canvas.
- Local readiness checks work without network probes and retain skipped or incomplete states. Imported reports disclose partial or unknown collection coverage; file timestamps remain distinct from successful live scans.
- Export and validate an experimental per-agent BOM from an exact agent in a completed scan through CLI, API and dashboard. The profile retains source and coverage references; structural validation does not establish evidence authenticity or compliance.
- Authenticated read-only Jamf Pro and CrowdStrike Falcon inventory sync with encrypted credentials, resumable collection, tenant-isolated SQLite/Postgres evidence and explicit freshness/collection gaps. Manage connections through the API, CLI and a bounded Connections → Endpoints tab; inspect inventory and run authorized syncs through MCP. Sensor health remains distinct from policy compliance.
### Security
- Require PyJWT 2.15.0 or newer so deeply nested JWT payloads raise documented token errors before signing-key lookup (CVE-2026-101918).
- Require urllib3 2.8.0 or newer for HTTPS proxy TLS isolation, bounded chunk-size parsing and terminating Deflate streams (CVE-2026-97687, CVE-2026-97688, CVE-2026-97689).
- Bind anonymous API exemptions to exact HTTP operations and reject unclassified protected operations. Scope scheduled work, source records and export operations to explicit tenant authority; revalidate stored credentials on active streams and reconnects.
- Reject malformed entries throughout browser-imported reports, including decoded structural keys, oversized UTF-8 input, invalid exposure labels and inconsistent severity totals. Rejection messages do not echo report contents.
- Keep trace correlation tied to the submitted input and authenticated session. Invalid gateway enforcement settings fail before startup, and failed visual screening withholds upstream results.
### Changed
- Findings pages (`/v1/findings`, `/v1/compliance/hub/findings`) order findings that tie on score and timestamp by code point of `canonical_id` (and `last_seen` / `first_seen`) on Postgres, matching SQLite. Previously a Postgres database with a locale collation such as `en_US.UTF-8` could order those ties differently. Existing cursors keep working. The Postgres migration `20260928_01` adds `COLLATE "C"` versions of the findings sort indexes, built concurrently, so every sorted page stays an index scan; a cursor page now seeks directly to its position instead of filtering the rows before it.
### Fixed
- Self-scan SARIF advisories retain their installed-environment identity as a logical location, avoiding GitHub's rejection of non-file source URIs. Real package manifest paths and lines remain physical locations.
- Retry bounded SQLite WAL startup contention when graph reads overlap initial graph creation; close failed startup connections and continue to reject non-contention database errors.
- Resolve CommonJS namespace aliases for command-execution analysis, including `node:child_process`. Readiness checks distinguish the OSV lookup cache from the vulnerability database and report the same database staleness threshold used by scans.
- Malformed CloudFormation template, resource and policy containers no longer abort a repository scan. Reports retain findings from valid resources and mark unevaluated input as partial coverage; IAM policies with a single Statement object are evaluated.
- Preserve the finding occurrence and source scan across graph investigation. Exact snapshot and canonical entity links work across Context Map and Agent Mesh without substituting another entity when a lookup fails.
- Show recorded relationship endpoint labels, direction and canonical identifiers with bounded incident pages. Preserve loaded pages across responsive layouts and disclose unavailable labels, unknown totals and stale evidence.
- Serialize finding ingest across replicas and share atomic ledger/current-state writes. Tenant-scoped job keys, owned dispatch leases and generation-fenced graph rollback preserve concurrent work.
- Align REST and MCP graph exposure evidence, preserve cloud resource scope and native identity in graph joins, and bound page projection work.
- Exclude generated source trees and benign JavaScript calls from relevant scanner analysis; preserve explicit coverage gaps when analysis is incomplete.
- `POST /v1/scan` now runs the same secret scan as `agent-bom scan -p` for `agent_projects` and `filesystem_paths`, not only for `repo_url` clones.
- MCP `scan` queries vulnerability sources online by default (honoring `AGENT_BOM_OFFLINE`), returns a bounded, always-valid JSON summary with paged `result_id`/`section` follow-ups (`detail="full"` for the whole document), sets `isError` on incomplete or failed scans, and accepts extra workspace roots via `AGENT_BOM_MCP_WORKSPACE_ROOTS` or `agent-bom mcp server --workspace-root`.
- `--external-scan` no longer disables project auto-detection (IaC, SAST, secrets); when another input does skip it, the scan warns.
- External SARIF results are mapped by rule type: dependency advisories resolve to the real package@version and merge with native matches, code results stay SAST findings with file and line and merge with the native finding. Merged findings keep both `native` and `external:<tool>` source labels. CycloneDX and SPDX inputs to `--external-scan` import their vulnerabilities, or are ingested as an SBOM with a notice.
- Redaction keeps finding titles, secret types and credential variable names readable (`Hardcoded credential: Stripe Key`, `secret_type: api_key`) while still redacting values.
- A password in a database connection string (`postgresql://user:pass@host`) is a critical `Connection String` credential, not personal data.
- npm and PyPI version ranges are resolved (lockfile first, then registry metadata) instead of being floored to a version; an unresolvable range stays unresolved, is not matched against advisories, and never appears in purls, CycloneDX or SPDX.
- Online and offline scans derive severity from one documented CVSS precedence, so `--fail-on-severity` no longer depends on mode.
- `--fail-on-kev` fetches only the KEV catalog when `--enrich` is not set, accepts a fresh local DB as KEV evidence, and prints the remedy when it fails closed, including under `-q`.
- `--license-check` reports `status: undetermined` with license coverage when licenses are unknown, instead of `compliant: true`.
- Withdrawn advisories are skipped by the GHSA database sync and removed from the bundled demo data.
## [0.106.1] - 2026-09-26
0.106.1 is the first published release of this line. The `v0.106.0` tag exists but its release run stopped at the version guard before building anything, so 0.106.0 was never published to PyPI, Docker Hub, Helm or GitHub Releases.
### Security
- Scope `GET /v1/graph/exposure-paths` and `POST /v1/graph/should-i-deploy` to the authenticated request tenant. These REST routes previously resolved the MCP process tenant, so on SQLite every tenant received the `default` tenant's credential-exposure paths and deploy verdict, and on Postgres with row-level security every tenant received an empty result.
- Keep demo estate data out of product stores. Demo mode now pins every local store to `AGENT_BOM_DEMO_STATE_DIR` (default `<state dir>/demo-estate`) before any store opens, refuses explicit store paths that point into product state, and refuses to seed over a non-demo snapshot.
### Added
- Bounded relationship paging across SQLite, Postgres, REST, and persisted Context exploration. Tenant-scoped cursors and generation checks prevent combining evidence across replaced snapshots.
- Inspect recorded authority and source receipts at each graph path hop, with bounded pagination and preserved investigation scope.
- Optional, bounded AWS credential validation through STS, disabled by default and suppressed by offline scans. Validation errors remain unknown without removing findings.
- Add an explicitly configured Snowflake workload-identity adapter for scan connections, binding each delegation to the authenticated tenant and injected SPCS account and rejecting caller-selected roles or credential fallback.
- Add collapsible asset-type and name/ID/context filters to the environment scope summary, with bounded, expandable relationship details and indexed incident pagination.
- Group estate assets by recorded provider, account, and environment, and focus an asset and its connections through the shared evidence drawer.
- Add tenant-isolated, retry-safe comparable finding history with on-demand history charts and a values table, extending `/v1/trends` additively with bounded metadata retention.
- Add bounded environment-group navigation with aggregate recorded relationships, retained camera state, and searchable asset/relationship controls.
- Python one-hop tainted-argument evidence for function-reachable CVEs. This additive signal does not establish exploitability or full interprocedural data flow.
### Fixed
- Distinguish conditional and unknown IAM admin evidence, recognize account-wide IAM policy scope, and expose the assessment in collapsible identity details.
- Continue browser extension discovery when a Chromium profile directory is unreadable and mark the resulting coverage gap.
- Match fix-first agent filters by canonical identity or display label, expose additional loaded Context relationships, and simplify security-area summaries.
- Refresh stale local vulnerability coverage through OSV during online scans while preserving offline behavior.
- Preserve structured report identifiers during redaction without exempting embedded credentials.
- Bind exposure pagination to snapshot generations and reject NUL graph identifiers before database access.
- Calculate CVSS v4 scores with the macrovector algorithm, preserving the correct severity at build-gate thresholds.
- Keep curated demo agent discovery independent of host configuration and serve matching agent detail and lifecycle routes.
- Accept unknown severity when importing JSON reports.
- Retain findings totals when the count cache expires during an in-flight request.
- Evaluate collected IAM policy documents before assigning unconditional admin-equivalence labels, including explicit denies, resource scope, and unresolved conditions.
- Repair Snowflake Native App installation contracts and reject unsupported scan dispatch explicitly.
- Explore persisted Context connections with canonical agent IDs, compact canvases, explicit paging and collapse, and selected relationship labels. Clear stale or account-scoped evidence and resolve graph IDs without downloading full scan reports.
- Persist tenant-scoped export destinations on Postgres and preserve unknown vulnerability scores in warehouse feeds.
- Preserve shared scan jobs across replica startup and bind dispatch lease updates to the owning claim.
- Connect scoped agent investigation to permission receipts, advisory prerequisites, directional neighbor expansion, and exact-agent runtime activity; retain recorded runtime event references consistently in REST and MCP.
- Bind graph vulnerability relationships to matching package owners, preserve package-specific evidence, and reject contradictory server matches.
- Require exact agent and tool identities for related runtime activity; distinguish scan configuration from execution and make Context Map relationship evidence inspectable.
- Reconcile final terminal severity totals with JSON after SAST scanning, enrichment, and filtering.
- Accept nullable advisory scores and current CLI tool fields when importing JSON reports.
- Keep imported report findings separate from tenant posture, coverage, services, and scan freshness; label tenant-wide compliance exports explicitly.
- Select ranked Context Map paths with keyboard support, discard stale scan responses, and preserve readable path drawers and graph framing across themes and viewport sizes.
- Preserve distinct authorization receipts, action-specific evidence, workload connections, and source witnesses across graph and exposure responses.
- Require evidence before reporting verified remediation, and keep runtime outcomes separate from exploitability signals.
- Improve scoped inventory, findings, compliance drilldowns, graph framing, and light/dark metric readability. Empty API snapshots now return an empty summary.
- Preserve dashboard metadata in refreshed images and enforce deployment and registry freshness checks while retaining Smithery as an explicitly non-blocking surface.
- Keep screenshot proof sensitive to rendering toolchain and resolved dependency changes while allowing Node type-only maintenance updates.
- Persist Native App graph and control-plane evidence, including the audit signing key, on an encrypted per-tenant volume so restarts preserve verification.
- Support opt-in external OAuth verification for remote MCP with a pinned issuer/audience and bounded token lifetime, and obtain fresh read tokens for deployment probes instead of embedding credentials in command arguments or logs. The public server card advertises the `oauth2` scheme in that mode and `bearer` only for static credentials.
- Render the agent mesh from paged persisted graph relationships, publish Native App audit keys atomically under a directory lock, and expose Snowflake workload onboarding only in Native App mode.
- Retain supporting agent identifiers and minimum hop distance when converting blast-radius results into finding rows, so corroborated reachability evidence is no longer dropped during response sanitization.
- Align demo agent-to-MCP-server relationships across the estate graph, fleet server counts, and MCP observation agent lists so they no longer diverge.
- Reject malformed SCIM `active` values instead of coercing them into lifecycle changes, and retry idempotent credential revocation when deactivation is repeated after a store failure.
- Preserve declared agent model and tool associations, including positional LangChain `initialize_agent(tools, llm)` calls, so tool and data-flow findings are retained.
- Distinguish unassessed graph-node risk from scored evidence, showing "Not assessed" consistently across graph inspection, search, and inventory while preserving an explicitly assessed zero score.
- Bound initial graph requests to 250 ranked assets with explicit partial counts, loading the broader selection only on request while keeping full-estate search available.
- Preserve readable graph framing and the investigation root after closing details, and stop native filter navigation from interrupting incoming client navigation.
- Add bounded, stable-identifier search to discovered identity rows, and bound API response caching to 64 entries with least-recently-used eviction and expiry cleanup.
- Keep copy-paste install surfaces on the latest published release. GitHub Action refs, pull-only Compose and Kubernetes image pins, Helm/bundle commands, and consumer pre-commit revs now track `PUBLISHED_VERSION`, which the release run advances only after publishing, so docs on `main` no longer point at an unreleased tag or image.
- Keep the daily `latest` image refresh labelled with the rebuilt release version when it applies the default branch's runtime security overlay, so the Docker Hub README sync no longer fails its release consistency check.
- Stop printing the ephemeral audit-signing-key warning in CLI scan output. It is now logged at API startup and when an audit entry is first signed without `AGENT_BOM_AUDIT_HMAC_KEY`.
- Replace the "Contact your administrator" dead end on the dashboard login when the API was started without any key. The page now shows the restart commands (`agent-bom api --api-key <key>`, or `--allow-insecure-no-auth` for local use); authentication behavior is unchanged.
- Point CLI help `Docs:` links at the documentation site, and stop directing users to the `mcp-server` extra for `agent-bom mcp server`, which works from the base install.
- Fetch every release tag in the release version guard. A tag-push checkout carried only the pushed tag, so the guard reported the published release as untagged and stopped the release.
- Store OSV version pins only when no stored range already matches the version, shrinking the PyPI advisory table from 1.34M to 44k rows (204 MB to 12.6 MB) with identical match results. Existing databases shrink on the next sync.
- Rescore stored CVSS 4.0 vectors at read time, so databases synced before the CVSS 4.0 fix no longer report inflated severities, including read-only databases.
- Count unreadable Firefox/Chromium extension entries, discovery probes, MLflow runs, and dpkg `status.d` directories as coverage gaps instead of aborting the scan or, on Python 3.14, skipping them silently.
- Match MCP registry and `fleet_scan` entries by exact package identifier (version stripped, PEP 503 names for PyPI, no match when ambiguous), so local servers are no longer labelled as unrelated third-party packages.
- Follow markdown paragraph rules in skill audit, so a negated instruction wrapped across lines is no longer reported as a critical bypass.
- Start SAST taint analysis at Flask, Quart, and FastAPI route handlers, not only MCP tool handlers. FastAPI `Depends()`/`Security()` parameters are not treated as sources.
- Give graph agent nodes the risk score and severity of the worst vulnerability they reach. Agents that reach none stay unassessed rather than scoring zero.
- Set `graph_reachable`, minimum hop distance, and reaching agents on CLI scan findings from evidence-bearing scan graph paths, using the same rule as the API. Topology-only paths still leave reachability unset.
- Page `GET /v1/graph/rollup?node=` drill-down with `offset`/`limit` (default 200) instead of returning every child, and support rollup on the Neptune backend.
- Show one findings count across the nav badge, dashboard, and findings page. `/v1/posture/counts` adds `issues` (issue-group counts matching the page) and `agents` (distinct agents in the tenant's current graph snapshot); occurrence totals are unchanged.
- Report a slow or failed enterprise demo build as loading or failed instead of "not enabled".
- Open the graph on the first level that offers a choice, and offer retry or a bounded map when a rollup fails instead of falling back to the raw topology.
- Advertise `oauth2` in the MCP server card when an external OAuth issuer protects the transport.
### Changed
- Increase opt-in DSPM object sampling from 10 to 50 objects per bucket/container and database sampling from 100 to 500 rows per table; byte and cell limits remain bounded.
- Update dependency and CI action pins, registry metadata, and operator documentation.
- **Upgrade note:** with `AGENT_BOM_STATE_DIR` set, assets, local analytics, history, baselines, report artifacts, outboxes, and catalogs now live under that directory instead of `~/.agent-bom`. SQLite deployments without `AGENT_BOM_DB` keep graph scenarios in the state-directory control-plane database instead of `./agent_bom.db`. Existing files are not migrated; move them before upgrading to keep them.
- `agent-bom api --persist` now sets `AGENT_BOM_DB` like `serve`.
- The README opens with the agent context map, self-host and deployment models; the quick-start sample output is collapsed.
## [0.105.0] - 2026-09-15
This release makes scan coverage and finding evidence clearer, adds explicit
credential validation, and improves the path from findings to investigation.
### Added
- Opt-in `agent-bom secrets --validate-credentials` checks for supported GitHub
and Stripe credentials, with bounded read-only requests and
`valid`/`invalid`/`unknown` verdicts. Default scans make no validation requests;
unsupported credentials remain unknown and finding severity is preserved.
- Optional Iceberg REST OAuth scope and token-endpoint configuration for Apache
Polaris, alongside existing credential and bearer-token authentication.
### Fixed
- Align the PyPI description with the seven shipped MCP resources and check
resource/prompt counts in lists that contain serial commas.
- Report excluded ancestor directories through shared scanner coverage and API
outcomes so an uninspected scope cannot silently appear complete.
- Preserve structured CWE identifiers in the finding drawer and standard SARIF
taxonomy relationships, including findings without CWE text in their prose.
- Scan upstream JSON-RPC errors, notifications, and response metadata through
shared proxy/gateway DLP. Inspect malformed stdio output in audit mode and
drop it in enforce mode; preserve the configured handling of detected secrets.
- Reap the proxy child on SIGTERM and wait for normal server exits before
sending shutdown signals. Forced termination with SIGKILL cannot run cleanup.
- Resolve positional tool arguments for supported LangChain agent constructors,
including imported aliases, so tool and data-flow findings are retained.
- Reconcile canonical finding occurrences across scan and hub ingestion in
findings, exports, and Overview counts. Hub-managed lifecycle and enrichment
take precedence; findings on distinct assets remain separate.
- Accept bounded compressed report uploads, reconcile package versions without
merging ambiguous findings, and evolve the additive Parquet/Iceberg schema
while preserving existing field identifiers and rows.
- Describe remediation taxonomy mappings without implying that controls have
passed, and keep the summary aligned with the filtered packages.
- Keep the full posture Overview first and Top risks in a second tab. Lead risk
summaries with affected workloads and conditional impact, with package,
advisory, and path details available on expansion.
- Admit graph requests before expensive reads, preserve the canvas during
background refresh failures, and restore bounded drill-in, direction/depth
controls, relationship navigation, and actionable empty states. Reserve space
for desktop detail drawers so focused nodes remain visible beside them.
- Reduce concurrent SQLite roll-up latency with batched topology reads while
retaining complete counts, tenant scope, and consistent snapshots. Attack-path
investigations continue to load full evidence.
- Preserve OSV enumerated affected versions alongside ranges, including mixed
GIT and ecosystem advisories such as Pillow's CVE-2022-45199. Existing local
databases need an OSV refresh after upgrading: `agent-bom db update --source
osv --osv-ecosystem PyPI` (repeat for other ecosystems in use).
- Paginate asset inventory and organize Identity and Audit tasks into tabs;
simplify Fleet and Connections, remove displayed identity token prefixes,
and distinguish evaluated controls, risk mappings, and unavailable checks.
- Use published package versions and real advisories in clearly simulated demo
scenarios; refresh Overview and framework screenshots to match the application.
- Preserve repository inventory as code assets in graphs and retain durable
report-export work across replica recovery.
### Changed
- Reuse the dependency-audit baseline only when its frozen Python inputs are
unchanged; fail the OSV gate on scanner errors or invalid reports.
- Skip redundant CodeQL analysis on prose/image-only main pushes while retaining
pull-request, merge-queue, scheduled, and manual analysis.
- Refresh supported dependency pins and release-registry verification checks.
## [0.104.0] - 2026-09-08
This release improves the path from scoped scans to shared findings,
operator review, and opt-in runtime controls. AI spend remains an estimate of
recorded token usage; runtime receipts distinguish reported behavior from
producer assurance.
### Fixed
- Preserve live gateway activity through the production dashboard proxy and
resume from the last event cursor after reconnect. Postgres identity upgrades
add the managed-profile lookup while preserving legacy installations that
have not provisioned identity storage.
- Upgrade the dashboard's YAML parser and image-processing dependencies to
js-yaml 4.3.2 and sharp 0.35.4. Distribution freshness checks now pass their
configured timeout and retry budget to the Glama probe.
- Preserve the imported report’s scan ID in finding links so investigation opens its persisted graph snapshot.
- Keep AI spend visible within Overview coverage, with compliance and top risks directly below the main cards.
- Keep advisory fixes scoped to their package and affected range; unknown fixes no longer inherit another package’s version.
- Focused investigation graphs retain identity and data nodes that belong to the
selected path, even when those layers are hidden in the broader estate view.
- Scan caching honors `AGENT_BOM_STATE_DIR` in read-only containers while
preserving explicit cache-path overrides.
- Headline AI spend, call, token, and agent totals cover all retained matching
records across memory, SQLite, and Postgres. Detail history is bounded and
labeled; unavailable storage, unpriced calls, and unsupported forecasts stay
explicit. Tenant budget fallback uses tenant spend.
- Runtime event retries preserve canonical identity across new HTTP request
traces. Submission origin, reported producer origin, receipt metadata, and
unknown or caller-asserted producer assurance remain distinct in persisted
records, feeds, metrics, and activity views.
- Disk side-scan retries bind to the full request scope. Setup failures and
incomplete cleanup retain recovery evidence; failed unmounts preserve owned
temporary resources for inspection.
- Skills scans retain the authenticated tenant in audit evidence and use
configured SQLite result storage. Unsupported remote result backends reject
scans before side effects; the UI clarifies mounted target scope, empty
results, and unavailable evidence.
- Findings preserve observation anchors and SLA deadlines. Dashboard coverage,
framework marks, finding columns, graph framing, and mobile cost tables are
easier to inspect in light and dark themes. CLI help avoids duplicate commands.
### Changed
- Manage server-side runtime profiles with `agent-bom runtime profiles`
create/list/validate/test commands, the API clients, and the dashboard.
Validation and test previews evaluate configured bindings; they do not
authenticate a caller or authorize a live tool call.
- Follow persisted gateway activity with `agent-bom runtime feed --follow
--cursor-file ./activity.cursor.json`. The dashboard uses authenticated SSE
with cursor backfill and explicit disconnected or degraded states. See
`docs/RUNTIME_REFERENCE.md` for retention and deployment proof boundaries.
- Runtime evidence ingestion now requires an expiring, source-scoped API key
or verified OIDC credential. API bodies, MCP arguments and CLI flags no longer
accept source secrets. CLI/MCP producers use the configured control-plane API;
legacy shared-secret source registrations fail closed. See the runtime
producer migration guide in `docs/CLOUD_CONNECT.md`.
- MCP server startup now exposes eight scan and remediation tools by default.
Select graph, cloud, runtime or audit profiles for specialized work, or use
`--profile full` to preserve the complete catalog in existing clients.
Prompts, server cards and health counts reflect the active profile;
`profiles://catalog` provides discovery without loading every tool schema.
Authentication, tenant scope and write authorization remain required.
## [0.103.2] - 2026-09-01
This patch makes the correlated-evidence journey automatic and easier to read,
while advancing the maintained dependency and MCP-registry baseline.
### Changed
- The committed reference evidence lab now executes the real repository,
CycloneDX, Kubernetes IaC, MCP configuration, advisory, correlation, and
authenticated gateway paths. Its generated proof binds every input artifact,
exact-identity join, signed runtime-facts bundle, live allow receipt, and
strict pre-upstream block receipt.
- The evidence journey is labeled Connect, Discover, Scan, Correlate,
Investigate, and Enforce (opt-in); fresh snapshots are selected by default,
stale or future-dated inputs remain explicit, and completed scans always
offer truthful Findings, Investigation, and Remediation handoffs.
- Investigation selects the newest completed correlation when no explicit
snapshot is deep-linked, keeps correlation setup secondary to the resulting
graph, and presents the evidence journey clearly across desktop, mobile,
light, and dark views.
- Correlated path proof now distinguishes complete, directed, traversable hop
evidence from incomplete or truncated receipts before presenting a path as
verified.
- Mobile Investigation renders long attack paths as readable sequential hops
and directed relationships instead of shrinking the complete chain into a
miniature canvas.
- Attack-path layout now collapses according to measured canvas width before
labels become unreadable, and light-theme risk cards use accessible text
contrast without changing risk semantics.
- Python, JavaScript, MCP, and CodeQL dependencies plus the bundled MCP registry
are refreshed under their existing release and serialization contracts.
- Private SQLite graph-build workspaces avoid durable fsync overhead while
retaining transactional staging semantics; durable graph stores are unchanged.
### Fixed
- Postgres observation migrations provision the full retained-history window as
well as the future runway, so backdated connector evidence no longer attempts
partition DDL through the least-privilege application role; stale deployments
receive a sanitized retryable response instead of a raw database failure.
- Correlation now canonicalizes exact package URLs without merging invalid or
case-distinct identities, compares observation times as UTC instants,
revalidates freshness when queued work executes, rejects excessive future
skew, and confirms only complete directed, traversable, provenance-backed
paths within their recorded analysis bounds.
- MCP runtime introspection now rejects security-blocked servers before any
transport is opened, keeps stdio process launch disabled by default, and
requires both explicit opt-in and authenticated operator authority before a
discovered command can run.
## [0.103.1] - 2026-08-29
This patch restores Alpine arm64 API-image publication and makes the
release-shaped multi-architecture image proof merge-blocking for relevant
Docker and dependency changes.
### Fixed
- Alpine arm64 API image builds now supply the exact missing
`tree-sitter-typescript` 0.23.2 build headers, and Docker input changes run
the release-shaped multi-architecture gate before merge.
## [0.103.0] - 2026-08-29
This release expands repository and architecture evidence beyond MCP-only
surfaces, strengthens fail-closed identity and audit boundaries, and removes
large-scan bottlenecks across scanning, graph projection, SBOM ingestion, and
API result delivery.
### Added
- Repository scans inventory remote GitHub Actions and reusable workflows with
exact-versus-mutable reference provenance across JSON, CycloneDX, SPDX, and
graph exports without treating those coordinates as an advisory ecosystem.
- Reachability analysis models evidence-backed application entrypoints such as
console scripts, main guards, Click/Typer commands, and FastAPI/Flask routes,
while keeping ordinary exported functions outside the reachable set.
- Tenant-scoped architecture scenarios compare current and proposed graph
states with deterministic deltas without mutating observed inventory or
attack-path evidence.
### Changed
- SARIF files use compact JSON encoding and indexed result taxonomy references;
extension taxa omit names that duplicate their IDs. The bundled demo
artifact is less than half its former size without removing results,
taxonomies, or evidence fields.
- OIDC setup emits file references instead of client-secret values. Compose
control-plane profiles load the generated non-secret env file and mount a
dedicated OIDC secret directory read-only; dashboard and Helm guidance use
the same file-first contract. The deprecated `--client-secret` compatibility
alias accepts only `@/absolute/runtime/path`; new automation must use
`--client-secret-file <absolute-runtime-path>`. Secret bytes are never
accepted, echoed, read, or written by the wizard.
- The graph opens on a risk-ranked estate investigation with authoritative
facets, deduplicated paths, bounded traversal, and an optional expert canvas.
- JavaScript and TypeScript AST parsing plus repository ignore semantics now
ship in the base installation, so first-run source and secret analysis does
not depend on optional parser packages.
### Fixed
- Budgeted and filtered SQLite graph loads now seek edges from the selected
node ids instead of scanning every edge in the snapshot and discarding most
of them in Python.
- Large scans reuse canonical report and graph projections, bound legacy
advisory coverage queries to inventory ecosystems, preserve distinct finding
occurrences, and avoid repeated redaction and CVE materialization work.
- CycloneDX dependency and vulnerability joins use authoritative `bom-ref`
indexes, reducing recovered 20,000-component fixture parsing from 18.548s to
0.132s while preserving duplicate component identity.
- Runtime reachability follows framework-registered tools and resolved
dependencies, preserves exposure-path topology, and uses attack-path evidence
rather than structural closure for reachable findings.
- Compliance and overview projections exclude failed, skipped, dry-run, and
no-scan jobs and require canonical, current scan evidence.
- Credential and scheduler source lifecycles close cleanly, scan targets must be
executable, and gateway audit delivery and tenant routing remain durable and
isolated.
- Purging secret-shaped legacy material from an already-retired credential
reference now records one tenant-scoped, sanitized audit event without
duplicating the event on idempotent DELETE retries.
- Full scan-result API responses now sanitize large AI-BOM payloads in the
bounded worker pool so polling them cannot monopolize the server event loop.
- Image and registry release gates preserve truthful multi-architecture
evidence, idempotent publication, and immediate post-release verification.
### Security
- API-key delegation, browser and WebSocket sessions, SAML/OIDC state, SCIM,
Snowflake state, revocation, replay protection, and shared brute-force limits
fail closed when their authoritative shared state is unavailable.
- Forwarded client identity is accepted only through an explicit bounded proxy
depth from allowlisted transport peers; direct callers cannot spoof private
network policy with forwarding headers.
- Exception logging, graph scenario errors, outbound evidence, native secret
findings, and repository scans use sanitized, ignore-aware projections that
do not expose credential values or server internals.
- PostgreSQL audit and governance writes serialize per tenant with persistent
signing, while runtime and source audit paths reject fallback routing that
could cross tenant boundaries or lose required evidence.
## [0.102.0] - 2026-08-23
A release-integrity and evidence-truth release. Published Python images remove
runtime packaging tools and their vendored dependency trees on both supported
architectures. Container evidence is accepted by GitHub code scanning, and
release completion now carries image
and registry fixes through independent live verification instead of leaving
automated findings open until the next daily schedule.
### Added
- The MCP control-plane surface now exposes 84 tools, including tenant-bound
exception listing, request, and approval workflows over the same lifecycle
store used by REST and the dashboard. Writes require the operator token,
`findings:write`, an administrator role, and an audit reason.
- Runtime self-governance ships as a policy template and blueprint with
durable-versus-fallback activity labels, cursor pagination, self-posture
evidence, and an operator runbook.
### Changed
- Large graph snapshots open on bounded, risk-ranked decision rollups; operators
can switch to the expert relationship map, traverse the proving path, and use
resizable, tabbed detail drawers without loading a dense estate as the first
view. Entity and relationship colors now derive from one schema.
- Scan jobs expose trigger, schedule, source, vulnerability-data freshness,
terminal timestamps, per-stage statistics, and the collector issues attached
to a degraded stage.
- Grouped findings with facets reduce and project the requested page before
sanitizing and serializing occurrences, avoiding repeated whole-window work
while preserving group identity and graph provenance.
### Fixed
- Canonical finding identity now distinguishes non-package asset evidence, and
vulnerability exceptions and local ignore rules converge on one auditable
suppression overlay across console/MCP active views, JSON, SARIF, VEX, and
severity exit gates. Suppressed evidence remains queryable rather than being
deleted.
- The dashboard uses readable light-theme surfaces for agent inventory,
consistent bounded drawers, and an evidence-first overview instead of a
promotional workflow panel. The public preview separates AI/MCP clients from
cloud, identity, and data integrations and states that integration depth
varies by surface.
- Documentation SVG generation resolves the current checkout before importing
product counts, preventing an older installed package from silently producing
stale architecture diagrams.
- Offline repository scans with a missing or unreadable vulnerability database
now return the documented incomplete-scan verdict (`1`) while preserving a
partial artifact, instead of misclassifying valid input as usage error `2`.
- Ranked attack paths render independently from slower full-estate fix guidance,
expose their correlated role chain, support bounded traversal from each hop,
and transfer only the relationships that prove the returned paths.
- The labeled sample estate now records an idempotent two-point posture
trajectory through the production trend store, and the public scanner story
links its mutation-tested OSV range-matching evidence without presenting the
bounded benchmark as universal accuracy.
- Graph label budgets survive accessibility normalization, persisted attack
paths derive from the final projected estate, and forced demo seeding never
displaces newer operator evidence. The demo now exposes and renders an
authenticated graph-ownership status before constructing graph deep links.
- Published amd64 and arm64 Python images install pip 26.2 with reviewed hashes,
clearing CVE-2026-13346 from the shared runtime overlay.
- Container SARIF normalization supplies GitHub-compatible physical context for
logical exposure-path hops while preserving the original image reference.
- Successful releases immediately rescan both published architectures and record
immutable image identities in automated issue evidence.
- Automatic forward releases fail when Smithery publication is skipped, and a
successful registry run immediately rechecks every public distribution surface
so stale-inventory findings close only after live convergence.
- The consumer pre-commit dependency hook now scopes scans to the downstream
repository instead of including unrelated workstation MCP configuration.
- Baseline and history diffs keep a CVE attached to the same normalized package
across version upgrades, while inventory diffs continue to report the package
version change separately.
- RPM scans preserve nonzero epochs, cloud benchmark PASS results require a
verified account boundary, and structural dependency closure no longer
masquerades as an evidence-backed reachable attack path.
- Security-relevant native AST flows now surface through unified findings,
SARIF, policy gates, and source-to-tool graph provenance; inventory-only LLM
client calls remain inventory rather than vulnerabilities.
- PostgreSQL audit and governance chains serialize tenant appends with
transaction-scoped advisory locks, reconcile migration-owned guards, and
require persistent HMAC signing for shared durable ledgers.
- Bare ASGI startup enforces the database RLS-role preflight, while public-demo
operator pages withhold deployment posture and integration diagnostics.
- OWASP LLM, MCP, and Agentic Top 10 risks are reported as applicability
overlays instead of passed or failed controls across REST and MCP; the MCP
surface now includes the Agentic catalog and preserves scored totals.
- Secret scans suppress explicit sample placeholders without hiding real
assigned passphrases, and MCP prompt heuristics no longer match words such as
`ecosystem` merely because they contain `system`.
- Amazon Linux, Oracle Linux, and SLES package inventories now emit explicit
advisory-source-unavailable evidence instead of a false-clean zero while
their dedicated advisory feeds remain unsupported.
## [0.101.0] - 2026-08-16
A workflow-completion and deployment-portability release. The same finding can
now move from scoped evidence through ownership, remediation, verification, and
export across the control plane, CLI, MCP, and dashboard. The release also makes
the self-hosted storage boundary explicit, adds portable evidence-table
publication, and replaces several release-time assumptions with fail-closed
proof.
### Added
- Finding lifecycle operations now persist owner, SLA, ticket, remediation,
retry, and verification history with tenant-scoped authorization and stable
idempotency across REST, CLI, MCP, and dashboard workflows.
- The dashboard guides investigations through Path → Impact → Owner and SLA →
Fix → Verify, with persona-specific starts, scoped counts, progressive graph
disclosure, accessible text summaries, and bounded tables.
- Skills scans and AWS, Azure, and GCP workload side scans are available through
the control plane, CLI, MCP, scheduler, and dashboard. Credentialed Azure/GCP
acceptance remains tracked separately and is not claimed by this release.
- Opt-in, disabled-by-default adoption events measure completed scans, artifacts,
investigations, verification, repeat use, CI adoption, and control-plane
adoption without collecting source contents, raw findings, credentials, or
customer resource identifiers.
- Managed Postgres readiness, capability reporting, Helm validation, reproducible
scale evidence, and portable Parquet/Iceberg evidence-table publication were
added for customer-controlled deployments and downstream security lakes.
### Changed
- Finding, path, correlation, returned, and total counts now carry explicit
source, scope, filters, completeness, and synthetic-versus-persisted semantics.
- Repository scan results expose existing AI-BOM, CycloneDX, and SPDX artifacts
beside the repository context graph instead of requiring a separate workflow.
- Investigation lenses share one graph surface, while large estates use bounded
Sigma rendering, filterable drill-downs, and readable light, dark, desktop,
mobile, keyboard, and reduced-motion states.
- Owner, SLA, and lifecycle state are preserved across CSV, Markdown, HTML, PDF,
OCSF, CycloneDX, and SPDX exports; shared-CVE CycloneDX workflow metadata stays
scoped to the affected package reference.
### Fixed
- Public counts, persona language, generated metrics, registry metadata, and MCP
deployment inventory now derive from checked product contracts instead of
drifting independently.
- Synthetic demo timestamps and auxiliary Skills, CWPP, campaign, graph, and
governance surfaces are deterministic, tenant-scoped, explicitly labeled, and
protected from replacing real tenant graph data.
- CWPP execution lists, Skills files, findings, and blast-radius results no longer
silently hide older rows or render unbounded card sets; filters and pagination
are applied consistently.
- Light-theme cloud evidence and graph surfaces use readable contrast and shared
surface tokens.
### Release integrity
- A release tag must point to the current `main` SHA and that exact commit must
have a completed successful `CI/CD Pipeline` main-push run. Pending, canceled,
stale, PR-only, or wrong-workflow evidence fails closed.
- The release evidence matrix now covers source commit, Python distributions,
dashboard, API/UI/collector images, Helm OCI, GitHub Action, MCP metadata,
registries, SBOM, signatures, provenance, and the hosted demo.
- CI now bounds Python and Alpine suites without weakening their assertions,
isolates benchmark environment state, validates exact MCP deployment inventory,
and keeps dependency-license exceptions package-scoped.
## [0.100.0] - 2026-08-11
An output-honesty release. Every local gate was already green before this work
started; nothing here fixes a crash or a build. What it fixes is the gap between
what agent-bom found and what it told you — findings that were computed and never
surfaced, bounded reads that claimed to be complete, a scanner that reported
clean where it had not looked, and two gates on the release path that could not
have failed if there had been something to catch.
If you run agent-bom against RHEL/UBI images, or read the executive view, or use
the graph, this release changes what you see.
### Security
- **Two gates on the release path could never fail.** GitHub runs an unqualified
`run:` step as `bash -e {0}` — without `-o pipefail` — so `osv-scanner … | tee`
exited with `tee`'s status and the `|| { … }` fixable-CVE branch below it was
unreachable. That dead branch was the blocking logic in `ci.yml` *and* in
`release.yml`, where the step is named "block release on fixable CVEs". Every
release that passed that step passed it vacuously. The same defect was found in
17 steps across 9 workflows. Both halves are worth stating plainly: the gate
could not have reported a vulnerable dependency — and, run for real against all
three lockfiles, it reports none, so nothing was shipped vulnerable.
`scripts/check_ci_pipefail.py` now blocks the class.
- CI bootstrapped Postgres from `init.sql` alone while the Docker entrypoint
mounts `init.sql` *and* `runtime-schema.sql`. Both paths ended on the same
Alembic head with a different schema — `hub_findings_current_observations`
partitioned with `observed_at timestamptz` on one and plain with
`observed_at text` on the other. Matching heads never proved matching schemas;
a `pg_dump --schema-only` diff now does.
- A read-only bind mount in `deploy/docker-compose.yml` pointed at `deploy/tests`
rather than the repository's `tests/`. Docker creates a missing read-only bind
source as an empty directory instead of failing, so the container scanned
nothing and had no way to say so.
- Secret scanning no longer reports a file it never opened as clean. Files over
the size ceiling and files that could not be read were returned as "no
findings" and counted toward `files_scanned`. They are now named in `warnings`
and excluded from the coverage count. `.tfstate` and `.tfvars` join the
scanned extensions — `.tf` was already there, so the two files in a Terraform
repo most likely to hold a plaintext provider credential were never opened at
any size.
- WebSocket streams failed open when the auth posture could not be derived, and
DLP excerpts echoed the secret they were redacting.
- Microsoft Graph pagination is pinned to the origin that issued the token, so a
bearer token can no longer follow a `@odata.nextLink` off-origin.
- The governance audit chain gained the fork guard its sibling chain already had.
- Ingested `graph_reachable: true` is rejected unless the row names an agent that
reaches the finding. `dependency_reach` defines reachable as
`bool(reachable_from)`; a claim with no path contradicted that definition and
still collected the reachability risk boost, outranking findings the engine had
actually proved.
### Fixed — wrong or missing output
- A `min_severity` filter no longer deletes the graph it was meant to narrow.
Only findings carry a severity, so applying the floor to every node dropped
the agents, servers, packages, resources and identities the findings hang
off. One backend already exempted them on one code path; every other read
path — both `load_graph` implementations, four Postgres sites, the Neptune
adapter and the in-memory filter — did not, so the same estate answered
differently depending on which store replied. Drift incidents were also
missing from the in-memory rated-type list.
- A graph node budget now bounds the read rather than the result on SQLite,
which previously loaded the whole snapshot before trimming it. At 200k nodes
under a 5,000-node budget: 270 MB peak and 22.9s before, 6.4 MB and 2.8s
after, returning the same nodes.
- `GET /v1/graph/search` now rejects a deep `offset` past the documented cap,
as `/v1/graph` and `/v1/graph/agents` already did; both stores enforce it too.
- `GET /v1/graph` documented an induced subgraph it has never returned. The
edge list deliberately reaches one hop past the node list — that is what keeps
a severity-ranked finding page attached to its assets — so the description is
corrected and `completeness.boundary_edges` now counts the crossing edges.
- Unfiltered graph statistics are served from the snapshot row on Postgres
instead of re-aggregating every node on each page view, matching SQLite. At
100k nodes: 73.5ms to 30.2ms.
- The hosted-demo deploy gate asserted an estate that no longer exists — exact
dict equality against 20 assets, 15 observations and 4 correlations, where the
seeded estate now builds 3,860 assets, 12,171 observations and 2,322
correlations. That gate runs *after* the Release workflow, so a tag would have
shipped the release and broken the demo deploy in the same run. It now asserts
the contract — shape, floors, and the source breakdown that is genuinely fixed
— rather than a snapshot of one day's numbers.
- `agent-bom demo` printed `http://127.0.0.1:8000/demo-estate` while the API
serves 8422.
- The advertised MCP registry size was stale on six surfaces, **two of them
strings the product emits at runtime**: the `registry://servers` resource
description every MCP client reads said 1,013 servers while the resource
returned 1,081. `docs/skills/mcp-server-review.md` additionally claimed 940
verified entries against an actual 60 — a 15x overstatement of verification
coverage on a security registry. The count is now derived from the bundled
registry at registration time, and a test sweeps whole shipping-surface trees
— including SVG diagrams, which the consistency check skipped entirely — rather
than a hand-maintained file list.
- The documented first-run command did not run: `examples/` taught
`--no-update-db`, a flag that has never existed. The test written to catch
exactly this read `README.md` only and validated only *path* options, so
neither the file nor the flag name was in its reach. It now covers every
`examples/**.md` and asserts each flag it teaches resolves on the command.
- `sdks/python` had been at `0.92.0` against a `0.100.0` platform since it was
bumped by hand once and then forgotten. It re-exports the client out of the
`agent-bom` wheel it depends on, so it tracks the platform and is now owned by
`bump-version.py`. The TypeScript SDKs stay independent on their own 0.x npm
line — retagging them to 0.100.0 would fabricate ~99 releases for their
consumers — and that split is now enforced rather than assumed.
- The 2D graph canvas grew without bound: `drawGraph` wrote `canvas.height` from
`clientHeight` on an in-flow canvas, so each draw grew the element and the
`ResizeObserver` triggered the next. On a 1,241-node estate the canvas reached
21,604px and the operator was looking at an empty slice of it. Taking it out of
flow holds it at 680px.
- CLI help and docs advertised registry coverage as "112 to 2800+" and a
"427-server" bundled registry. Both were long stale and are now stated without
a count rather than re-pinned to a new one.
- `CONTRIBUTING.md` claimed "7,000+ monthly installs", which nothing in this
repository can substantiate. Removed rather than restated.
- **RHEL / UBI / AlmaLinux / Rocky images read zero packages.** The scanner
queried an `rpmdb.sqlite` schema those images do not ship, so every scan of
them returned a false clean. This is the most consequential fix in the release.
- The executive grade was pinned at 0% on any real estate, so eliminating half of
everything moved the headline by under one point. The curve now responds to
orders of magnitude rather than raw counts; a large failing estate is still an
F, just a readable one.
- CIS findings were titled with the state they failed to reach — a critical
finding headlined "No full-admin IAM policies attached". Titles now carry the
outcome ("CIS 1.16 not met: ..."), and `ERROR` says "not evaluated" rather than
implying failure.
- A bounded compliance or node-context read reported completeness in a word no
client understood, and one backend read unbounded while the other stopped at
50,000 nodes. Both now use the shared `complete` / `truncated` / `sampled`
vocabulary and the same budget.
- `GET /v1/graph` returned the same edge twice whenever a page had a containment
parent outside it.
- `composite_risk` meant two different things on two surfaces.
- CIS remediation advisories silently dropped the check's own `fix_cli` /
`fix_console`, degrading a copy-pasteable command to prose.
- Identity counts and the compliance matrix rendered as contradictions; an
asset-type page asked for the whole estate rather than its own type; a nested
count object rendered as `[object Object]`.
### Added
- `GET /v1/findings?kev=true` filters to known-exploited findings (and `false`
for the inverse). The executive view has always led with a KEV count; it is now
reachable from the list.
- `GET /v1/scan/{job_id}/remediation` returns the remediation plan for a scan.
- The MCP server serves the OAuth authorization server it advertises, and
advertises the reachable URL rather than its bind address.
- CI runs the CVE accuracy measurements that already existed and had never been
wired to a workflow.
- **Pre-publish drift gates.** Every stale claim in this release existed because
a number or a contract was written once and never recomputed, so each gate
derives the truth from the artifact that ships: release-version consistency,
discovered structurally across `sdks/`, compose files, k8s manifests and
Dockerfiles, where a genuinely independent version must declare itself with a
reason and silence fails closed; external-surface freshness for registry and
MCP tool/resource/prompt counts; a runtime JSON-RPC check that boots the real
MCP server over stdio and compares *names* — not counts — against the wheel's
own server card; Postgres schema parity by `pg_dump` diff; and a rendered
`docker compose config` contract asserting every service resolves, every
`depends_on` names a real service, and every read-only bind source exists.
Nothing in CI had ever run `docker compose config` while the deploy docs told
operators the stack passes it. Every gate was watched failing on real input
before it was made to pass, and six of those failures were live on `main`.
- A text equivalent for the graph canvas. Past 200 nodes / 500 edges the page
swaps React Flow for a 2D canvas or a WebGL stage, which offered a screen
reader only a widget label and a sentence about the draw budget — no node, no
relationship, no severity. Both stages now render a census by node and
relationship type, the nodes ranked by severity and connection count, and the
connections spelled out as sentences, wired to the canvas by `role="img"` and
`aria-describedby`. Every truncation is stated rather than applied silently.
- **Continue offline with a report file**, on the auth failure screens and the
login page. The offline report importer was mounted inside the auth gate, so
the feature built for "no working backend" could not render in exactly that
situation. It is opt-in, grants no data — every API call behind it fails
exactly as it would have — and lives in `sessionStorage`, so it dies with the
tab.
- `SUPPORT.md`, the one community-health file GitHub surfaces that this
repository lacked. The only support routing that existed was in
`docs/archive/`, a folder whose own README says it holds material that should
not crowd the primary documentation. It promises no new SLA: the only
committed response time remains the one `SECURITY.md` already makes.
- `GET /v1/graph` reports `completeness.boundary_edges` — the count of edges
reaching past the returned node list — so a client cannot mistake the payload
for an induced subgraph.
- `youcom_search`, an optional MCP tool returning live web and news context
alongside the local threat-intel database, contributed by @mouse-value-add.
It is the only tool that sends a query to a third party, so it is off unless
`YDC_API_KEY` is set, and the credentialed request is pinned to the You.com
origin over TLS — `YOUCOM_BASE_URL` cannot redirect the key to another host.
This brings the MCP surface to 78 tools.
- The release run owns the Docker MCP submission pin. `source.commit` names the
commit a release tag points at, so it cannot be written before the tag exists
and could not ride along with the version bump; it was a line in SUBMISSION.md
asking a human to remember, and had not moved in several releases. The release
now opens a PR carrying the tagged SHA.
### Changed
- The graph canvas opens at a readable zoom. The roll-up grid was fixed at three
columns, turning 106 containers into a portrait tower that `fitView` correctly
resolved to 0.16 — roughly 2.6px labels. Column count now follows the estate
size; the demo estate opens at 0.40.
- The collapsed estate is drawn as a topology rather than a grid of disconnected
cards.
- ATLAS is an applicability overlay, not 65 failing controls.
- TanStack Table upgraded to v9.
- A non-zero `scan` exit reads as a verdict rather than a crash. The exit code
itself is unchanged; what changed is that the CLI names the gate that fired and
links the contract, `scan --help` documents the two gates that fail closed with
no flag set — a known-malicious package, and a scan that did not complete —
rather than implying exit 1 is reachable only via `--fail-on-*`, and
`docs/FIRST_RUN.md` no longer contradicts itself between §1 and §5. The note is
suppressed under `--quiet` so CI output stays machine-clean.
- Dense lineage graphs open at a readable zoom. The roll-up grid had already been
fixed, but the raw lineage branch lays out through dagre, which stacks a wide,
shallow DAG into a portrait tower dropped into a landscape canvas — one agent
fanning out to 40 servers fit at 0.064. Ranks now reflow into sub-columns when
it materially improves the fit, preserving rank order; the same shape opens at
0.380.
- The large graph surfaces render in light mode. Both overview renderers painted
a fixed `#050505` stage with light labels regardless of theme, so on a light
page the largest element on screen was a black rectangle with its own contrast
rules. The severity chips on those surfaces had the mirror-image problem.
## [0.99.0] - 2026-08-05
A correctness release. Several defects here caused agent-bom to report *less*
than it found, or to report it under the wrong name — the failure mode that looks
clean. Read "Security" and "Fixed — wrong or missing output" before upgrading:
some numbers you may already be reporting will move.
### Fixed — wrong or missing output
- RPM 4.16+ stores its package database as `rpmdb.sqlite`, which is the default
on RHEL 9+, UBI9, Fedora 33+, Amazon Linux 2023 and Rocky 9. agent-bom could
not read it, so **every one of those images reported zero OS packages and
therefore zero OS CVEs** — a clean-looking scan that had not looked. Two
independent causes: the header parser required the 8-byte magic that legacy
BerkeleyDB records carry and the sqlite blob column does not, and the live
RPM parser shelled out to the `rpm` binary with no file fallback (dpkg and apk
both already read their database files). A UBI9 image now yields 107 packages
and 18 findings where it previously yielded none.
- `parse_rpm_packages` ignored its `root` argument while its docstring claimed
the database path was derived from it. On a host that *did* have `rpm`,
scanning a mounted snapshot reported the **host's** packages as the
snapshot's. It no longer consults the host binary for a non-live root.
### Security
- `sanitize_env_vars` wrote credential **values** in cleartext for `SSH_KEY`,
`ENCRYPTION_KEY`, `PRIVATE_KEY` and `CA_CERT` while the same report listed the
variable as an exposed credential. Four separate answers to "is this name a
credential" existed; they now share one predicate.
- `sanitize_text` redacted an AWS access key **id** and printed the 40-char
secret beside it. It also leaked a keyed value in eight shapes — quoted,
spaced, JSON, YAML, and base64 `=` padding.
- OpenSSH, ENCRYPTED and PGP private keys survived redaction entirely; only
PKCS#1/SEC1 labels matched. `OPENSSH` has been `ssh-keygen`'s default since
OpenSSH 7.8.
- The Docker Hub retention tool followed whatever `next` URL the registry
returned **with the Docker Hub bearer token attached**, so a redirected
pagination link would have carried the credential off-origin. It now refuses
any pagination URL that leaves `hub.docker.com`, bounds the loop, and fails
rather than planning deletions against a partial tag list.
- The GHCR freshness probe had the same off-origin pagination weakness and is
now pinned to `ghcr.io` on the same terms. In the same pass, the surface
freshness monitor's tool-count assertion silently switched itself off when
`--expected-tool-count` was unset: only the Glama probe derived a fallback, so
a bare `python scripts/check_surface_freshness.py` reported a listing fresh
without ever checking the count. CI passed the flag, so only humans saw the
weaker check.
- Configuration was being counted as credentials (`CERTIFICATE_PATH`,
`DB_CONNECTION_POOL_SIZE`), and real credentials were being missed
(`PGPASSWORD`, `ID_RSA`, `GOOGLE_APPLICATION_CREDENTIALS`, and every plural
form). Six copies of that predicate existed across Python, the CLI and the UI;
one server reported 25 credentials in the mesh view and 21 in the graph export.
### Breaking
- `Package.provenance_attested` is now `None` when the registry could not be
reached, rather than `False`. "We could not ask" and "we asked and the answer
was no" were indistinguishable in every machine format. The reason is on the
new `Package.provenance_status` (`verified` · `not_published` ·
`not_provenance` · `partial` · `unavailable`). Consumers treating `false` as a
negative attestation should now check `provenance_status`.
- Compliance framework statuses are now evidence-scoped — `evidence_current` ·
`review` · `action_required` — replacing the narrative `passing` · `at_risk` ·
`failing`. The old wording asserted that a mapped vulnerability established
framework failure or a regulatory breach; it does not. An explicit
non-certification boundary is now carried through CLI JSON, Markdown and the
REST responses. agent-bom supplies review evidence; it does not issue an audit,
certification, legal or regulatory verdict. The underlying finding-to-control
mappings, severities, remediation and freshness are unchanged.
### Fixed — wrong or missing output
- **The secret scanner spent its entire file budget on a shadow copy.** It
walked with a name-based skip list, and `.` sorts early, so on a checkout
containing a gitignored nested worktree the 1000-file cap was consumed before
the primary tree was reached. Scanning agent-bom itself reported **177
credential findings that were all in the shadow copy** while the real tree went
unscanned — a false negative wearing a false positive's clothes.
- Five walkers enumerated the tree themselves rather than going through the
shared traversal, and descended into linked worktrees. A worktree is a full
second copy of the project, so everything inside it was reported twice.
- **Git submodule dependencies were silently dropped from the SBOM.** Nested
checkout pruning keyed on the `.git` pointer file, which a linked worktree and
a submodule share — but a worktree is a duplicate while a submodule is vendored
code that ships. Its manifests and their CVEs disappeared.
- **Low-level MCP servers were inventoried under the dispatcher's name.** On the
official `fetch` reference server, agent-bom reported a tool called
`call_tool` instead of `fetch`.
- Django REST `@action` viewset methods, `@transaction.atomic`, `@property` and
`@staticmethod` were reported as agent tools. Agent-tool detection existed in
four places that disagreed; a test now asserts they cannot drift apart.
- **TypeScript `registerTool` matched nothing** — the current API of the official
`@modelcontextprotocol/sdk`, and the largest MCP ecosystem.
- PCI DSS was never tagged by any scan: its tagger had zero production callers
while every other framework was stamped, so a framework counted among the
advertised compliance surfaces could only ever report `not_evaluated`.
FedRAMP could not match its own catalog (namespaced tags against bare keys).
- `scan(package="flask==0.12.2")` returned a false clean over MCP: an
unrecognised launcher was forced to `npx`, so a PyPI spec was scanned as npm —
0 packages presented as a complete AI-BOM while `check()` reported 4 CVEs.
- Unrated and `none`-severity findings were published to GitHub code scanning as
**Medium**. One SARIF document carried five severity tables; four lacked
`none` and `unknown` keys and defaulted to `warning`.
- A green CIS **PASS** was printed at zero controls evaluated, in three separate
renderers, beside "0% pass (0/0 checks)".
- A "Clean" badge rendered next to a non-zero vulnerability count, because any
advisory without a CVSS vector fell out of the severity histogram while still
counting toward the total.
- `/v1/proxy/alerts` summarised the page rather than the estate, so `total_alerts`
collapsed to the page size and a filtered view described itself as the whole.
- A graph trimmed by the node budget serialised byte-identically to the whole
estate; a secret scan that hit its file cap reported `total: 0` with no partial
marker; `WITH GRANT OPTION` never left the process.
- `agent-bom connect` exited 0 on failure and truncated the control plane's
remediation guidance mid-word.
- `agent-bom scan .` produced nameless assets, because `Path(".").name` is `""`.
- CSPM findings never landed on the inventoried asset: one S3 bucket produced
three disjoint nodes, breaking finding→asset correlation on every provider.
- A partial `[gcp]` extra took down the entire inventory scan instead of
degrading: `except ImportError` cannot catch the `AttributeError` a
real-but-incomplete SDK raises.
- `environment` was populated for Azure and silently `null` for AWS and GCP —
only two of roughly twelve emission sites derived it, and the one normalizer
that did was Azure-only. GCP compounded it by labelling resources with
`labels`, which nothing read. A Snowflake account drill-down returned 1 node of
6, and the canvas silently discarded `api_gateway` nodes.
- Three of the sixteen advertised compliance surfaces could not produce evidence
at all, and the default signed bundle was unverifiable — found by walking each
README persona's journey with real commands rather than by reading code.
- The UI's `graphTruncated` banner keyed on `pagination.has_more` alone, which is
false on the last page of an already-trimmed estate, so a snapshot cut by the
load-time budget rendered with no banner.
- Integrity and provenance verdicts reached no machine-readable output at all —
console only. They now appear in JSON, CycloneDX, SPDX 2.x/3.0, SARIF, CSV and
`/v1/findings`.
### Added
- **Kotlin analyzer.** An official `modelcontextprotocol/kotlin-sdk` exists and
Kotlin MCP servers were previously invisible.
- MCP tool detection for the idioms the official SDKs actually use: Go composite
literals, TypeScript `registerTool` and low-level `setRequestHandler`, C#
attribute lists, Java `Tool.builder`, PHP `#[McpTool]`, Ruby `define_tool` and
`MCP::Tool`, Swift `Tool(name:)`, and Python low-level `list_tools`.
- A synthetic enterprise demo estate: **2,068 assets, 6,159 observations across
nine log sources, 439 findings on 407 assets, 45 controls**, with the graph
growing from 112 to 2,766 nodes. GCP audit collection is deliberately
`partial`, so incomplete evidence is shown as incomplete.
### Changed
- `/v1/demo-estate/story` went from 1,643 ms with the event loop parked for
1,648 ms to 28 ms warm; a warm read behind an eight-tenant burst went from
16,873 ms to 342 ms.
- The SQLite graph store now records planner statistics. Both indexes shared a
prefix, and SQLite documents the tie-break without statistics as arbitrary; it
had been picking the wrong one for full-snapshot reads.
- Release-tag container rebuilds now overlay the current security contract, so
refreshing `latest` from an older tag cannot reintroduce a fixed dependency.
`cryptography` is pinned to 50.0.0 with platform-specific wheel hashes, and
container SARIF paths are normalised before upload.
- The registry publish workflow verifies the public catalog's tool count instead
of treating an HTTP 202 as freshness, and fails closed on a mismatch. The
hosted MCP endpoint now requires a scoped, revocable `bearerToken` rather than
being reachable unauthenticated.
- `make preflight` now checks the product-metrics snapshot, so drift is caught
locally instead of nine minutes into CI.
### Known limitations
- The event loop is still parked for roughly 75% of a **cold** demo-story
request. The build is GIL-bound; offloading to a thread does not stop it
starving the loop, and eliminating it needs process offload or a cheaper build.
- `summary.cross_source_correlations` is 3 of 6,148 correlation groups. The demo generator gives
each synthetic observation its own trace, so most correlation groups contain a
single event. The figure is now reported explicitly rather than left implied by
the raw total.
- The 2,766-node demo graph has not been verified in a browser.
## [0.98.3] - 2026-08-02
Behaviour changes in this release move numbers you may already be reporting.
Read "Changed" before upgrading.
### Breaking
- The compliance evidence bundle's canonical signing form changed: the
`signature` key is now embedded in the document body and excluded from the
hash. External verifiers must drop that key before hashing. Bundles archived
before this release remain distinguishable by the field's absence.
- `summary.attack_pass`, `summary.attack_warn` and `summary.attack_fail` are
removed, replaced by `attack_applicable` and `attack_not_applicable`. MITRE
ATT&CK is now an applicability overlay and no longer contributes to any score.
- `observed_at` is required on `POST /v1/cloud/runtime-evidence/ingest`.
Collectors that omitted it now receive a 422.
### Fixed — evidence the product detected but did not report
- A scanned estate with nothing gradeable reported a compliance score of 100
across six surfaces — the REST aggregate, per-framework routes, the MCP tool,
the HTML report handed to auditors, the evidence bundle, and the Overview
landing page. The score is now derived from the status through one shared
scorer, so the two can no longer disagree.
- Detective controls (RA-5, CM-8, ID.RA-01, ID.RA-02, DE.CM-09, CIS-02.1/07.1/07.5)
were failed by the very scan that evidences them. They now pass on fresh
evidence and fail on stale. RA-7 and IR-5, which a scanner cannot observe, are
reported as not evaluated rather than failed.
- Undateable and future-dated scan evidence scored as fresh; both now fail closed.
- Malicious model artifacts were shown in the console but never became findings,
so every CI gate passed a pickle implant. They are now `MALICIOUS_MODEL`
findings and gate correctly.
- Prompt-injection audits of instruction-only files were skipped unless the file
also referenced a package, so `agent-bom scan .` missed a malicious `CLAUDE.md`.
- A degraded scan reported `coverage_warnings: []` and exited 0. Warnings now
reach the written report, and a partial scan exits non-zero.
- CISA KEV, EPSS and `kev_due_date` enrichment reached the report; an unavailable
KEV feed no longer reads as "not exploited".
- Both finding-ingest routes echoed the payload count as `ingested` while
payloads sharing a canonical id were correctly deduped on the way in, leaving
a client unable to explain why it pushed N and the store holds fewer. The
responses now also carry `distinct_findings` and `duplicate_payloads`, and
`/v1/findings/bulk` warns when any payload collapsed.
### Fixed — accuracy
- Version ranges now fail closed when a bound cannot be compared. Maven
qualifier ordering (`.RELEASE`, `.GA`, `SNAPSHOT`) and all three Go
pseudo-version forms are implemented, and the GHSA path evaluates the
advisory's lower bound rather than only its fix version.
- Reachability no longer claims function-level reach from a bare class token.
- The posture grade is withheld when vulnerability coverage is incomplete,
instead of grading an unscanned estate an A.
- Conflicting advisory severities resolve upward rather than by sort order.
- Effective-reach scores are clamped to their documented 0–100 range; the
highest-risk graph fixture previously emitted 142.8.
- Azure private-endpoint inventory now calls the supported
`list_by_subscription()` SDK operation; the previous `list_all()` call exists
on no stable `azure-mgmt-network` release and left inventory empty.
- Azure ML online endpoints and deployments now use the v2 `MLClient`, scoped
per workspace, instead of operations absent from the management client.
### Fixed — graph
- Roll-ups, drill-downs and every derived view inherit the snapshot's truncation
instead of reporting a bounded result as complete.
- Credential identity is scoped per server; a shared environment-variable *name*
no longer fabricates lateral-movement edges between unrelated agents.
- Bottleneck rankings are sampled deterministically and declare their sample,
rather than presenting the first 50 nodes in insertion order as fact.
- Postgres edge reconciliation is bounded, so a second scan no longer fails to
persist and silently freeze the graph.
- A finding drill-down no longer substitutes an older graph when the requested
scan has no snapshot, and focused investigations no longer show an unrelated
global path when no path matches.
### Fixed — control plane and tenancy
- Row-level security is forced on the `teams` table and on auto-created monthly
partition children, closing a cross-tenant read and delete path.
- HTTP- and scheduler-triggered scans run bound to the job's tenant.
- Compliance-hub observation partitions are provisioned a year ahead and topped
up on every deploy. The previous three-month window could not be extended at
runtime and would have broken all current-dated hub ingest permanently.
- Malformed requests return a 4xx envelope instead of a bare 500, and validation
errors no longer reflect the submitted body. Bulk finding ingest now also
rejects a missing or ambiguous JSON content type instead of accepting it.
- `agent-bom scan --inventory <path>` checks that the inventory exists before
refreshing the vulnerability database, so a typo fails immediately instead
of paying a full cold-start download.
- An unreadable or non-UTF8 manifest degrades with a named coverage warning
instead of aborting the scan with no artifact.
- `HTTP(S)_PROXY` is honoured; it was silently ignored, so pinned egress was not
actually pinned.
- SIEM delivery rides the hardened delivery client: credentials are redacted,
URLs validated, and failures retried into a dead-letter queue.
### Changed
- Compliance scores can now move off zero, because controls the scan actually
evidenced can pass. An unchanged estate may therefore report a different
number than it did on 0.98.2.
- Detective-control evidence older than 90 days fails
(`AGENT_BOM_COMPLIANCE_DETECTIVE_EVIDENCE_MAX_AGE_DAYS`).
- Scope-filtered and free-text findings queries are bounded and report
`scope_completeness` when partial, rather than walking the whole tenant.
- Read concurrency scales with the worker pool instead of a fixed limit of 8.
- The PCI DSS coverage table now reports 12 *sub-requirements* across
requirements 2, 6, 8, 11 and 12. It previously read "12 requirements / 12",
which implied complete PCI coverage.
- The test suite no longer downloads the advisory corpus unless a test opts in
with `@pytest.mark.real_vuln_db_sync`, removing accidental network work from
ordinary CI runs.
- Type-check CI installs the cloud extras and asserts that scanner SDK
operations exist, so missing provider methods cannot be hidden by
`--ignore-missing-imports`.
## [0.98.2] - 2026-07-27
### Changed
- Operator dashboards use decision-focused findings, activity, remediation,
and graph workspaces, and README onboarding uses a compact product-first
layout.
### Fixed
- Demo scans keep stable agent identities and report their bundled offline
advisory evidence without depending on an ambient vulnerability database;
optional online transitives remain subject to normal advisory lookup.
- Optional runtime enrichment remains read-only, signed runtime audit records
retain bounded activity and isolation evidence, and findings expose persisted
tenant- and scan-scoped graph reachability when assessed.
- Operator views preserve gateway token and cost evidence and distinguish
loading, empty, and unavailable state; release builds reject wheels missing
the dashboard, schemas, or CSP assets.
## [0.98.1] - 2026-07-26
### Added
- Managed-trial invitation, quota, durable execution, suspension, expiry, and
cleanup contracts with PostgreSQL tenant isolation and replay protection.
- Personal graph layouts with fit, zoom, pan, reset, auto-layout, edit/lock,
keyboard navigation, and locally persisted node positions and viewports.
### Changed
- Jobs, finding-class filters, page queries, exports, and totals now use the
server contract instead of a client-side sample window.
- Pipeline telemetry distinguishes observed, partial, and unavailable evidence;
graph summaries report factual freshness, completeness, coverage, and
provenance instead of a synthesized quality score.
- Findings, remediation, compliance, overview, and investigation use compact
task-focused workspaces with shared evidence and progressive disclosure.
- README onboarding now starts with a repository scan, keeps synthetic data
optional, and presents developer, security, GRC, platform, leadership, and
AI/MCP workflows in a compact product story.
### Fixed
- Finding-class filters apply before pagination and export, deep links use typed
page-query contracts, and missing metrics are not presented as factual zeroes.
- Focused graph paths are filtered before layout, relationship labels remain
readable at initial fit, and dragged nodes retain their positions without
changing relationship endpoints.
- Dashboard error boundaries render and report fixed safe messages instead of
exposing raw exception text; valid server-generated digests remain available
as bounded correlation references.
### Removed
- The optional Go runtime tier (`runtime/gateway-relay`, `runtime/event-collector`)
is retired. Selecting the Go relay backend also unlocked private-network
egress, bypassing the pinned transport's metadata/link-local block for a
measured ~8% p95 gain that never met the promote gate; private egress is once
again governed solely by an upstream's operator-authored
`private_network_approved` bit. The collector's queue-poll mode was never
wired and Python already covers it. The relay contract and the
`POST /v1/cloud/connections/events/ingest` route are unchanged, so any
external collector keeps working. `sdks/go` is unaffected.
## [0.98.0] - 2026-07-24
### Added
- Cloud Connections now persist authoritative account or organization scope,
full or continuous scan mode, scan-on-create behavior, and per-connection
cadence. Organization-scoped AWS connections fan inventory and CIS evidence
across member accounts through short-lived read-only assumed roles; the
scheduler and UI expose the effective scope and both cadence gates
(#4430, #4431, #4437, #4442, #4459).
- Optional Go gateway-relay and posture-event collector contracts. The gateway
remains Python-first unless an operator selects the Go relay. The collector
ships an authenticated, loopback-first forwarding proof; bounded SQS polling
and a published collector image remain explicitly unshipped (#4425, #4427,
#4429, #4459).
- Public proof surfaces now use the canonical Scan -> Centralize -> Enforce
product loop, generated architecture diagrams, and byte-for-byte SVG drift
gates in preflight (#4461).
### Changed
- Scheduled and manual connection scans share the same full inventory and CIS
path. Continuous mode drains configured provider event queues between full
scans; it does not replace the full-scan evidence contract (#4431, #4437).
- The control-plane scheduler reports the distinct "enabled but no cadence"
state. Helm keeps the event collector off unless both an image and the
required secret-backed configuration are supplied (#4461).
- README and deployment proof now lead with runnable first commands, keep the
CLI, Docker, Helm/EKS, gateway, and hosted-demo paths distinct, and avoid
treating deployment targets as shipped extensions (#4449, #4460, #4461).
### Fixed
- Postgres-hosted demo and control-plane scans persist findings through the
migrated workload-evidence schema, password-file/IAM-aware pool, tenant
context, grants, and RLS. CI now runs the Postgres contract when scheduler or
ingest paths change (#4433, #4455).
- Cloud connection scope is authoritative on every scan, scheduler failures are
tenant-bound and sanitized, and event-collector helper endpoints require a
separate inbound bearer secret before they can forward with the control-plane
credential (#4452, #4459).
- The packaged API image contains the dashboard again, and the air-gapped Helm
profile renders with its intended vulnerability database contract (#4453).
- SARIF rules retain advisory descriptions and remediation text, while the CLI
normalizes push URLs and publishes a runnable gateway first command (#4456,
#4460).
- Exposure-path UI labels no longer hide credential or tool hops and remain
legible in light and dark themes (#4454).
- Replaced the vulnerable transitive brace-expansion dependency line and
restored full-suite Ruff compliance found during the release audit (#4458,
#4462).
## [0.97.5] - 2026-07-23
### Fixed
- EKS reference install path: co-located baseline `aws_readonly_policy.json`, inline scanner IAM policies, SQLAlchemy-style Postgres URL normalization, Helm writable `/tmp` and UI public seed under restricted PSA, default Kubernetes 1.32, and `abom-eks-lab-ro-*` in scanner `connect_role_arns` (#4421).
- UI `postcss` override bumped to 8.5.12 for CVE-2026-45623 (#4420, closes #4419).
## [0.97.4] - 2026-07-22
### Fixed
- Release self-scan gate counts **critical** SARIF results via
`security-severity >= 9.0` (`scripts/count_sarif_critical.py`) instead of
raw result cardinality. Medium/low first-party `SKILL_RISK` noise no longer
aborts publish when the scan exits non-zero for coverage-limited evidence
(v0.97.3 tag publish was blocked by this false positive).
- Skill auto-discovery skips catalog/index markdown (`README.md` / `index.md`
under skills trees) and `site-docs/skills/**` (same as `docs/skills/**`), so
first-party docs no longer open medium Skill Risk code-scanning alerts.
- Release SARIF upload drops medium/low `SKILL_RISK` rows (keeps high/critical)
so GitHub Security is not flooded by first-party instruction hygiene.
## [0.97.3] - 2026-07-22
### Added
- Skill audit feeds the unified Finding stream (`FindingType.SKILL_RISK` /
`FindingSource.SKILL`) on CLI and API scans, matches extracted MCP servers
against the curated blocklist, and registers a `skill-audit` scanner driver.
- Project skill/MCP discovery covers IDE agent trees
(`.cursor/{agents,skills,commands}`, `.claude/{skills,agents,commands,rules}`,
`.codex/skills`, `.agents/{skills,agents}`) and project MCP settings
(`.claude/settings.json`, `.claude/settings.local.json`, `.windsurf/mcp.json`).
Auto-discovery skips `tests/fixtures` skill samples so intentional malicious
fixtures do not fail project/self-scan severity