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
7 changes: 7 additions & 0 deletions .github/workflows/sbom-diff-and-risk-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ jobs:
- name: Install build tooling
run: python -m pip install build

- name: Set reproducible build epoch
shell: bash
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "${GITHUB_ENV}"

- name: Build distributable artifacts
run: python -m build

- name: Normalize source distribution timestamps
run: python scripts/normalize_sdist.py --epoch "${SOURCE_DATE_EPOCH}" dist/*.tar.gz

- name: Generate SHA256 checksum manifest
shell: bash
run: |
Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ Project:
[`sbom-diff-and-risk`](tools/sbom-diff-and-risk/README.md)

Status:
Main is the `v1.0.0` final candidate. The currently published release remains
`v1.0-rc.1` until the final tag is explicitly approved and created.
Stable GitHub release: `v1.0.0`. Production PyPI publishing remains deferred.

What to review:
Deterministic SBOM/dependency diffing, JSON/Markdown/SARIF output, local policy
Expand All @@ -99,7 +98,7 @@ Useful entry points:
- [Reviewer path](tools/sbom-diff-and-risk/docs/reviewer-path.md)
- [Reviewer brief](tools/sbom-diff-and-risk/docs/reviewer-brief.md)
- [Reviewer evidence pack](tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md)
- [v1.0.0 release notes draft][release-notes-v100]
- [v1.0.0 release notes][release-notes-v100]
- [v1.0-rc.1 release notes][release-notes-v10rc1]
- [Examples](tools/sbom-diff-and-risk/examples/)

Expand Down Expand Up @@ -226,11 +225,10 @@ the review question:

## Status

- Current published flagship release candidate: `sbom-diff-and-risk` `v1.0-rc.1`
- GitHub Release assets: available for `v1.0-rc.1`
- GitHub Latest may still show `v0.9.0` because `v1.0-rc.1` is a release candidate.
- Next release policy: GitHub `v1.0.0` final, with production PyPI still deferred
- Main package metadata: `1.0.0` final candidate
- Current stable flagship release: `sbom-diff-and-risk` `v1.0.0`
- GitHub Release assets: available for `v1.0.0`
- GitHub Latest: `v1.0.0`
- Package metadata: `1.0.0`
- TestPyPI Trusted Publishing dry-run: completed
- Production PyPI publishing: intentionally deferred

Expand Down
12 changes: 7 additions & 5 deletions scripts/validate-reviewer-routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@
"not a package safety verdict engine",
"not part of the `sbom-diff-and-risk` release surface",
"why the scientific-computing background helps",
"GitHub Latest may still show `v0.9.0` because `v1.0-rc.1` is a release candidate.",
"Current stable flagship release: `sbom-diff-and-risk` `v1.0.0`",
"GitHub Latest: `v1.0.0`",
"Production PyPI publishing: intentionally deferred",
),
Path("docs/reviewer-brief.md"): (
Expand Down Expand Up @@ -266,9 +267,9 @@
"any malware verdict",
),
Path("tools/sbom-diff-and-risk/docs/release-provenance.md"): (
"publish `v1.0.0` as the stable GitHub Release contract",
"production PyPI publishing deferred",
"marks a final tag such as `v1.0.0` as GitHub Latest",
"`v1.0.0` is the stable GitHub Release contract and is marked GitHub Latest",
"Production PyPI publishing remains deferred as a separate contract",
"marks final tags such as `v1.0.0` as GitHub Latest",
"workflow artifact attestation",
),
Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): (
Expand All @@ -287,7 +288,8 @@
"runs the tool, uploads `policy.json`, and fails or passes from the policy exit code",
"not current PyPI package truth",
"not current repository reputation",
"GitHub Latest may still show `v0.9.0` because `v1.0-rc.1` is a release candidate.",
"`v1.0.0` is the stable GitHub release and GitHub Latest",
"`v1.0-rc.1` remains historical prerelease evidence",
"It does not decide whether a dependency is safe.",
),
Path("projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md"): (
Expand Down
7 changes: 3 additions & 4 deletions tools/sbom-diff-and-risk/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# sbom-diff-and-risk

Main is the `v1.0.0` Policy Evidence final candidate. The currently published
release remains `v1.0-rc.1` until the final tag is explicitly approved and
created. The selected release policy is a stable GitHub `v1.0.0` release while
production PyPI publishing remains intentionally deferred.
`v1.0.0` is the stable Policy Evidence GitHub release. GitHub Release assets
are the supported distribution surface for this version, while production
PyPI publishing remains intentionally deferred.

`sbom-diff-and-risk` is a local, deterministic CLI for comparing two SBOMs or dependency manifests and producing JSON plus Markdown reports.

Expand Down
24 changes: 18 additions & 6 deletions tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,34 @@ A stable, bounded release surface for dependency diff and local policy evidence.
- The TestPyPI Trusted Publishing dry-run remains evidence of the test
publishing path only.

## Compatibility and boundaries
## v1.0-rc.1 to v1.0.0 compatibility

- The CLI commands and flags are unchanged from `v1.0-rc.1`.
- The package version moves from PEP 440 `1.0rc1` to `1.0.0`.
- JSON and Markdown report structure remains compatible, except that the rc
evidence label `enrichment_recorded` is replaced by the more specific
`scorecard_recorded` value. Consumers matching the old string must update.
- SARIF remains a conservative subset. The final release additively records
`policy_matched`, `policy_level`, and `policy_rule_id` on direct mapped
findings, and emits `sdr.new_package` only when `new_package` matched policy.
- Production PyPI publishing remains deferred; moving from rc to final changes
the GitHub Release contract, not the PyPI publishing contract.

## Boundaries

- Default analysis remains local-file based and deterministic.
- No default network enrichment was added.
- No CVE lookup, advisory resolution, malware scanning, or package safety
verdict was added.
- Policy warnings and failures remain bounded local policy decisions.
- `enrichment_recorded` was an rc label and is replaced by the more specific
`scorecard_recorded` label in the final contract.

## Release evidence

The tag-gated GitHub Actions workflow builds the wheel and source distribution,
generates a SHA256 checksum manifest, records workflow artifact attestations,
and publishes the same built files as GitHub Release assets. Final tags are
explicitly marked as GitHub Latest; rc tags remain prereleases.
normalizes build timestamps from the tagged commit time, generates a SHA256
checksum manifest, records workflow artifact attestations, and publishes the
same built files as GitHub Release assets. Final tags are explicitly marked as
GitHub Latest; rc tags remain prereleases.

Expected assets:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ Production PyPI publishing is intentionally deferred, so consumers should not
install `sbom-diff-and-risk` from production PyPI. Use a GitHub Release asset or
a local checkout instead.

The checked-in workflow pins the selected stable contract, `v1.0.0`. Before
that final release is published, use the currently available `v1.0-rc.1`
release asset instead.
The checked-in workflow pins the stable GitHub Release contract, `v1.0.0`.

## Minimal policy workflow

Expand Down
30 changes: 18 additions & 12 deletions tools/sbom-diff-and-risk/docs/release-provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,36 @@ This page is only about the `sbom-diff-and-risk` tool's own GitHub Releases. If

## v1.0 release policy

Chosen policy: publish `v1.0.0` as the stable GitHub Release contract while
keeping production PyPI publishing deferred.
`v1.0.0` is the stable GitHub Release contract and is marked GitHub Latest.
Production PyPI publishing remains deferred as a separate contract.

For `v1.0.0`, stability applies to the CLI, report schemas, policy decision
examples, evidence-confidence labels, reviewer case, and GitHub Release asset
verification path. It does not claim production PyPI availability. Until the
final tag is explicitly approved and created, `v1.0-rc.1` remains the current
published release candidate.
verification path. It does not claim production PyPI availability.

The tag workflow marks rc tags as prereleases and not Latest. It explicitly
marks a final tag such as `v1.0.0` as GitHub Latest.
marks final tags such as `v1.0.0` as GitHub Latest.

Release assets produced by the updated workflow also include a deterministic SHA256 checksum manifest named `sbom-diff-and-risk-SHA256SUMS.txt`. The manifest is written with filenames sorted in a stable order. It is not a separate provenance system; it is a local byte-integrity check that helps reviewers confirm downloaded wheel and source distribution files match the hashes published with the same GitHub Release.

For repeat builds of the same commit, the workflow sets `SOURCE_DATE_EPOCH` to
the tagged commit timestamp. It also normalizes gzip and tar timestamps in the
source distribution before checksums and attestations are generated. This
removes build-clock variance from the wheel and source distribution while
preserving package contents.

## What the release workflow now does

For version tags matching `v*`, the `sbom-diff-and-risk-ci` workflow:

1. builds the wheel and source distribution in `build-and-attest`
2. generates `dist/sbom-diff-and-risk-SHA256SUMS.txt` with SHA256 hashes for the built `.whl` and `.tar.gz`
3. uploads the distributions and checksum manifest as the workflow artifact `sbom-diff-and-risk-dist`
4. generates a workflow artifact attestation for the built distribution files
5. downloads that same workflow artifact in `publish-release-assets`
6. publishes those exact `.whl` and `.tar.gz` files plus the checksum manifest as GitHub Release assets for the matching tag
1. sets `SOURCE_DATE_EPOCH` from the tagged commit timestamp
2. builds the wheel and source distribution in `build-and-attest`
3. normalizes source-distribution gzip and tar timestamps
4. generates `dist/sbom-diff-and-risk-SHA256SUMS.txt` with SHA256 hashes for the built `.whl` and `.tar.gz`
5. uploads the distributions and checksum manifest as the workflow artifact `sbom-diff-and-risk-dist`
6. generates a workflow artifact attestation for the built distribution files
7. downloads that same workflow artifact in `publish-release-assets`
8. publishes those exact `.whl` and `.tar.gz` files plus the checksum manifest as GitHub Release assets for the matching tag

This intentionally reuses the same workflow-built bytes for both the workflow artifact and the release asset surfaces. It does not add PyPI publishing or a separate rebuild-only release pipeline.

Expand Down
5 changes: 1 addition & 4 deletions tools/sbom-diff-and-risk/docs/reviewer-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

`sbom-diff-and-risk` is a local CLI for comparing two SBOMs or dependency manifests and producing deterministic review artifacts: JSON, Markdown, and SARIF. It is built for conservative supply-chain review, not for vulnerability scanning or package reputation scoring.

Current released version: `v1.0-rc.1` release candidate.

Main is the `v1.0.0` final candidate. The chosen release policy is a stable
GitHub final release while production PyPI remains deferred.
Current stable GitHub release: `v1.0.0`. Production PyPI remains deferred.

For the shortest ordered review route, use
[reviewer-path.md](reviewer-path.md).
Expand Down
22 changes: 11 additions & 11 deletions tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ workflow templates.

`sbom-diff-and-risk` is a local-first deterministic CLI for comparing SBOMs and dependency manifests. It is designed to produce stable review evidence for dependency changes.

Current released version: `v1.0-rc.1` release candidate.
Current stable GitHub release: `v1.0.0`.

Core identity:

Expand Down Expand Up @@ -149,22 +149,22 @@ For CI dashboard, job-summary, and local-threshold examples that consume

## Release Verification Path

Start with the GitHub Release for the version under review. For `v1.0-rc.1`,
Start with the GitHub Release for the version under review. For `v1.0.0`,
inspect the release and assets:

```powershell
gh release view v1.0-rc.1 `
gh release view v1.0.0 `
--repo stacknil/scientific-computing-toolkit `
--json tagName,name,isDraft,isPrerelease,assets,url
```

Expected release assets:

- `sbom_diff_and_risk-1.0rc1-py3-none-any.whl`
- `sbom_diff_and_risk-1.0rc1.tar.gz`
- `sbom_diff_and_risk-1.0.0-py3-none-any.whl`
- `sbom_diff_and_risk-1.0.0.tar.gz`
- `sbom-diff-and-risk-SHA256SUMS.txt`

For this rc, `isPrerelease` should be `true`.
For this stable release, `isPrerelease` should be `false`.

The checksum manifest checks local downloaded distribution bytes before or alongside provenance verification:

Expand Down Expand Up @@ -194,13 +194,13 @@ For workflow-built artifacts downloaded from a trusted workflow run, verify
artifact attestations with the signer workflow:

```powershell
gh attestation verify path/to/sbom_diff_and_risk-1.0rc1-py3-none-any.whl `
gh attestation verify path/to/sbom_diff_and_risk-1.0.0-py3-none-any.whl `
--repo stacknil/scientific-computing-toolkit `
--signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml
```

```powershell
gh attestation verify path/to/sbom_diff_and_risk-1.0rc1.tar.gz `
gh attestation verify path/to/sbom_diff_and_risk-1.0.0.tar.gz `
--repo stacknil/scientific-computing-toolkit `
--signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml
```
Expand All @@ -210,15 +210,15 @@ releases. Use them only when the repository release is immutable and GitHub has
generated release attestations:

```powershell
gh release view v1.0-rc.1 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url
gh release view v1.0.0 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url
```

If `isImmutable` is true, release verification can check the release record and
downloaded release assets:

```powershell
gh release verify v1.0-rc.1 --repo stacknil/scientific-computing-toolkit
gh release verify-asset v1.0-rc.1 path/to/sbom_diff_and_risk-1.0rc1-py3-none-any.whl --repo stacknil/scientific-computing-toolkit
gh release verify v1.0.0 --repo stacknil/scientific-computing-toolkit
gh release verify-asset v1.0.0 path/to/sbom_diff_and_risk-1.0.0-py3-none-any.whl --repo stacknil/scientific-computing-toolkit
```

If `isImmutable` is false, use the workflow artifact attestation path as the primary artifact verification story.
Expand Down
10 changes: 3 additions & 7 deletions tools/sbom-diff-and-risk/docs/reviewer-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,9 @@ Use this section only when the review question is about the released
`sbom-diff-and-risk` tool artifacts. It is not the path for judging third-party
dependency safety.

Release status note: GitHub Latest may still show `v0.9.0` because
`v1.0-rc.1` is a release candidate. Reviewers checking the current release
candidate should open the explicit `v1.0-rc.1` release page.

Main is prepared as the `v1.0.0` final candidate. The selected policy is a
stable GitHub final release with production PyPI still deferred; this is not a
claim that the final tag already exists.
Release status: `v1.0.0` is the stable GitHub release and GitHub Latest.
`v1.0-rc.1` remains historical prerelease evidence. Production PyPI publishing
is still deferred as a separate contract.

| Evidence surface | Use when | Read |
| --- | --- | --- |
Expand Down
98 changes: 98 additions & 0 deletions tools/sbom-diff-and-risk/scripts/normalize_sdist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from __future__ import annotations

import argparse
import gzip
import os
import tarfile
import tempfile
from collections.abc import Sequence
from pathlib import Path


_VOLATILE_PAX_HEADERS = {"atime", "ctime", "mtime"}


def normalize_sdist(path: Path, *, epoch: int) -> None:
if epoch < 0:
raise ValueError("epoch must be non-negative")

resolved = path.resolve()
if not resolved.is_file():
raise FileNotFoundError(f"source distribution not found: {path}")
if not resolved.name.endswith(".tar.gz"):
raise ValueError(f"source distribution must end with .tar.gz: {path}")

temporary_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
dir=resolved.parent,
prefix=f".{resolved.name}.",
suffix=".tmp",
delete=False,
) as temporary:
temporary_path = Path(temporary.name)
with tarfile.open(resolved, mode="r:gz") as source:
members = sorted(source.getmembers(), key=lambda member: member.name)
with gzip.GzipFile(
filename="",
mode="wb",
fileobj=temporary,
mtime=epoch,
) as compressed:
with tarfile.open(
fileobj=compressed,
mode="w",
format=tarfile.PAX_FORMAT,
) as destination:
for member in members:
content = source.extractfile(member) if member.isfile() else None
try:
member.mtime = epoch
member.pax_headers = {
key: value
for key, value in member.pax_headers.items()
if key not in _VOLATILE_PAX_HEADERS
}
destination.addfile(member, content)
finally:
if content is not None:
content.close()

temporary_path.replace(resolved)
except Exception:
if temporary_path is not None:
temporary_path.unlink(missing_ok=True)
raise


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Normalize gzip and tar timestamps in Python source distributions.",
)
parser.add_argument("paths", nargs="+", type=Path, help="Source distribution .tar.gz files.")
parser.add_argument(
"--epoch",
type=int,
default=None,
help="Timestamp to record. Defaults to SOURCE_DATE_EPOCH.",
)
args = parser.parse_args(argv)

epoch = args.epoch
if epoch is None:
value = os.environ.get("SOURCE_DATE_EPOCH")
if value is None:
parser.error("--epoch or SOURCE_DATE_EPOCH is required")
try:
epoch = int(value)
except ValueError:
parser.error("SOURCE_DATE_EPOCH must be an integer")

for path in args.paths:
normalize_sdist(path, epoch=epoch)
print(f"normalized reproducible sdist: {path.name}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading