Skip to content

Commit 5de1f8a

Browse files
committed
Tighten parser boundaries for deterministic inputs
1 parent 742e27e commit 5de1f8a

20 files changed

Lines changed: 713 additions & 77 deletions

tools/sbom-diff-and-risk/README.md

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ It uses conservative heuristics for change intelligence. By default it does not
99
- Normalize two local inputs into a shared component schema.
1010
- Diff components as `added`, `removed`, and `changed`.
1111
- Apply conservative, heuristic risk buckets to newly added and changed components.
12+
- Apply optional local policy enforcement over those findings.
1213
- Produce machine-friendly JSON and reviewer-friendly Markdown reports.
1314
- Stay fully local-file based by default.
1415

@@ -42,13 +43,15 @@ When a `purl` includes a version, the tool keeps the full value in `Component.pu
4243
- No reputation scoring or malware verdicts.
4344
- No hidden enrichment or implicit network access.
4445
- No web UI.
46+
- No packaged GitHub Marketplace Action.
4547

4648
## Supported Formats
4749

4850
- CycloneDX JSON
4951
- SPDX JSON
5052
- `requirements.txt`
51-
- `pyproject.toml`
53+
- `pyproject.toml` via PEP 621 `[project]` metadata
54+
- `pyproject.toml` dependency groups via PEP 735 `[dependency-groups]` with explicit selection
5255

5356
## Risk Bucket Semantics
5457

@@ -123,13 +126,26 @@ sbom-diff-risk compare \
123126
--out-md outputs/pyproject-report.md
124127
```
125128

129+
Generate reports for a specific PEP 735 dependency group:
130+
131+
```bash
132+
sbom-diff-risk compare \
133+
--before examples/pyproject_groups_before.toml \
134+
--after examples/pyproject_groups_after.toml \
135+
--format pyproject-toml \
136+
--pyproject-group dev \
137+
--out-json outputs/pyproject-groups-report.json \
138+
--out-md outputs/pyproject-groups-report.md
139+
```
140+
126141
## CLI Flags
127142

128143
- `--before path`
129144
- `--after path`
130145
- `--format auto|cyclonedx-json|spdx-json|requirements-txt|pyproject-toml`
131146
- `--before-format cyclonedx-json|spdx-json|requirements-txt|pyproject-toml`
132147
- `--after-format cyclonedx-json|spdx-json|requirements-txt|pyproject-toml`
148+
- `--pyproject-group name`
133149
- `--out-json path`
134150
- `--out-md path`
135151
- `--out-sarif path`
@@ -144,18 +160,19 @@ sbom-diff-risk compare \
144160

145161
## Examples
146162

147-
The [`examples/`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples) directory includes:
163+
The [examples/](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples) directory includes:
148164

149165
- before/after inputs for CycloneDX JSON, SPDX JSON, `requirements.txt`, and `pyproject.toml`
166+
- dependency-group examples at `examples/pyproject_groups_before.toml` and `examples/pyproject_groups_after.toml`
150167
- example policies at `examples/policy-minimal.yml` and `examples/policy-strict.yml`
151-
- a sample CycloneDX-based JSON report at [`sample-report.json`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-report.json)
152-
- a sample CycloneDX-based Markdown report at [`sample-report.md`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-report.md)
153-
- sample policy-warn reports at [`sample-policy-warn-report.json`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json) and [`sample-policy-warn-report.md`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md)
154-
- sample policy-fail reports at [`sample-policy-fail-report.json`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.json) and [`sample-policy-fail-report.md`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.md)
155-
- a sample SARIF export at [`sample-sarif.sarif`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-sarif.sarif)
156-
- requirements-based sample reports at [`sample-requirements-report.json`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-requirements-report.json) and [`sample-requirements-report.md`](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-requirements-report.md)
168+
- a sample pass JSON report at [sample-report.json](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-report.json)
169+
- a sample pass Markdown report at [sample-report.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-report.md)
170+
- sample policy-warn reports at [sample-policy-warn-report.json](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json) and [sample-policy-warn-report.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md)
171+
- sample policy-fail reports at [sample-policy-fail-report.json](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.json) and [sample-policy-fail-report.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.md)
172+
- a sample SARIF export at [sample-sarif.sarif](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-sarif.sarif)
173+
- requirements-based sample reports at [sample-requirements-report.json](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-requirements-report.json) and [sample-requirements-report.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/examples/sample-requirements-report.md)
157174

