Skip to content

Commit a2c7a61

Browse files
committed
Expose package remediation context
Project the package summary into scan and diff package collections and focused explain dependencies. Reuse enriched vulnerabilities for compact MCP remediation with optional audit overlays, show the summary in component details, and document the read-only contract.
1 parent 007614d commit a2c7a61

45 files changed

Lines changed: 943 additions & 82 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev-docs/ARCHITECTURE.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Stage summary:
6565
1. Runtime preparation builds the filtered registry and execution plan.
6666
2. Subproject discovery finds supported package-manager roots for the target. By default only the execution-target root is inspected; `--recursive` walks nested directories (bounded by `--max-depth`, `--exclude`, and built-in ignore rules) and plans one subproject per directory-and-package-manager pair, with workspace-expanding managers pruned below ancestors that already cover them.
6767
3. Detection resolves a dependency graph per package manager and then consolidates the per-subproject graphs into the single graph and package registry the rest of the pipeline uses. When `--scope` is set, the requested scope is part of the detector request so build-tool detectors can narrow command execution where the package manager supports it; all detector results pass through the shared SDK scope filter, and consolidation is the tail of this stage rather than a separate step.
68-
4. Matchers enrich packages with additional metadata such as licenses, EOL status, and vulnerability records.
68+
4. Matchers enrich packages with additional metadata such as licenses, EOL status, and vulnerability records. After matcher results are consolidated, the engine derives a small package remediation summary from the fix evidence already on each vulnerable package.
6969
5. Analyzers run when `--analyze` is set. They consume the matched graph and annotate `sdk.Vulnerability.Reachability` (on the PURL-keyed registry package) with status (reachable/unreachable/unknown), tier (symbol/module/package/none), and call paths. Failures degrade to `Status=unknown` rather than aborting the pipeline. See [`../docs/REACHABILITY.md`](../docs/REACHABILITY.md) for ecosystem coverage and tier semantics.
7070
6. Auditors evaluate policy against the enriched graph + registry pair and create reference-style findings (`PackageRef` + `VulnerabilityID`) when `--audit` is enabled. As the final part of that same audit stage, neutral policy-status resolvers may change only `Finding.PolicyStatus`; they never remove or rewrite finding evidence. The built-in `vulnerability`, `license`, and `package` auditors cover advisory thresholds, SPDX policy, and denied or suspicious packages respectively.
7171
7. Users combine `--enrich --audit` when they want external matcher data to feed policy evaluation in the same run.
@@ -133,6 +133,22 @@ same behavior without owning global identity policy. Baseline construction
133133
repeats the identity normalization defensively for legacy or independently
134134
constructed registries.
135135

136+
### Decision: package remediation is derived enrichment
137+
138+
After vulnerability consolidation, the engine derives
139+
`sdk.Package.Remediation` from each package's vulnerability fix evidence. The
140+
summary reports whether the evidence is complete, partial, unavailable, or
141+
unknown. A recommended version is present only when every vulnerability has a
142+
usable version and those versions can be compared.
143+
144+
This remains part of matching rather than becoming a pipeline stage.
145+
Detectors, matchers, and protocol-v1 plugins do not own the result, and the
146+
engine overwrites any incoming value. The derivation is read-only: it makes no
147+
network calls, does not inspect manifests or dependency relationships, and
148+
does not apply changes. JSON keeps the summary on the PURL-keyed package.
149+
MCP uses the same summary when it can safely name one group-level recommended
150+
version, while preserving its existing bounded relationship and policy view.
151+
136152
Pipeline plumbing: `engine.PipelineResult` exposes `Graph`, `Registry`, `Findings`, and `RiskScores`. The registry is built right after consolidation (`consolidation.BuildPackageRegistry`) and threaded through match/analyze/audit requests; output helpers (`BuildScanResponse`, `WriteSARIF`, `FindingsFromScan`, `PackagesFromGraph`) all accept `*sdk.PackageRegistry` and re-enrich their projections by resolving `PackageRef` and `VulnerabilityID`. See [`MODELS.md`](MODELS.md) for the full schema reference.
137153

138154
### Decision: finding policy-status resolution belongs inside audit

