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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ the right evidence path.
For the shortest boundary check before adding or reviewing new material, use
the [repository scope map](docs/repo-scope-map.md).

For the SBOM tool's risk-model boundary, use
[`docs/risk-model-boundary.md`](docs/risk-model-boundary.md). It states which
fields affect risk buckets, which fields are context only, and which claims the
model never infers.

## Supporting Diagnostics Projects

These projects are internal supporting material for reviewer depth. They are
Expand Down Expand Up @@ -165,6 +170,8 @@ they do not prove the same thing.
[`scripts/validate-reviewer-routes.py`](scripts/validate-reviewer-routes.py)
- Repository scope map:
[`docs/repo-scope-map.md`](docs/repo-scope-map.md)
- Risk model boundary:
[`docs/risk-model-boundary.md`](docs/risk-model-boundary.md)

The TestPyPI Trusted Publishing dry-run has been validated. Production PyPI
publishing is intentionally deferred.
Expand Down Expand Up @@ -205,4 +212,3 @@ the review question:
- Production PyPI publishing: intentionally deferred

[release-notes-v090]: tools/sbom-diff-and-risk/RELEASE_NOTES_v0.9.0.md

4 changes: 4 additions & 0 deletions docs/reviewer-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ workflows, but they are not part of the `sbom-diff-and-risk` release surface.
| --- | --- | --- |
| What is the repository shape? | This brief, the root [README](../README.md), and the [repository scope map](repo-scope-map.md). | You can distinguish the flagship SBOM tool from the supporting diagnostics projects. |
| What should I review for the SBOM tool? | The SBOM [reviewer path](../tools/sbom-diff-and-risk/docs/reviewer-path.md). | You have chosen the right 30-second, 5-minute, 15-minute, release, or deep-review route. |
| What does the SBOM risk model actually use? | The [risk model boundary](risk-model-boundary.md). | You can separate risk inputs from context-only fields and non-claims. |
| Can the SBOM examples be reproduced? | The SBOM [example artifact regeneration guide](../tools/sbom-diff-and-risk/docs/example-artifact-regeneration.md). | `python scripts/regenerate-example-artifacts.py --check` passes. |
| Can the released SBOM artifacts be verified? | The SBOM [verification guide](../tools/sbom-diff-and-risk/docs/verification.md). | You know whether to use checksums, release verification, or workflow artifact attestations. |
| Are the reviewer routes still valid? | The repository [reviewer route contract](../scripts/validate-reviewer-routes.py). | `python scripts/validate-reviewer-routes.py` passes. |
Expand All @@ -52,6 +53,9 @@ workflows, but they are not part of the `sbom-diff-and-risk` release surface.
intentionally deferred production PyPI decision docs.
- Scope map: `docs/repo-scope-map.md` keeps the flagship/supporting split and
repository non-claims explicit.
- Risk model boundary: `docs/risk-model-boundary.md` states which fields affect
risk classification, which fields are context only, and what the model never
infers.
- Non-goals: vulnerability scanning, CVE resolution, exploitability scoring,
package safety verdicts, hidden enrichment, or production PyPI claims.

Expand Down
174 changes: 104 additions & 70 deletions docs/risk-model-boundary.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,102 @@
# Risk model boundary
# Risk Model Boundary

This document defines the SBR-02 boundary for the SBOM risk model: which inputs
can change risk findings, which inputs are context only, and which conclusions the
tool must never infer.
This page defines the SBR-02 boundary for the
[`sbom-diff-and-risk`](../tools/sbom-diff-and-risk/README.md) risk model: which
inputs can change risk findings, which inputs are context only, and which
conclusions the tool must never infer.

The current risk model is a deterministic heuristic layer implemented in
`tools/sbom-diff-and-risk/src/sbom_diff_risk/risk.py`. It is not a vulnerability
scanner, malware detector, legal reviewer, or package trust oracle.
The model is a deterministic local heuristic layer. It is not a vulnerability
scanner, not a CVE resolver, and not a dependency safety verdict.

## Risk-affecting inputs
Implementation references:

Only the following inputs may affect emitted risk buckets.
- [`risk.py`](../tools/sbom-diff-and-risk/src/sbom_diff_risk/risk.py)
- [`diffing.py`](../tools/sbom-diff-and-risk/src/sbom_diff_risk/diffing.py)
- [`models.py`](../tools/sbom-diff-and-risk/src/sbom_diff_risk/models.py)
- [`dependency-risk-heuristics.md`](../tools/sbom-diff-and-risk/docs/dependency-risk-heuristics.md)

| Input | Risk effect | Boundary |
## Fields that affect risk classification

The risk model evaluates the added component set and the changed component set.
Removed components are reported as diff output but do not currently produce
risk findings.

Some fields affect whether a component enters the added or changed set. A
smaller set of fields directly selects the emitted risk bucket.

### Diff membership inputs

| Input or field | Affects | Boundary |
| --- | --- | --- |
| Diff category: added component | Emits `new_package`. | The component exists in the after input and not in the before input. This is a change signal only. |
| Diff category: changed component | Enables version, hygiene, and stale-evaluation findings on the after component. | Removed and unchanged components are not evaluated by `evaluate_risks`. |
| `ComponentChange.before.version` and `ComponentChange.after.version` | Emit `major_upgrade` or `version_change_unclassified`. | `major_upgrade` requires both versions to parse as strict SemVer `x.y.z` and the after major version to be higher. If both versions are present and changed but do not qualify, the finding is `version_change_unclassified`. |
| `Component.license_id` | Emits `unknown_license`. | Missing, empty, `UNKNOWN`, and `NOASSERTION` are unknown. Other license strings are not interpreted for compliance or risk severity. |
| `Component.purl` | Participates in `suspicious_source`. | If both `purl` and `source_url` are missing, source provenance is suspicious. If `purl` exists and `source_url` is missing, the source is not suspicious solely for missing `source_url`. |
| `Component.source_url` | Emits `suspicious_source` when the value is missing with no `purl`, local, non-HTTPS, or otherwise suspicious. | Suspicious examples include `http://`, `git+`, `git://`, `ssh://`, `file://`, relative paths, absolute local paths, missing URL host, IP-address hosts, `localhost`, `localdomain`, and `.local` hosts. |
| Source allowlist | Participates in `suspicious_source` for single-label hosts. | The allowlist is not a general denylist. In the current implementation, an unallowlisted host is suspicious only when an allowlist is configured and the host has no dot. |
| `stale_enrichment_enabled` | Controls `not_evaluated` for stale package checks. | When false, the model emits `not_evaluated` instead of guessing staleness. When true, the placeholder finding is suppressed; the current model still does not infer `stale_package`. |

## Context-only inputs

These fields may be useful for display, parsing, reporting, policy evaluation, or
future enrichment, but they do not currently select a risk bucket in the risk
model.

| Input | Current role |
| Component identity from `purl`, `bom_ref`, or `ecosystem` plus `name` | Whether a component is considered added, removed, or shared across inputs. | Identity is used by the diff layer before risk evaluation. It is not a package reputation signal. |
| Component signature fields `name`, `version`, `ecosystem`, `purl`, `license_id`, `supplier`, `source_url`, `bom_ref`, and `raw_type` | Whether a shared component is classified as changed. | A metadata-only change can enter risk evaluation, but bucket selection still comes only from the direct checks below. |

### Direct bucket inputs

| Input or field | Affects | Boundary |
| --- | --- | --- |
| Added component membership | `new_package` | A component that exists only in the after input receives `new_package`. This does not mean the package is unsafe. |
| `before.version` and `after.version` on changed components | `major_upgrade` or `version_change_unclassified` | Parseable SemVer major increases receive `major_upgrade`; other before/after version changes receive `version_change_unclassified`. Missing versions do not get a version-change risk bucket. |
| `license_id` on added components and changed-after components | `unknown_license` | Missing, empty, `UNKNOWN`, and `NOASSERTION` license values receive `unknown_license`. The model does not infer license compatibility. |
| `purl` and `source_url` on added components and changed-after components | `suspicious_source` | Missing source provenance, suspicious schemes, local paths, IP hosts, localhost-style hosts, and selected unqualified hosts can receive `suspicious_source`. |
| Source host allowlist passed to risk evaluation | `suspicious_source` only | The allowlist narrows source-host hygiene checks. It is not an approval list for dependency safety. |
| `stale_enrichment_enabled` | `not_evaluated` | When stale enrichment is disabled, the model records `not_evaluated` instead of guessing `stale_package`. When the flag is enabled, that offline placeholder is suppressed. |

Current risk bucket names are:

- `new_package`
- `major_upgrade`
- `version_change_unclassified`
- `unknown_license`
- `stale_package`
- `suspicious_source`
- `not_evaluated`

`stale_package` is reserved for explicit stale-package enrichment. The offline
default does not infer it; it emits `not_evaluated` for that question.

## Context-only fields

These fields can appear in reports, policy explanations, or enrichment
evidence, but they do not directly select a core risk bucket.