158-
## Enforcement
175+
## Enforcement Mode
159176

160177
Policy enforcement is optional and deterministic. Exit codes are stable:
161178

@@ -207,23 +224,47 @@ sbom-diff-risk compare \
207224
--out-sarif outputs/report.sarif
208225
```
209226

210-
For GitHub code scanning integration guidance and a minimal upload workflow, see [docs/github-code-scanning.md](D:/OneDrive/Code/scientific-computing-toolkit-real/tools/sbom-diff-and-risk/docs/github-code-scanning.md).
227+
For GitHub code scanning integration guidance and a minimal upload workflow, see [docs/github-code-scanning.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/docs/github-code-scanning.md).
228+
229+
## Parser Boundaries
230+
231+
Deterministic local mode intentionally supports a conservative subset of packaging syntax. The detailed matrix lives in [docs/parser-boundaries.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/docs/parser-boundaries.md).
232+
233+
### requirements.txt subset
234+
235+
| Syntax | Status | Notes |
236+
| --- | --- | --- |
237+
| Plain PEP 508 requirement entries | Supported | Names, specifiers, extras, and markers |
238+
| Comments, blank lines, line continuations | Supported | Normalized locally without installer behavior |
239+
| `-r`, `--requirement` | Unsupported | Include chains fail closed |
240+
| `-c`, `--constraint` | Unsupported | Constraint files fail closed |
241+
| Editable installs | Unsupported | `-e` and `--editable` are rejected |
242+
| Direct URL, VCS, and local path refs | Unsupported | Includes `pkg @ https://...`, `git+...`, wheels, archives, and local paths |
243+
| Index and source options | Unsupported | Includes `--index-url`, `--extra-index-url`, `--find-links`, and related flags |
244+
245+
### pyproject.toml subset
246+
247+
- default parsing supports PEP 621 `[project.dependencies]` and `[project.optional-dependencies]`
248+
- dependency groups are supported through PEP 735 `[dependency-groups]`
249+
- dependency groups must be selected explicitly with `--pyproject-group <name>`
250+
- dependency groups are not treated as aliases for `[project.optional-dependencies]`
251+
- tool-specific layouts such as Poetry, Hatch, and PDM remain out of scope in v0.2
211252

212253
## Limitations
213254

214-
- v0.1 is local-file based only.
255+
- default mode is local-file based only.
215256
- `generated_at` remains `null` to preserve deterministic report output.
216257
- `stale_package` is not resolved offline. The report emits `not_evaluated` instead.
217-
- SARIF export intentionally covers only a conservative subset of findings in v0.1.
258+
- SARIF export intentionally covers only a conservative subset of findings in v0.2.
218259
- No vulnerability database integration, CVE matching, or advisory enrichment.
219-
- `requirements.txt` support intentionally covers a conservative subset: plain PEP 508 requirement entries, comments, direct URL requirements, and line continuations.
220-
- `requirements.txt` intentionally does not support pip include/constraint directives such as `-r`, `-c`, or arbitrary install flags in v0.1.
221-
- `pyproject.toml` support intentionally covers a conservative subset: PEP 621 `[project.dependencies]` and `[project.optional-dependencies]`.
222-
- `pyproject.toml` intentionally does not support tool-specific layouts such as Poetry, Hatch, or PDM sections in v0.1.
260+
- `requirements.txt` support intentionally covers a conservative subset: plain PEP 508 requirement entries, comments, extras, markers, and line continuations.
261+
- `requirements.txt` intentionally rejects include/constraint directives, editable installs, direct URL/path refs, index/source options, and other pip-only install flags in deterministic mode.
262+
- `pyproject.toml` support intentionally covers a conservative subset: PEP 621 `[project.dependencies]`, `[project.optional-dependencies]`, and explicit PEP 735 `[dependency-groups]` selection.
263+
- `pyproject.toml` intentionally does not support tool-specific layouts such as Poetry, Hatch, or PDM sections in v0.2.
223264
- Risk buckets are heuristics, not security verdicts.
224265
- Runtime-generated `outputs/` artifacts are ignored; tracked examples live in `examples/`.
225-
- Policy files are YAML-only in v0.1 and unknown rule ids fail closed.
266+
- Policy files are YAML-only in v0.2 and unknown rule ids fail closed.
226267

227268
## Current Status
228269

229-
The project now normalizes local CycloneDX JSON, SPDX JSON, `requirements.txt`, and PEP 621 `pyproject.toml` inputs into the shared component model, diffs them deterministically, and generates stable JSON/Markdown/SARIF reports with golden tests and optional policy enforcement.
270+
The project now normalizes local CycloneDX JSON, SPDX JSON, `requirements.txt`, and conservative `pyproject.toml` inputs, including explicit PEP 735 dependency-group selection, into the shared component model, diffs them deterministically, and generates stable JSON/Markdown/SARIF reports with tests and optional policy enforcement.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Parser boundaries
2+
3+
`sbom-diff-and-risk` intentionally supports a conservative parser subset so local runs remain deterministic, auditable, and CI-friendly.
4+
5+
The project does not try to emulate a package installer. When syntax would require resolver behavior, implicit includes, index lookups, or environment-specific side effects, the parser fails closed with an explicit error.
6+
7+
## requirements.txt
8+
9+
`requirements.txt` is treated as a narrow manifest format, not as "everything pip can do in a file".
10+
11+
| Syntax | Status | Notes |
12+
| --- | --- | --- |
13+
| Plain PEP 508 names and version specifiers | Supported | Example: `requests==2.31.0` |
14+
| Extras and markers | Supported | Example: `pytest[testing]>=8.0 ; python_version >= "3.11"` |
15+
| Comments and blank lines | Supported | Stripped before parsing |
16+
| Line continuations | Supported | Continued lines are joined deterministically |
17+
| `-r`, `--requirement` | Unsupported | Include chains are rejected |
18+
| `-c`, `--constraint` | Unsupported | Constraint files are rejected |
19+
| `-e`, `--editable` | Unsupported | Editable installs are rejected |
20+
| Direct URL, VCS, or local path references | Unsupported | Includes `pkg @ https://...`, `git+...`, `file://...`, wheels, and local archives |
21+
| Index and source options | Unsupported | Includes `--index-url`, `--extra-index-url`, `--find-links`, `--trusted-host`, `--no-index` |
22+
| Other pip-only install flags | Unsupported | Includes hash flags, binary toggles, prerelease flags, and related installer controls |
23+
24+
When unsupported syntax appears, the parser raises `UnsupportedInputError` and the CLI returns exit code `2`.
25+
26+
## pyproject.toml
27+
28+
`pyproject.toml` support is also intentionally narrow:
29+
30+
| Section | Status | Notes |
31+
| --- | --- | --- |
32+
| `[project.dependencies]` | Supported | Parsed by default |
33+
| `[project.optional-dependencies]` | Supported | Parsed by default and kept distinct from dependency groups |
34+
| `[dependency-groups]` | Supported | Requires explicit `--pyproject-group <name>` selection |
35+
| `{ include-group = "name" }` inside dependency groups | Supported | Includes are resolved locally and deterministically |
36+
| Missing requested dependency group | Explicit error | Reported as `InputSelectionError` |
37+
| Poetry, Hatch, PDM, or other tool-specific dependency sections | Unsupported | Not parsed in v0.2 |
38+
39+
Dependency groups are not merged automatically with `[project.optional-dependencies]`. They solve different problems and are kept separate on purpose.
40+
41+
## Error taxonomy
42+
43+
The parser uses explicit error classes so CI logs are understandable:
44+
45+
- `MalformedInputError`: the file is syntactically malformed.
46+
- `UnsupportedInputError`: the file is valid enough to read, but deterministic mode intentionally does not support the construct.
47+
- `InputSelectionError`: the user asked for a parser selection the input cannot satisfy, such as a missing dependency group.
48+
49+
The CLI maps these parser failures to exit code `2`.