dev-docs/MODELS.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Bomly's domain model standardizes around three pipeline stages — detection, ma
77
| Stage | Type | Lives in | Identity | Purpose |
88
|------------|---------------------|--------------------------|--------------|----------------------------------------------------------|
99
| Detection | `sdk.Dependency` | per-manifest `sdk.Graph` | `Dependency.ID` (stable within manifest) | One node per dependency instance; carries scope, locations, edges |
10-
| Matching | `sdk.Package` | `sdk.PackageRegistry` | `Package.PURL` (canonical) | One artifact per unique PURL; carries licenses, vulnerabilities, scorecard, EOL |
10+
| Matching | `sdk.Package` | `sdk.PackageRegistry` | `Package.PURL` (canonical) | One artifact per unique PURL; carries licenses, vulnerabilities, remediation, scorecard, EOL |
1111
| Audit | `sdk.Finding` | `engine.PipelineResult.Findings` | `Finding.ID` + `Finding.PackageRef` + `Finding.VulnerabilityID` | Reference-style policy outcome with no inlined vuln fields |
1212

1313
Vulnerabilities themselves are OSV-aligned `sdk.Vulnerability` records owned by the registry; analyzers annotate them in place with reachability.
@@ -123,6 +123,7 @@ type Package struct {
123123
Digests []Digest
124124
Licenses []PackageLicense
125125
Vulnerabilities []Vulnerability // OSV-aligned
126+
Remediation *PackageRemediation // derived from vulnerability fix evidence
126127
Scorecard *PackageScorecard
127128
EOL *PackageEOL
128129
Copyright string
@@ -142,6 +143,23 @@ Registry API (`sdk/registry.go`):
142143

143144
Built by `consolidation.BuildPackageRegistry(consolidated)` right after the consolidation stage; threaded through match/analyze/audit and into the output layer via `PipelineResult.Registry`.
144145

146+
`Package.Remediation` is a small summary derived by the engine after all
147+
matcher results have been consolidated. It is absent when a package has no
148+
vulnerabilities:
149+
150+
- `complete` means every vulnerability has usable fix evidence and
151+
`RecommendedVersion` is the lowest package version known to address all of
152+
them.
153+
- `partial` means some fix evidence exists but it does not support one complete
154+
recommendation.
155+
- `unavailable` means every vulnerability explicitly reports no fix or
156+
won't-fix.
157+
- `unknown` means evidence is missing or contradictory.
158+
159+
The summary is derived data, not matcher policy. The engine replaces any
160+
incoming value after matching. Derivation does not inspect manifests or the
161+
dependency graph, make network calls, run commands, or write files.
162+
145163
## `sdk.Vulnerability` — OSV-aligned
146164

147165
```go

docs/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ flowchart TD
3333

3434
1. **Discover** — Bomly inspects the target root and finds every supported package-manager root (a `go.mod`, a `package-lock.json`, a `pom.xml`, and so on). With `--recursive` it also walks nested directories, discovering independent subprojects in a monorepo while workspace-aware managers (npm workspaces, Maven reactors, …) keep expanding their own modules from the root. See [Scan targets](SCAN_TARGETS.md#recursive-discovery----recursive).
3535
2. **Detect** — For each root, a [detector](DETECTORS.md) reads the lockfile, manifest, or SBOM and resolves a dependency graph. Per-subproject graphs are then *consolidated* into one graph and one deduplicated package set for the rest of the run. `--scope` narrows the graph to runtime or development dependencies here.
36-
3. **Match** — When you pass `--enrich`, [matchers](MATCHERS.md) add data to published registry packages: known vulnerabilities, licenses, end-of-life status, and project health scores. Project roots, workspace members, and local/file/Git/URL artifacts remain in the graph and reports but are not queried as if they were registry releases.
36+
3. **Match** — When you pass `--enrich`, [matchers](MATCHERS.md) add data to published registry packages: known vulnerabilities, licenses, end-of-life status, and project health scores. Bomly then summarizes the available vulnerability fix evidence on each affected package. Project roots, workspace members, and local/file/Git/URL artifacts remain in the graph and reports but are not queried as if they were registry releases.
3737
4. **Analyze** — When you pass `--analyze`, [reachability](REACHABILITY.md) analysis runs on top of the matched data to flag whether a vulnerability is actually reachable from your code.
3838
5. **Audit** — When you pass `--audit`, [auditors](AUDITORS.md) evaluate policy (severity thresholds, license rules, denied packages) against the enriched data and produce findings. As part of this same step, configured policy-status rules may mark a finding non-gating without removing it. Combine `--enrich --audit` to gate on fresh external data in one run.
3939
6. **Render** — Bomly emits the result as text, JSON, SARIF, or an SBOM. See [Output formats](OUTPUT_FORMATS.md) and [SBOM formats](SBOM.md).
@@ -45,7 +45,7 @@ flowchart TD
4545
Bomly keeps three kinds of data separate, which is why the same fact never appears twice in the output:
4646

4747
- **Dependencies** are detection-time graph nodes. Each is one instance of a dependency in a manifest, carrying its scope, where it was found, and its edges to other dependencies. A dependency points at a package by its PURL but does not itself hold license or vulnerability data.
48-
- **Packages** are deduplicated artifacts keyed by [PURL](GLOSSARY.md). There is one package per unique PURL across the whole scan, and it owns the enrichment: licenses, vulnerabilities, scorecard, and EOL. If 50 dependencies all reference `react@18.2.0`, they share one package — and one set of CVEs.
48+
- **Packages** are deduplicated artifacts keyed by [PURL](GLOSSARY.md). There is one package per unique PURL across the whole scan, and it owns the enrichment: licenses, vulnerabilities, remediation status, scorecard, and EOL. If 50 dependencies all reference `react@18.2.0`, they share one package — and one set of CVEs.
4949
- **Findings** are reference-style audit results. A finding names a policy outcome and points back at a package (and, for a vulnerability, at a specific advisory) rather than copying that data inline.
5050

5151
```mermaid

docs/MATCHERS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ bomly scan --enrich --matchers +clearlydefined-license-matcher
4242

4343
External matchers are published as plugins — see the [ClearlyDefined License Matcher](https://github.com/bomly-dev/bomly-plugin-clearlydefined-matcher) and [EOL Lifecycle Matcher](https://github.com/bomly-dev/bomly-plugin-eol-matcher) for worked examples, and [PLUGINS.md](PLUGINS.md) to install and enable them.
4444

45+
## Package remediation summary
46+
47+
After all vulnerability matcher results are combined, Bomly summarizes the fix
48+
evidence for each affected package. The summary reports whether a complete
49+
recommended version is available, only part of the needed evidence is
50+
available, every advisory reports no fix, or the evidence is unclear.
51+
52+
This is read-only enrichment data. It does not inspect or change manifests and
53+
does not run a package manager. JSON places the summary on `packages[]`; the
54+
interactive view shows it in a selected component's details.
55+
4556
## Network endpoints
4657

4758
When `--enrich` is set, Bomly may call:

docs/MCP.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ Bomly registers four MCP tools.
9595

9696
| Tool | Use it for | Required arguments | Optional arguments | Enrichment behavior |
9797
| --- | --- | --- | --- | --- |
98-
| `bomly_scan` | Scan a path, Git URL, container image, or SBOM and get a compact, remediation-grouped summary of what needs fixing. | None | `path`, `image`, `url`, `ref`, `enrich`, `audit`, `analyze`, policy arguments, `ecosystems`, `scope`, `recursive`, `max_depth`, `exclude` | Calls external matchers only when `enrich` is true. Pass `enrich` and `audit` together for a security review. |
99-
| `bomly_explain` | Drill into one package: dependency paths, full advisory detail, and concrete fix context. | `package` | `path`, `enrich`, `audit`, `analyze`, policy arguments, `recursive`, `max_depth`, `exclude` | Calls external matchers only when `enrich` is true. |
100-
| `bomly_diff` | Branch-aware security delta between two Git refs, container tags/digests, or SBOM files: what head fixes, introduces, and leaves open after merge. | `base`, `head` | `path`, `image`, `sbom`, `enrich`, `audit`, `analyze`, policy arguments, `recursive`, `max_depth`, `exclude` | Calls external matchers only when `enrich` is true. |
98+
| `bomly_scan` | Scan a path, Git URL, container image, or SBOM and return a compact dependency summary. | None | `path`, `image`, `url`, `ref`, `enrich`, `audit`, `analyze`, policy arguments, `ecosystems`, `scope`, `recursive`, `max_depth`, `exclude` | `enrich` adds vulnerabilities and remediation. `audit` adds policy results. |
99+
| `bomly_explain` | Show why one package is present, with full advisory details when enriched. | `package` | `path`, `enrich`, `audit`, `analyze`, policy arguments, `recursive`, `max_depth`, `exclude` | `enrich` adds vulnerabilities and remediation. `audit` adds policy results. |
100+
| `bomly_diff` | Compare dependencies between Git refs, container images, or SBOM files. | `base`, `head` | `path`, `image`, `sbom`, `enrich`, `audit`, `analyze`, policy arguments, `recursive`, `max_depth`, `exclude` | `enrich` adds head-side remediation. `audit` adds the policy finding delta. |
101101
| `bomly_plugins` | List built-in and installed external plugins with their enabled state. | None | None | Does not enrich package data. |
102102

103103
MCP coverage matches Bomly CLI coverage: `bomly_scan`, `bomly_explain`, and `bomly_diff` use the same detector, matcher, auditor, and analyzer registry as the CLI. See [Support Matrix](SUPPORT_MATRIX.md) for the current ecosystem and package-manager list.
@@ -116,7 +116,7 @@ MCP tool results land in an agent's context window, so they use a compact respon
116116
`bomly_scan` returns:
117117

118118
- **`summary`** — manifest/package counts, `subprojects` and `modules` counts for scans that span nested projects or workspace/reactor members (omitted for flat scans), vulnerable vs clean packages, findings by severity, and whether enrich/audit ran. Clean packages are counted, never listed. The full hierarchy is derived from `manifests[].subproject` + `path` in the complete CLI JSON document.
119-
- **`remediations`** — ranked groups, each one concrete change and every finding it closes: the direct dependency to change (`target_package`, full identity with org/scope and PURL), the manifest to edit, the version to move to, and an `action` (`direct-bump`, `transitive-override`, `lockfile-refresh`, `no-fix-upstream`, `policy-review`). Transitive cases carry package-manager-specific `override_advice` (npm `overrides`, pnpm `pnpm.overrides` / `pnpm-workspace.yaml`, yarn `resolutions`, Maven `dependencyManagement`, Gradle constraints, `go get` + `go mod tidy`, and so on). Groups are ranked by known-exploited (KEV) first, then severity, EPSS, and fixability.
119+
- **`remediations`** — ranked groups built from enriched vulnerabilities. Each group identifies the affected package, its relationship to the project, and the manifest when known. `recommended_version` appears only when one affected package has a complete package-level recommendation. When audit also runs, matching policy status is added without hiding enriched vulnerabilities that did not produce a finding.
120120
- **`informational`** — warning and policy-only findings, separated from actionable work.
121121
- **`diagnostics`** — pipeline warnings (detector fallbacks, matcher failures) so partial results explain themselves.
122122
- **`truncation`** — explicit counters whenever a cap cut anything; nothing is dropped silently.
@@ -125,10 +125,14 @@ Each finding carries advisory identifiers (`vuln_id`, aliases), severity, classi
125125

126126
For the omitted detail:
127127

128-
- **One package**: call `bomly_explain` with `enrich` (and `audit` for remediation context). Its response carries the package's full advisory records — descriptions, references, CVSS, affected ranges — bounded to that package.
128+
- **One package**: call `bomly_explain` with `enrich`. Its response carries the package's full advisory records and remediation context, bounded to that package. Add `audit` when you also need policy results.
129129
- **The complete document**: run the CLI (`bomly scan --format json -o <file>`). The MCP server intentionally never returns the full scan document; it does not fit tool-result limits on real projects.
130130

131-
`bomly_diff` returns the same finding shape bucketed into a `security_delta``introduced` (new on head), `resolved` (closed when head merges), `persisted` (still open after merge) — keyed by advisory id independent of version bumps, plus remediation groups for everything still open. By default `base` and `head` are Git refs; set `image` to diff two container tags/digests, or `sbom: true` to diff two SBOM file paths (SPDX or CycloneDX), mirroring the CLI's `--image` and `--sbom` diff modes.
131+
With `enrich`, `bomly_diff` returns remediation groups for vulnerable packages
132+
on the head side. With `audit`, it also returns findings bucketed into a
133+
`security_delta`: `introduced`, `resolved`, and `persisted`. By default `base`
134+
and `head` are Git refs; set `image` to compare two container tags or digests,
135+
or `sbom: true` to compare two SBOM files.
132136

133137
## Example Prompts
134138

0 commit comments

Comments
 (0)