Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
- name: ruby
run: 'TestScan$/scan-bundler'
- name: sbom
run: 'TestScan$/scan-sbom|TestDiff/diff-sbom|TestLiteScan/lite-scan-sbom'
run: 'TestScan$/scan-sbom|TestDiff/(diff-sbom$|diff-sbom-detail-change$)|TestLiteScan/lite-scan-sbom'
- name: dotnet
run: 'TestScan$/scan-nuget'
dotnet: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-smoke-goldens.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
- name: cpp
run: 'TestScan$/scan-cpp-conan'
- name: sbom
run: 'TestScan$/scan-sbom|TestDiff/diff-sbom|TestLiteScan/lite-scan-sbom'
run: 'TestScan$/scan-sbom|TestDiff/(diff-sbom$|diff-sbom-detail-change$)|TestLiteScan/lite-scan-sbom'
- name: plugin
run: 'TestPluginWorkflows'
- name: container
Expand Down
22 changes: 22 additions & 0 deletions dev-docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,28 @@ the CLI output schema identifier remains `1.0` and the compact MCP schema
remains `mcp/1`. Protocol-v1 decoding still accepts the earlier wire field from
existing external auditor plugins.

### Decision: dependency detail changes are canonical diff results

`sdk.Compare` classifies package version changes separately from changes to an
occurrence's dependency relationship, source, or registry-matching
eligibility. The same occurrence may appear in both lists when both kinds of
change happened. Keeping these as parallel results avoids treating a move from
direct to transitive, registry to Git, or eligible to ineligible as a package
addition or removal.

Each transition keeps before and after evidence and an ordered list of changed
fields. Explicit detector relationships win. For older protocol-v1 graphs that
omit the relationship, the classifier derives direct or transitive from graph
edges and uses unknown when the graph cannot prove either. Exact and trusted
fuzzy identity matches call the same SDK classifier. Output code only projects
that result; it does not repeat the policy.

Manifest results preserve duplicate occurrences. The global JSON and MCP
views deduplicate only identical evidence and use stable ordering and bounded
MCP truncation. Diff package enrichment still uses the head-side registry, so
reporting a detail change does not replace current vulnerability or
remediation data.

### Decision: registry matching eligibility is an occurrence-level engine boundary

Detection keeps every dependency occurrence and every PURL-backed package artifact, including application roots, workspace members, local sources, and unknown relationships. Immediately before matcher selection and execution, `engine.registryMatchRequest` clones only occurrences for which `Dependency.RegistryMatchEligible()` is true and preserves edges whose endpoints are both eligible. Every built-in and external matcher therefore receives the same filtered graph, while the full `PackageRegistry` remains shared so enrichment is still deduplicated by PURL. Analysis and auditors continue with the complete original graph.
Expand Down
11 changes: 10 additions & 1 deletion dev-docs/MODELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Key helpers:
- `dep.PrimaryScope()`, `dep.HasScope(s)`, `dep.AddScope(s)` — scope helpers.
- `sdk.DetectionLicenses(dep)` / `sdk.SetDetectionLicenses(dep, licenses)` — read/write detection-time license facts stashed in `dep.Metadata`.
- `sdk.NormalizeDependencyIdentity(dep)` — canonical identity for diff matching.
- `sdk.CompareDependencyDetails(baseGraph, headGraph, before, after)` — classify occurrence-level relationship, source, and registry-matching eligibility transitions.
- `sdk.CanonicalPackageURLFromDependency(dep)` — derive the canonical PURL when the detector didn't supply one.
- `sdk.RelationshipForPath(path)` — preserve an explicit relationship or derive direct/transitive from a root-to-target path.
- `dep.RegistryMatchEligible()` — classify whether this occurrence may be sent to external registry enrichment.
Expand Down Expand Up @@ -415,7 +416,15 @@ SARIF projects the same registry-resolved findings; SBOM (SPDX/CycloneDX)
projects the `packages` enrichment onto components (licenses, vulnerabilities,
CPEs, checksums, EOL).

`bomly diff` and `bomly explain` use the same vocabulary. SARIF and SBOM output are projected from the same registry-aware helpers; see [`../docs/OUTPUT_FORMATS.md`](../docs/OUTPUT_FORMATS.md) and [`../docs/SBOM.md`](../docs/SBOM.md) for format-specific details.
`bomly diff` and `bomly explain` use the same vocabulary. Diff reports version
changes separately from occurrence detail changes. A transition carries
the before and after dependency relationship, source, and registry-matching
eligibility plus an ordered list of the fields that changed. This preserves
changes that do not alter package identity or version, including changes on
duplicate occurrences in different manifests. SARIF and SBOM output are
projected from the same registry-aware helpers; see
[`../docs/OUTPUT_FORMATS.md`](../docs/OUTPUT_FORMATS.md) and
[`../docs/SBOM.md`](../docs/SBOM.md) for format-specific details.

## Common patterns

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ flowchart TD
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.
6. **Render** — Bomly emits the result as text, JSON, SARIF, or an SBOM. See [Output formats](OUTPUT_FORMATS.md) and [SBOM formats](SBOM.md).

`bomly explain` reuses the detect and match stages, then traces the dependency paths that pull in a given package. `bomly diff` runs the pipeline against two states and reports what changed.
`bomly explain` reuses the detect and match stages, then traces the dependency paths that pull in a given package. `bomly diff` runs the pipeline against two states and reports package additions, removals, version changes, and changes to dependency relationship, source, or registry-matching eligibility.

## Configuration trust

Expand Down
14 changes: 14 additions & 0 deletions docs/OUTPUT_FORMATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ independent of a package version bump. A finding present on both sides is
`persisted`; it is not reported as one resolved finding plus one introduced
finding merely because the affected package version changed.

Dependency changes are split into separate kinds. A version change says that
the package release changed. A detail change says that the same package
occurrence changed in one of these ways:

- its relationship changed between direct, transitive, and unknown;
- its source changed, such as registry to Git or workspace;
- its eligibility for registry matching changed.

A dependency can have both a version change and a detail change in the same
diff. Structured output calls each detail-change record a `transition`. JSON
keeps the before and after evidence under
`results.dependencies.transitions` and under the matching manifest. Text,
Markdown, the interactive view, and MCP show the same classification.

## `sarif` — CI security tools

SARIF 2.1.0. Findings only. One result per (rule × package) pair. Includes:
Expand Down
24 changes: 24 additions & 0 deletions docs/schemas/diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,28 @@ Complete reference for the `bomly diff` JSON output.
| `added` | Array<[`DiffPackageChange`](#diffpackagechange)> | |
| `removed` | Array<[`DiffPackageChange`](#diffpackagechange)> | |
| `changed` | Array<[`DiffChangedPackage`](#diffchangedpackage)> | |
| `transitions` | Array<[`DiffDependencyTransition`](#diffdependencytransition)> | |

### `DiffDependencyTransition`

| Field | Type | Description |
|-------|------|-------------|
| `before` | [`DiffDependencyTransitionState`](#diffdependencytransitionstate) | |
| `after` | [`DiffDependencyTransitionState`](#diffdependencytransitionstate) | |
| `changed_fields` | Array<`string`> | |

### `DiffDependencyTransitionState`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | |
| `name` | `string` | |
| `version` | `string` | |
| `purl` | `string` | |
| `scope` | `string` | |
| `relationship` | `string` | |
| `source` | `string` | |
| `registry_eligible` | `boolean` | |

### `DiffLicenseChange`

Expand Down Expand Up @@ -169,6 +191,7 @@ Complete reference for the `bomly diff` JSON output.
| `added` | Array<[`DiffPackageChange`](#diffpackagechange)> | |
| `removed` | Array<[`DiffPackageChange`](#diffpackagechange)> | |
| `changed` | Array<[`DiffChangedPackage`](#diffchangedpackage)> | |
| `transitions` | Array<[`DiffDependencyTransition`](#diffdependencytransition)> | |

### `DiffPackageChange`

Expand All @@ -195,6 +218,7 @@ Complete reference for the `bomly diff` JSON output.
| `unchanged_manifest_count` | `integer` | |
| `added_package_count` | `integer` | |
| `changed_package_count` | `integer` | |
| `transitioned_package_count` | `integer` | |
| `removed_package_count` | `integer` | |
| `exact_match_count` | `integer` | |
| `fuzzy_match_count` | `integer` | |
Expand Down
182 changes: 182 additions & 0 deletions docs/schemas/diff.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3116,6 +3116,95 @@
"type": "object"
},
"type": "array"
},
"transitions": {
"items": {
"properties": {
"after": {
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"purl": {
"type": "string"
},
"registry_eligible": {
"type": "boolean"
},
"relationship": {
"type": "string"
},
"scope": {
"type": "string"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"id",
"name",
"relationship",
"registry_eligible"
],
"type": "object"
},
"before": {
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"purl": {
"type": "string"
},
"registry_eligible": {
"type": "boolean"
},
"relationship": {
"type": "string"
},
"scope": {
"type": "string"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"id",
"name",
"relationship",
"registry_eligible"
],
"type": "object"
},
"changed_fields": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"before",
"after",
"changed_fields"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
Expand Down Expand Up @@ -7008,6 +7097,95 @@
},
"subproject": {
"type": "string"
},
"transitions": {
"items": {
"properties": {
"after": {
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"purl": {
"type": "string"
},
"registry_eligible": {
"type": "boolean"
},
"relationship": {
"type": "string"
},
"scope": {
"type": "string"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"id",
"name",
"relationship",
"registry_eligible"
],
"type": "object"
},
"before": {
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"purl": {
"type": "string"
},
"registry_eligible": {
"type": "boolean"
},
"relationship": {
"type": "string"
},
"scope": {
"type": "string"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"id",
"name",
"relationship",
"registry_eligible"
],
"type": "object"
},
"changed_fields": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"before",
"after",
"changed_fields"
],
"type": "object"
},
"type": "array"
}
},
"required": [
Expand Down Expand Up @@ -9877,6 +10055,9 @@
"removed_package_count": {
"type": "integer"
},
"transitioned_package_count": {
"type": "integer"
},
"unchanged_manifest_count": {
"type": "integer"
},
Expand All @@ -9891,6 +10072,7 @@
"unchanged_manifest_count",
"added_package_count",
"changed_package_count",
"transitioned_package_count",
"removed_package_count",
"exact_match_count",
"fuzzy_match_count",
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/diff_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func TestRenderDiffMarkdownIncludesPatchedVersionsByDefault(t *testing.T) {
for _, want := range []string{
"# Bomly Diff Summary",
"Compared `main` to `feature`.",
"**Summary:** 1 added, 1 changed, 0 removed.",
"**Summary:** 1 added, 1 version changed, 0 detail changes, 0 removed.",
"| added | react@18.2.0 | 18.2.0 | - | unknown | - |",
"| changed | zod | 3.22.0 → 3.23.0 | - | unknown | - |",
"## Vulnerabilities",
Expand Down
Loading