tools/sbom-diff-and-risk/docs/sbom-basics.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This project treats SBOMs as one possible source of dependency inventory data.
44

5-
For v0.1, the tool is intentionally limited to local-file parsing, normalization, diffing, and heuristic reporting.
5+
For v0.2, the tool is intentionally limited to local-file parsing, normalization, diffing, and conservative heuristic reporting.
66

77
## Supported local inputs
88

@@ -17,18 +17,20 @@ For v0.1, the tool is intentionally limited to local-file parsing, normalization
1717

1818
- supported: plain PEP 508 requirement entries
1919
- supported: comments and blank lines
20-
- supported: direct URL requirements
2120
- supported: line continuations
22-
- not supported: `-r`, `--requirement`, `-c`, `--constraint`, or arbitrary pip install flags
21+
- not supported: `-r`, `--requirement`, `-c`, `--constraint`, editable installs, direct URL/path refs, or pip index/options
2322

24-
`pyproject.toml` support is intentionally conservative in v0.1:
23+
`pyproject.toml` support is intentionally conservative in v0.2:
2524

2625
- supported: PEP 621 `[project.dependencies]`
2726
- supported: PEP 621 `[project.optional-dependencies]`
27+
- supported: PEP 735 `[dependency-groups]` with explicit `--pyproject-group` selection
2828
- not supported: Poetry, Hatch, PDM, or other tool-specific dependency sections
2929

3030
These boundaries are deliberate so the tool can stay deterministic and explicit about what it does and does not parse.
3131

32+
For the detailed supported/unsupported matrix, see [parser-boundaries.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/docs/parser-boundaries.md).
33+
3234
## Normalization goals
3335

3436
- keep one internal `Component` model
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[project]
2+
name = "grouped-project"
3+
version = "0.2.0"
4+
dependencies = [
5+
"requests==2.32.0",
6+
]
7+
8+
[project.optional-dependencies]
9+
docs = [
10+
"mkdocs>=1.6",
11+
]
12+
13+
[dependency-groups]
14+
lint = [
15+
"ruff==0.6.3",
16+
]
17+
dev = [
18+
"pytest==8.3.1",
19+
"mypy==1.11.2",
20+
{ include-group = "lint" },
21+
]
22+
test = [
23+
"pytest-cov==5.0.0",
24+
{ include-group = "dev" },
25+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[project]
2+
name = "grouped-project"
3+
version = "0.1.0"
4+
dependencies = [
5+
"requests==2.31.0",
6+
]
7+
8+
[project.optional-dependencies]
9+
docs = [
10+
"mkdocs>=1.6",
11+
]
12+
13+
[dependency-groups]
14+
lint = [
15+
"ruff==0.5.0",
16+
]
17+
dev = [
18+
"pytest==8.2.0",
19+
{ include-group = "lint" },
20+
]
21+
test = [
22+
"pytest-cov==5.0.0",
23+
{ include-group = "dev" },
24+
]

tools/sbom-diff-and-risk/src/sbom_diff_risk/cli.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .diffing import diff_components
99
from .errors import ParseError, PolicyError
1010
from .models import CompareReport, ReportComponents, ReportMetadata, ReportSummary
11-
from .normalize import SUPPORTED_FORMATS, normalize_input
11+
from .normalize import SUPPORTED_FORMATS, normalize_input_with_options
1212
from .policy_evaluator import evaluate_policy
1313
from .policy_parser import build_policy
1414
from .presentation import effective_policy_evaluation, summarize_violations_by_rule
@@ -51,6 +51,11 @@ def build_parser() -> argparse.ArgumentParser:
5151
default=None,
5252
help="Explicit format for the after input.",
5353
)
54+
compare.add_argument(
55+
"--pyproject-group",
56+
default=None,
57+
help="Select a PEP 735 [dependency-groups] group when a compared input is pyproject.toml.",
58+
)
5459
compare.add_argument("--out-json", type=Path, default=None, help="Write a JSON report to this path.")
5560
compare.add_argument("--out-md", type=Path, default=None, help="Write a Markdown report to this path.")
5661
compare.add_argument(
@@ -107,6 +112,8 @@ def main(argv: Sequence[str] | None = None) -> int:
107112

108113

109114
def run_compare(args: argparse.Namespace) -> int:
115+
pyproject_group = getattr(args, "pyproject_group", None)
116+
110117
if args.enrich_pypi:
111118
raise NotImplementedError("--enrich-pypi is reserved for a later network-enabled release.")
112119

@@ -122,8 +129,18 @@ def run_compare(args: argparse.Namespace) -> int:
122129

123130
before_declared = _resolve_declared_format(args.format, args.before_format)
124131
after_declared = _resolve_declared_format(args.format, args.after_format)
125-
before_format, before_components, before_notes = normalize_input(before_path, before_declared)
126-
after_format, after_components, after_notes = normalize_input(after_path, after_declared)
132+
before_format, before_components, before_notes = normalize_input_with_options(
133+
before_path,
134+
declared_format=before_declared,
135+
pyproject_group=pyproject_group,
136+
)
137+
after_format, after_components, after_notes = normalize_input_with_options(
138+
after_path,
139+
declared_format=after_declared,
140+
pyproject_group=pyproject_group,
141+
)
142+
if pyproject_group and before_format != "pyproject-toml" and after_format != "pyproject-toml":
143+
raise ValueError("--pyproject-group requires at least one pyproject.toml input.")
127144
policy, policy_path = build_policy(policy_path=args.policy, fail_on=args.fail_on, warn_on=args.warn_on)
128145

129146
added, removed, changed = diff_components(before_components, after_components)

tools/sbom-diff-and-risk/src/sbom_diff_risk/errors.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,17 @@ class ParseError(ValueError):
55
"""Raised when an input file cannot be parsed into normalized components."""
66

77

8+
class MalformedInputError(ParseError):
9+
"""Raised when an input is syntactically malformed."""
10+
11+
12+
class UnsupportedInputError(ParseError):
13+
"""Raised when deterministic mode rejects otherwise valid input syntax."""
14+
15+
16+
class InputSelectionError(ParseError):
17+
"""Raised when an explicit parser selection cannot be satisfied."""
18+
19+
820
class PolicyError(ValueError):
9-
"""Raised when a policy file or policy override is invalid."""
21+
"""Raised when policy parsing or evaluation inputs are invalid."""

tools/sbom-diff-and-risk/src/sbom_diff_risk/normalize.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,23 @@ def detect_format(path: Path) -> str:
4747

4848

4949
def normalize_input(path: Path, declared_format: str | None = None) -> tuple[str, list[Component], list[str]]:
50+
return normalize_input_with_options(path, declared_format=declared_format, pyproject_group=None)
51+
52+
53+
def normalize_input_with_options(
54+
path: Path,
55+
*,
56+
declared_format: str | None = None,
57+
pyproject_group: str | None = None,
58+
) -> tuple[str, list[Component], list[str]]:
5059
selected_format = declared_format or detect_format(path)
5160
if selected_format not in SUPPORTED_FORMATS:
5261
raise ValueError(
5362
f"Unsupported input format {selected_format!r}. Supported formats: {', '.join(SUPPORTED_FORMATS)}."
5463
)
5564

65+
if selected_format == "pyproject-toml":
66+
return selected_format, pyproject_toml.parse(path, dependency_group=pyproject_group), []
67+
5668
parser = _FORMAT_PARSERS[selected_format]
5769
return selected_format, parser(path), []

0 commit comments

Comments
 (0)