| Field or evidence | How to read it |
| --- | --- |
| `Component.name` | Report identity and stable finding ordering. It does not by itself imply risk. |
| `Component.ecosystem` | Normalized package context used elsewhere in the toolchain. It does not currently change risk buckets. |
| `Component.supplier` | Context only. The risk model does not infer trust, ownership, or maintainer identity from it. |
| `Component.bom_ref` | SBOM identity context only. |
| `Component.raw_type` | Parser/source context only. |
| `Component.evidence` | Parser evidence context only. |
| `Component.provenance` | Enrichment evidence for reporting or policy layers only. The risk model does not convert provenance availability, attestation availability, or enrichment errors into risk buckets. |
| `Component.scorecard` | Scorecard evidence for reporting or policy layers only. The risk model does not convert score, checks, or repository mapping into risk buckets. |
| `ComponentChange.key` | Finding identity for changed components. It does not decide the bucket. |
| `ComponentChange.classification` | Diff context only. The version values drive version-related risk findings. |
| `ReportEnrichmentMetadata` | Report context only. Network flags, candidate counts, and status counts do not change risk buckets. |

Policy evaluation is a separate layer. A policy may warn, fail, or suppress based
on findings or enrichment evidence, but that does not change what the risk model
itself is allowed to infer.

## Never infer

The risk model must never infer or imply any of the following unless a future,
explicitly documented feature adds a dedicated evidence source and tests.

- A package is vulnerable, exploitable, compromised, malicious, or safe.
- A package has or does not have CVEs, advisories, exploit chains, or reachable
vulnerable code.
- A package is trustworthy because it has a familiar name, domain, supplier,
repository, PyPI provenance record, or Scorecard result.
- Missing metadata, missing provenance, missing attestations, or enrichment
errors prove compromise.
- License compliance, legal acceptability, or redistribution permission beyond
the narrow `unknown_license` metadata check.
- Maintainer identity, project ownership, organization affiliation, or source
authenticity from package names, supplier strings, URLs, or repository mapping.
- Runtime reachability, deployment exposure, production usage, or transitive
impact.
- Package staleness when stale enrichment is disabled. The correct output is
`not_evaluated`, not an invented stale or fresh conclusion.
- Risk severity beyond the emitted bucket name and rationale.
- Network-derived facts when enrichment has not explicitly performed network
access.
| `name` | Report identity, finding ordering, and diff-signature context. It does not by itself imply risk. |
| `ecosystem` | Package context and identity fallback. It is not treated as ecosystem risk. |
| `supplier` | Report metadata and diff-signature context. It is not treated as maintainer trust. |
| `bom_ref` | Component identity fallback and report context. It is not a security proof. |
| `raw_type` | Parser/report context and diff-signature context. It is not a risk score. |
| `evidence` | Parser-specific supporting data. The current risk classifier does not derive hidden findings from it. |
| `provenance` | Optional evidence used by reporting and policy paths when explicitly enabled. It does not prove dependency safety. |
| `scorecard` | Optional OpenSSF Scorecard evidence used by reporting and policy paths when explicitly enabled. It does not prove repository trustworthiness. |
| Report `metadata`, `notes`, `summary`, and `policy_evaluation` | Output context. They explain what ran and how policy consumed findings; they do not add hidden risk buckets. |

Policy evaluation is a separate layer. A policy may pass, warn, fail, or ask
for consumer-side review based on findings or enrichment evidence, but policy
evaluation does not change what the risk model itself is allowed to infer.

## Never inferred

The model never infers:

- CVE, advisory, exploitability, or vulnerability status
- package safety or unsafe-package verdicts
- malicious intent, maintainer identity, publisher trust, or account ownership
- current PyPI package truth unless an explicit enrichment path produced
evidence for that run
- current repository reputation from mocked or checked-in example artifacts
- production PyPI release status
- climate, weather, or meteorology portfolio claims for this repository
- hidden network enrichment in the default local path
- license compatibility or legal suitability
- dependency freshness when stale-package enrichment is disabled
- risk severity beyond the emitted bucket name and rationale
- runtime reachability, deployment exposure, production usage, or transitive
impact

When the model lacks evidence for a question, it should leave the question
unanswered or emit `not_evaluated`; it should not fill the gap with a guess.

## Bucket boundaries

Expand All @@ -83,12 +110,19 @@ explicitly documented feature adds a dedicated evidence source and tests.
| `not_evaluated` | A check was intentionally not answered, currently stale-package evaluation in offline mode. | Safe, unsafe, stale, or fresh. |
| `stale_package` | Reserved for future explicit stale-package enrichment. | Must not be emitted from missing data or guesswork. |

## Reading the output

`risk_counts` is a count of local heuristic review findings. It is useful for
review triage, policy gating, and SARIF/report summaries. It is not a security
rating and should not be rewritten as a dependency safety verdict.

## Maintenance checklist

Update this document and the focused risk tests when any of these change:
Update this document and the focused docs tests when any of these change:

- `tools/sbom-diff-and-risk/src/sbom_diff_risk/risk.py`
- `tools/sbom-diff-and-risk/src/sbom_diff_risk/models.py`
- parser normalization that changes the populated `Component` fields
- [`risk.py`](../tools/sbom-diff-and-risk/src/sbom_diff_risk/risk.py)
- [`diffing.py`](../tools/sbom-diff-and-risk/src/sbom_diff_risk/diffing.py)
- [`models.py`](../tools/sbom-diff-and-risk/src/sbom_diff_risk/models.py)
- parser normalization that changes populated `Component` fields
- enrichment behavior that becomes a direct risk-model input
- policy behavior that might be confused with risk-model bucket generation
- policy behavior that might be confused with risk bucket generation
25 changes: 25 additions & 0 deletions scripts/validate-reviewer-routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Path("README.md"),
Path("docs/reviewer-brief.md"),
Path("docs/repo-scope-map.md"),
Path("docs/risk-model-boundary.md"),
Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"),
Path("projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md"),
Path("projects/precipitation-anomaly-diagnostics-lab/docs/reviewer-path.md"),
Expand Down Expand Up @@ -44,6 +45,7 @@
Path("README.md"): {
"docs/reviewer-brief.md",
"docs/repo-scope-map.md",
"docs/risk-model-boundary.md",
"tools/sbom-diff-and-risk/docs/reviewer-path.md",
"tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md",
"projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md",
Expand All @@ -53,15 +55,23 @@
Path("docs/reviewer-brief.md"): {
"README.md",
"docs/repo-scope-map.md",
"docs/risk-model-boundary.md",
"tools/sbom-diff-and-risk/docs/reviewer-path.md",
"tools/sbom-diff-and-risk/docs/example-artifact-regeneration.md",
"projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md",
"projects/precipitation-anomaly-diagnostics-lab/docs/reviewer-path.md",
"projects/python-weather-diagnostics-toolkit/docs/reviewer-path.md",
},
Path("docs/repo-scope-map.md"): set(),
Path("docs/risk-model-boundary.md"): {
"tools/sbom-diff-and-risk/docs/dependency-risk-heuristics.md",
"tools/sbom-diff-and-risk/src/sbom_diff_risk/diffing.py",
"tools/sbom-diff-and-risk/src/sbom_diff_risk/models.py",
"tools/sbom-diff-and-risk/src/sbom_diff_risk/risk.py",
},
Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): {
".github/workflows/reviewer-route-contract-ci.yml",
"docs/risk-model-boundary.md",
"scripts/validate-reviewer-routes.py",
"tools/sbom-diff-and-risk/docs/reviewer-brief.md",
"tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md",
Expand Down Expand Up @@ -146,6 +156,21 @@
"not a CVE resolver",
"not a production PyPI release claim",
),
Path("docs/risk-model-boundary.md"): (
"Fields that affect risk classification",
"Context-only fields",
"Never inferred",
"not a vulnerability scanner",
"not a CVE resolver",
"not a dependency safety verdict",
"new_package",
"major_upgrade",
"version_change_unclassified",
"unknown_license",
"stale_package",
"suspicious_source",
"not_evaluated",
),
Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): (
"Artifact evidence map",
"Reviewer route contract",
Expand Down
3 changes: 3 additions & 0 deletions tools/sbom-diff-and-risk/docs/reviewer-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Then read:

- [report-schema.md](report-schema.md)
- [policy-decision-explainability.md](policy-decision-explainability.md)
- [risk-model-boundary.md](../../../docs/risk-model-boundary.md)
- [github-code-scanning.md](github-code-scanning.md)

Look for these reviewer anchors:
Expand All @@ -71,6 +72,8 @@ Look for these reviewer anchors:
- `summary.enrichment` appears only when enrichment evidence exists
- policy findings explain `decision_reason`, `policy_rule`,
`matched_threshold`, and `observed_value`
- the risk model boundary separates risk inputs from context-only fields and
non-claims
- SARIF is intentionally narrow and does not mirror every report finding

Stop here if you need to understand the review outputs without running code.
Expand Down
Loading