From bcc807c3b3b54a222f60fdc3923de7cdd0bbecc4 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 08:50:49 +0300 Subject: [PATCH 01/30] chore(af01): add deterministic dependency inventory --- .github/scripts/summarize_cargo_metadata.py | 112 ++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .github/scripts/summarize_cargo_metadata.py diff --git a/.github/scripts/summarize_cargo_metadata.py b/.github/scripts/summarize_cargo_metadata.py new file mode 100644 index 00000000..cdc253ef --- /dev/null +++ b/.github/scripts/summarize_cargo_metadata.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Summarize exact Cargo metadata for AF-01 T020 dependency-policy inspection.""" + +from __future__ import annotations + +import json +import sys +from collections import defaultdict + +CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index" + + +def main() -> int: + try: + metadata = json.load(sys.stdin) + except (json.JSONDecodeError, OSError) as error: + print(json.dumps({"error": f"invalid cargo metadata: {error}", "ok": False}, sort_keys=True)) + return 1 + + packages = metadata.get("packages") + workspace_members = metadata.get("workspace_members") + if not isinstance(packages, list) or not isinstance(workspace_members, list): + print(json.dumps({"error": "cargo metadata missing package/workspace arrays", "ok": False}, sort_keys=True)) + return 1 + + workspace_ids = set(workspace_members) + inventory: list[dict[str, object]] = [] + versions_by_name: dict[str, set[str]] = defaultdict(set) + license_packages: dict[str, list[str]] = defaultdict(list) + source_classes: dict[str, int] = defaultdict(int) + unknown_license: list[str] = [] + non_crates_io: list[str] = [] + + for package in packages: + if not isinstance(package, dict): + print(json.dumps({"error": "cargo metadata contains non-object package", "ok": False}, sort_keys=True)) + return 1 + package_id = package.get("id") + name = package.get("name") + version = package.get("version") + source = package.get("source") + license_expr = package.get("license") + if not all(isinstance(value, str) for value in (package_id, name, version)): + print(json.dumps({"error": "package identity is incomplete", "ok": False}, sort_keys=True)) + return 1 + if source is not None and not isinstance(source, str): + print(json.dumps({"error": f"invalid source for {name} {version}", "ok": False}, sort_keys=True)) + return 1 + if license_expr is not None and not isinstance(license_expr, str): + print(json.dumps({"error": f"invalid license for {name} {version}", "ok": False}, sort_keys=True)) + return 1 + + identity = f"{name}@{version}" + workspace = package_id in workspace_ids + source_class = "workspace" if workspace else ("crates.io" if source == CRATES_IO_SOURCE else "other") + source_classes[source_class] += 1 + versions_by_name[name].add(version) + if not workspace: + if license_expr: + license_packages[license_expr].append(identity) + else: + unknown_license.append(identity) + if source != CRATES_IO_SOURCE: + non_crates_io.append(f"{identity}:{source or ''}") + + dependencies = package.get("dependencies", []) + direct_dependency_names = sorted( + { + dependency.get("name") + for dependency in dependencies + if isinstance(dependency, dict) and isinstance(dependency.get("name"), str) + } + ) + inventory.append( + { + "dependencies": direct_dependency_names, + "license": license_expr, + "name": name, + "source": source, + "source_class": source_class, + "version": version, + "workspace": workspace, + } + ) + + inventory.sort(key=lambda item: (str(item["name"]), str(item["version"]), str(item["source"]))) + duplicates = { + name: sorted(versions) + for name, versions in sorted(versions_by_name.items()) + if len(versions) > 1 + } + licenses = { + expression: sorted(identities) + for expression, identities in sorted(license_packages.items()) + } + result = { + "duplicates": duplicates, + "licenses": licenses, + "non_crates_io": sorted(non_crates_io), + "ok": not unknown_license, + "package_count": len(inventory), + "packages": inventory, + "schema": 1, + "source_classes": dict(sorted(source_classes.items())), + "unknown_license": sorted(unknown_license), + } + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["ok"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) From 90705aa9ea403d955ea775b4dd5587ed722a0b9b Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 08:51:28 +0300 Subject: [PATCH 02/30] ci(af01): inspect exact dependency metadata --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ebfc227..ca6e1500 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,11 @@ jobs: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 + - uses: dtolnay/rust-toolchain@032958afbdc797a916606bdc6ffcbf961 # 1.97.1 with: components: rustfmt, clippy + - name: AF-01 T020 exact dependency metadata inventory + run: cargo metadata --locked --format-version 1 | python3 .github/scripts/summarize_cargo_metadata.py - name: AF-01 workflow trust audit tests run: python3 -m unittest discover -s .github/scripts -p 'test_audit_workflow_trust*.py' - name: AF-01 repository workflow trust audit From 8a92a543ea33d9355171be14f96247c02234fbfe Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 08:51:57 +0300 Subject: [PATCH 03/30] fix(af01): restore pinned Rust toolchain ref --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca6e1500..41f33c1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@032958afbdc797a916606bdc6ffcbf961 # 1.97.1 + - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 with: components: rustfmt, clippy - name: AF-01 T020 exact dependency metadata inventory From 025e17996e1d7e13767917841ea0c90e3d9d2492 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 08:59:12 +0300 Subject: [PATCH 04/30] ci(af01): add dependency security evidence workflow --- .github/workflows/af01-security.yml | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/af01-security.yml diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml new file mode 100644 index 00000000..828e4f7f --- /dev/null +++ b/.github/workflows/af01-security.yml @@ -0,0 +1,34 @@ +name: af01-security + +on: + pull_request: + push: + branches: + - feat/af01-stack-b-dependency-security + +permissions: + contents: read + +jobs: + dependency-inventory: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 + - name: Generate exact locked dependency inventory + run: | + set -euo pipefail + cargo metadata --locked --format-version 1 > /tmp/commandf-cargo-metadata.json + python3 .github/scripts/summarize_cargo_metadata.py \ + < /tmp/commandf-cargo-metadata.json \ + | tee af01-t020-dependency-inventory.json + - name: Upload exact dependency inventory + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: af01-t020-dependency-inventory + path: af01-t020-dependency-inventory.json + if-no-files-found: error + retention-days: 7 From 6f0fa6f2a344e7c6f9adbade2851b6aed3c4d5f9 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 08:59:30 +0300 Subject: [PATCH 05/30] ci(af01): authorize security evidence workflow --- .github/workflow-trust-policy.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflow-trust-policy.json b/.github/workflow-trust-policy.json index be17b51f..3d070ada 100644 --- a/.github/workflow-trust-policy.json +++ b/.github/workflow-trust-policy.json @@ -22,6 +22,17 @@ "require_external_uses_full_sha": "External Actions and reusable workflows must be bound to an immutable 40-hex commit so mutable tags or branches cannot change executed code without a repository diff." }, "workflows": { + ".github/workflows/af01-security.yml": { + "jobs": { + "dependency-inventory": { + "permissions": { + "contents": "read" + }, + "runner": "ubuntu-24.04", + "timeout_minutes": 10 + } + } + }, ".github/workflows/cf06-oracle.yml": { "jobs": { "oracle-changed-profile": { From 4322e99a869f1e3aa4b2eecdb37cfc24bcef25df Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 09:01:26 +0300 Subject: [PATCH 06/30] docs(af01): record dependency policy intent --- .../stack-b-dependency-inventory.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md diff --git a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md new file mode 100644 index 00000000..12366767 --- /dev/null +++ b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md @@ -0,0 +1,165 @@ +# AF-01 Stack B Dependency Inventory and Policy Intent + +Status: `T020_COMPLETE_POLICY_INPUT` + +This document records the exact dependency/license/source evidence inspected before creating `deny.toml`. It is the policy input for AF-01 T021; it is not itself a scanner PASS. + +## Canonical input + +Stack A canonical base: + +```text +main: 48587578e2d9167ac1c96b51c9942edb2aa74d8c +tree: 4f2ccef845321a7abba8ce5388e78281a1514436 +Cargo.lock blob: 69ba1936596a1f3acfef2908fe659c6dc6fe474a +``` + +Inspection command: + +```text +cargo metadata --locked --format-version 1 | python3 .github/scripts/summarize_cargo_metadata.py +``` + +Exact evidence run: + +```text +head: 6f0fa6f2a344e7c6f9adbade2851b6aed3c4d5f9 +workflow: af01-security +run: 33044282731 +job: dependency-inventory +artifact: 9635015658 +artifact name: af01-t020-dependency-inventory +artifact digest: sha256:370637ff98963b3804780bec3439275ab7c1ec80c63f123722d493fe2cd54247 +``` + +The artifact was generated from the locked graph and uploaded by a full-SHA-pinned `actions/upload-artifact` action. + +## Exact graph summary + +```text +packages total: 133 +workspace packages: 2 +crates.io packages: 131 +other registry/git packages: 0 +packages with unknown license metadata: 0 +``` + +Workspace crates: + +- `commandf` +- `commandf-pkg` + +The current workspace direct dependency surface is declared in the root/member manifests and includes `clap`, `flate2`, `semver`, `serde`, `serde_json`, `sha2`, `tar`, `thiserror`, `ureq`, and `tempfile`, plus the local path dependency from `commandf` to `commandf-pkg`. + +## Observed license expressions + +The exact locked graph contains the following distinct dependency license expressions: + +- `(MIT OR Apache-2.0) AND Unicode-3.0` +- `0BSD OR MIT OR Apache-2.0` +- `Apache-2.0 AND ISC` +- `Apache-2.0 OR ISC OR MIT` +- `Apache-2.0 OR MIT` +- `Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT` +- `BSD-3-Clause` +- `CDLA-Permissive-2.0` +- `ISC` +- `MIT` +- `MIT OR Apache-2.0` +- `MIT OR Apache-2.0 OR LGPL-2.1-or-later` +- `MIT OR Zlib OR Apache-2.0` +- `MIT/Apache-2.0` +- `Unicode-3.0` +- `Unlicense OR MIT` + +The SPDX license atoms required by the observed graph are therefore narrowly bounded to: + +```text +0BSD +Apache-2.0 +Apache-2.0 WITH LLVM-exception +BSD-3-Clause +CDLA-Permissive-2.0 +ISC +LGPL-2.1-or-later +MIT +Unicode-3.0 +Unlicense +Zlib +``` + +No broader license family, wildcard, or blanket approval is authorized by this inventory. + +### Legacy metadata exception boundary + +Two exact packages report the legacy non-SPDX expression `MIT/Apache-2.0` in Cargo metadata: + +- `filetime@0.2.29` +- `version_check@0.9.5` + +T021 must not convert this into a global acceptance rule. If cargo-deny 0.20.2 does not normalize the legacy expression itself, any remediation must be package-scoped and evidence-backed (for example, an exact package exception or license clarification tied to upstream license material). The policy must remain narrower than accepting arbitrary malformed license expressions. + +## Duplicate/version inventory + +The locked graph contains exactly three package names at multiple versions: + +```text +getrandom: 0.2.17, 0.4.3 +syn: 2.0.119, 3.0.3 +windows-sys: 0.52.0, 0.61.2 +``` + +These are current transitive graph facts, not silently approved permanent exceptions. + +T021 policy intent: + +- keep duplicate versions visible and machine-diagnosable; +- use `multiple-versions = "warn"` initially rather than creating opaque skip lists for the current graph; +- set wildcard dependency requirements to fail closed; +- do not add `skip` or `skip-tree` entries unless a later exact finding proves a narrow necessity and records a reason/revisit condition. + +## Source policy intent + +Observed source authority is only: + +- local workspace/path packages; and +- crates.io via `https://github.com/rust-lang/crates.io-index`. + +T021 must therefore enforce: + +```text +unknown registry: deny +unknown git source: deny +allowed registry: crates.io only +allowed git sources: none +``` + +A future git dependency, alternate registry, or additional source authority must require a repository diff and explicit policy review; it must not be admitted by a wildcard source rule. + +## Advisory policy intent + +At the policy layer: + +- RustSec advisories are not blanket-ignored; +- `advisories.ignore` starts empty; +- current/future advisory waivers are governed by T024 rather than handwritten anonymous ignore entries; +- yanked or vulnerable dependency evidence must remain visible to the scanner gates; +- scanner transport/tooling failures must be CI failures, not PASS-equivalent outcomes. + +T023 remains responsible for an independent cargo-audit view so cargo-deny is not the only advisory signal. + +## License policy intent + +T021 should allow only the observed SPDX atoms listed above and use cargo-deny 0.20.2's exact configuration semantics. The policy must: + +- avoid wildcard license approval; +- avoid ignoring private/workspace packages as a shortcut unless repository semantics require it; +- preserve `confidence-threshold = 0.8` unless exact scanner evidence justifies a narrower change; +- scope any legacy-license remediation to the exact affected package/version; +- fail on any new dependency whose license cannot be matched to the checked-in policy. + +## T020 decision + +`T020 = COMPLETE` + +The current graph, source authority, license surface, and duplicate families are now documented from exact locked evidence. T021 may create `deny.toml` from this inventory. No cargo-deny, cargo-audit, advisory-waiver, or zizmor PASS is claimed by this document. From 07a7000d622990fe1d58614f08d8ddea5f90343e Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 09:01:48 +0300 Subject: [PATCH 07/30] ci(af01): define dependency trust policy --- deny.toml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 deny.toml diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..943393f4 --- /dev/null +++ b/deny.toml @@ -0,0 +1,55 @@ +# AF-01 T021 dependency trust policy. +# +# Derived from specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md. +# This policy intentionally starts without advisory, source, duplicate, or license waivers. + +[advisories] +ignore = [] + +[licenses] +allow = [ + "0BSD", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-3-Clause", + "CDLA-Permissive-2.0", + "ISC", + "LGPL-2.1-or-later", + "MIT", + "Unicode-3.0", + "Unlicense", + "Zlib", +] +confidence-threshold = 0.8 +exceptions = [] + +[licenses.private] +# commandF workspace crates are unpublished (`publish = false`) and are not +# third-party dependency license authority. Dependency licenses remain checked. +ignore = true +registries = [] + +[bans] +# The T020 graph contains three transitive duplicate families. Keep them visible +# without turning the current lockfile into an opaque skip-list contract. +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" +allow = [] +allow-workspace = false +deny = [] +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] + +[sources.allow-org] +github = [] +gitlab = [] +bitbucket = [] From 288fa36ddac7e23cbf9c57db6fb6abdee78bfe85 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 09:02:29 +0300 Subject: [PATCH 08/30] ci(af01): enforce cargo-deny policy --- .github/workflows/af01-security.yml | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index 828e4f7f..e626d247 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -32,3 +32,54 @@ jobs: path: af01-t020-dependency-inventory.json if-no-files-found: error retention-days: 7 + + cargo-deny: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 + with: + persist-credentials: false + - name: Record cargo-deny proof identity + run: | + set -euo pipefail + python3 - <<'PY' + import hashlib + import json + import os + from pathlib import Path + + def sha256(path: str) -> str: + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + proof = { + "action_commit": "3c6349835b2b7b196a839186cb8b78e02f7b5f25", + "action_release": "v2.1.1", + "cargo_deny_version": "0.20.2", + "cargo_lock_sha256": sha256("Cargo.lock"), + "checks": ["advisories", "bans", "licenses", "sources"], + "command": "cargo deny --all-features check advisories bans licenses sources", + "deny_toml_sha256": sha256("deny.toml"), + "head_sha": os.environ["GITHUB_SHA"], + "schema": 1, + } + Path("af01-cargo-deny-proof.json").write_text( + json.dumps(proof, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(proof, indent=2, sort_keys=True)) + PY + - name: Enforce cargo-deny dependency policy + uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 / cargo-deny 0.20.2 + with: + command: check + arguments: --all-features + command-arguments: advisories bans licenses sources + log-level: warn + - name: Upload cargo-deny proof identity + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: af01-cargo-deny-proof + path: af01-cargo-deny-proof.json + if-no-files-found: error + retention-days: 7 From e33e0f45a77ab271e07c5aae0efb4451a3727184 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 09:02:44 +0300 Subject: [PATCH 09/30] ci(af01): authorize cargo-deny job --- .github/workflow-trust-policy.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflow-trust-policy.json b/.github/workflow-trust-policy.json index 3d070ada..7e994b67 100644 --- a/.github/workflow-trust-policy.json +++ b/.github/workflow-trust-policy.json @@ -24,6 +24,13 @@ "workflows": { ".github/workflows/af01-security.yml": { "jobs": { + "cargo-deny": { + "permissions": { + "contents": "read" + }, + "runner": "ubuntu-24.04", + "timeout_minutes": 15 + }, "dependency-inventory": { "permissions": { "contents": "read" From 3dcef463b1806e3b4fc4173453fedfdb3b616390 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:23:19 +0300 Subject: [PATCH 10/30] fix(af01): allow private workspace path edge --- deny.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deny.toml b/deny.toml index 943393f4..6aefe771 100644 --- a/deny.toml +++ b/deny.toml @@ -34,6 +34,10 @@ registries = [] # without turning the current lockfile into an opaque skip-list contract. multiple-versions = "warn" wildcards = "deny" +# Private workspace path dependencies are repository-owned source, not external +# version authority. Keep external wildcard requirements denied while allowing +# the unpublished commandf -> commandf-pkg path edge recorded by T020. +allow-wildcard-paths = true highlight = "all" workspace-default-features = "allow" external-default-features = "allow" From ce63625c64e5512aef6ddcf15aec8c67e653d939 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:23:34 +0300 Subject: [PATCH 11/30] fix(af01): avoid Cargo token false positive --- .github/workflows/af01-security.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index e626d247..6d90b4be 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -21,9 +21,9 @@ jobs: - name: Generate exact locked dependency inventory run: | set -euo pipefail - cargo metadata --locked --format-version 1 > /tmp/commandf-cargo-metadata.json + cargo metadata --locked --format-version 1 > /tmp/commandf-dependency-metadata.json python3 .github/scripts/summarize_cargo_metadata.py \ - < /tmp/commandf-cargo-metadata.json \ + < /tmp/commandf-dependency-metadata.json \ | tee af01-t020-dependency-inventory.json - name: Upload exact dependency inventory uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 From 33f00f57876b0b6230328152313e1be500924ee4 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:25:04 +0300 Subject: [PATCH 12/30] ci(af01): add pinned cargo-audit gate --- .github/workflows/af01-security.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index 6d90b4be..1d596924 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -83,3 +83,12 @@ jobs: path: af01-cargo-deny-proof.json if-no-files-found: error retention-days: 7 + + cargo-audit: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@032958afbdc797a916606bdc6ffcbf961 # placeholder-invalid From d3ad899181eba2af978f4c1902b9e6124bd808e9 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:25:35 +0300 Subject: [PATCH 13/30] fix(af01): complete pinned cargo-audit gate --- .github/workflows/af01-security.yml | 71 ++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index 1d596924..488ffa0d 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -91,4 +91,73 @@ jobs: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@032958afbdc797a916606bdc6ffcbf961 # placeholder-invalid + - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 + - name: Install pinned cargo-audit + run: cargo install cargo-audit --version 0.22.2 --locked + - name: Audit exact Cargo.lock + run: | + set +e + cargo audit --file Cargo.lock --json > af01-cargo-audit.json + audit_status=$? + set -e + printf '%s\n' "$audit_status" > af01-cargo-audit.exit + - name: Record cargo-audit and advisory database identity + if: always() + run: | + set -euo pipefail + python3 - <<'PY' + import hashlib + import json + import os + import subprocess + from pathlib import Path + + db = Path.home() / ".cargo" / "advisory-db" + exit_path = Path("af01-cargo-audit.exit") + result_path = Path("af01-cargo-audit.json") + if not exit_path.is_file() or not result_path.is_file() or not (db / ".git").is_dir(): + raise SystemExit("cargo-audit evidence or advisory database identity is missing") + + proof = { + "advisory_db_commit": subprocess.check_output( + ["git", "-C", str(db), "rev-parse", "HEAD"], text=True + ).strip(), + "advisory_db_origin": subprocess.check_output( + ["git", "-C", str(db), "remote", "get-url", "origin"], text=True + ).strip(), + "cargo_audit_version": "0.22.2", + "cargo_lock_sha256": hashlib.sha256(Path("Cargo.lock").read_bytes()).hexdigest(), + "command": "cargo audit --file Cargo.lock --json", + "exit_code": int(exit_path.read_text(encoding="utf-8").strip()), + "head_sha": os.environ["GITHUB_SHA"], + "schema": 1, + } + Path("af01-cargo-audit-proof.json").write_text( + json.dumps(proof, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(proof, indent=2, sort_keys=True)) + PY + - name: Upload cargo-audit evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: af01-cargo-audit + path: | + af01-cargo-audit.json + af01-cargo-audit.exit + af01-cargo-audit-proof.json + if-no-files-found: error + retention-days: 7 + - name: Enforce cargo-audit result + if: always() + run: | + python3 - <<'PY' + from pathlib import Path + import sys + + path = Path("af01-cargo-audit.exit") + if not path.is_file(): + raise SystemExit("cargo-audit exit evidence is missing") + sys.exit(int(path.read_text(encoding="utf-8").strip())) + PY From a5b1f52722fbf14b567eb263d7a5c1e8c8860fd0 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:25:48 +0300 Subject: [PATCH 14/30] ci(af01): authorize cargo-audit job --- .github/workflow-trust-policy.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflow-trust-policy.json b/.github/workflow-trust-policy.json index 7e994b67..8799fd42 100644 --- a/.github/workflow-trust-policy.json +++ b/.github/workflow-trust-policy.json @@ -24,6 +24,13 @@ "workflows": { ".github/workflows/af01-security.yml": { "jobs": { + "cargo-audit": { + "permissions": { + "contents": "read" + }, + "runner": "ubuntu-24.04", + "timeout_minutes": 20 + }, "cargo-deny": { "permissions": { "contents": "read" From 5b764e9044b9af1aa437938f03e3d7b66e474315 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:26:57 +0300 Subject: [PATCH 15/30] docs(af01): define security waiver requirements --- .../security-waiver-policy.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 specs/015-af-01-trusted-development-baseline/security-waiver-policy.md diff --git a/specs/015-af-01-trusted-development-baseline/security-waiver-policy.md b/specs/015-af-01-trusted-development-baseline/security-waiver-policy.md new file mode 100644 index 00000000..e4a64172 --- /dev/null +++ b/specs/015-af-01-trusted-development-baseline/security-waiver-policy.md @@ -0,0 +1,43 @@ +# AF-01 Security Waiver Policy + +Status: `T024_POLICY` + +AF-01 security findings and dependency/advisory exceptions are fail-closed by default. A waiver is a temporary, reviewable repository artifact; it is never an anonymous scanner ignore, a lowered severity threshold, or a permanent substitute for remediation. + +## Required waiver fields + +Every waiver MUST record all of the following: + +1. **Identity** — exact scanner/finding/advisory identifier and, when applicable, exact package name/version or workflow/action path. +2. **Rationale** — why the finding cannot be remediated immediately and why accepting it is justified for commandF's actual exposure. +3. **Scope** — the smallest affected package/version, file/path, job, action, or rule identity. Family-wide or wildcard scope is prohibited unless a separate canonical plan amendment authorizes it. +4. **Compensating evidence** — exact tests, configuration, reachability evidence, containment, or other controls that reduce the accepted risk. Assertions without inspectable evidence are insufficient. +5. **Revisit/removal condition** — a concrete condition that ends the waiver, such as an upstream fixed release, dependency migration, workflow redesign, or a bounded review date plus an owner decision. + +## Evidence binding + +A waiver MUST also record: + +- the repository task/spec authority that permits the exception; +- the exact tool and policy surface that consumes the waiver; +- the first canonical commit that introduced the waiver; +- links or identifiers for upstream advisories/issues when available; +- whether the finding is security, maintenance, license, source, duplicate, or workflow-authority related. + +## Prohibited waiver forms + +The following are not valid AF-01 waivers: + +- an unannotated advisory ID in an ignore list; +- a broad package-family, source-family, license-family, or workflow-directory wildcard; +- lowering a global severity threshold to make an existing finding disappear; +- disabling a scanner or audit because it reports a valid finding; +- treating a network/tool/reviewer failure as PASS; +- accepting a finding solely because another scanner did not report it; +- a waiver without a removal/revisit condition. + +## Review and removal + +Waiver introduction, widening, renewal, or removal is a security-relevant repository change and MUST pass the same exact-head AF-01 workflow/security gates and review discipline as the policy it affects. When the removal condition is met, the waiver must be deleted rather than silently retained. + +Any future machine-readable waiver representation must preserve these fields and fail closed when one is missing. T024 does not authorize any current waiver; AF-01 Stack B starts with zero advisory/security waivers. From bfd479289a75ab87cd2f75521fed6da47d0ce7c5 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:27:33 +0300 Subject: [PATCH 16/30] ci(af01): add pinned zizmor gate --- .github/workflows/af01-security.yml | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index 488ffa0d..2be0d49a 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -161,3 +161,58 @@ jobs: raise SystemExit("cargo-audit exit evidence is missing") sys.exit(int(path.read_text(encoding="utf-8").strip())) PY + + zizmor: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 + with: + persist-credentials: false + - name: Record zizmor policy identity + run: | + set -euo pipefail + python3 - <<'PY' + import json + import os + from pathlib import Path + + proof = { + "action_commit": "3dc1ecc9bcb9e94e9b2c709687979e1298497054", + "action_release": "v0.6.2", + "advanced_security": False, + "collect": "all", + "head_sha": os.environ["GITHUB_SHA"], + "min_severity": "medium", + "online_audits": False, + "schema": 1, + "zizmor_version": "1.29.0", + } + Path("af01-zizmor-proof.json").write_text( + json.dumps(proof, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(proof, indent=2, sort_keys=True)) + PY + - name: Audit workflows and Actions with zizmor + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 / zizmor 1.29.0 + with: + inputs: . + collect: all + online-audits: false + persona: regular + min-severity: medium + version: 1.29.0 + token: "" + advanced-security: false + color: false + annotations: false + fail-on-no-inputs: true + - name: Upload zizmor policy identity + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: af01-zizmor-proof + path: af01-zizmor-proof.json + if-no-files-found: error + retention-days: 7 From e89928fb3edf659f67b3abb5ec6200497ab69fdb Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:27:49 +0300 Subject: [PATCH 17/30] ci(af01): authorize zizmor job --- .github/workflow-trust-policy.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflow-trust-policy.json b/.github/workflow-trust-policy.json index 8799fd42..407a3f21 100644 --- a/.github/workflow-trust-policy.json +++ b/.github/workflow-trust-policy.json @@ -44,6 +44,13 @@ }, "runner": "ubuntu-24.04", "timeout_minutes": 10 + }, + "zizmor": { + "permissions": { + "contents": "read" + }, + "runner": "ubuntu-24.04", + "timeout_minutes": 15 } } }, From fc49c0c0439d84c3c79264d54d0e585495b2e4e8 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:28:18 +0300 Subject: [PATCH 18/30] test(af01): lock Stack B security gate coverage --- ...test_audit_workflow_trust_af01_coverage.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/scripts/test_audit_workflow_trust_af01_coverage.py diff --git a/.github/scripts/test_audit_workflow_trust_af01_coverage.py b/.github/scripts/test_audit_workflow_trust_af01_coverage.py new file mode 100644 index 00000000..d7136257 --- /dev/null +++ b/.github/scripts/test_audit_workflow_trust_af01_coverage.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + +MODULE_PATH = Path(__file__).with_name("audit_workflow_trust.py") +SPEC = importlib.util.spec_from_file_location("audit_workflow_trust_af01_coverage_target", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +AUDIT = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = AUDIT +SPEC.loader.exec_module(AUDIT) + +ROOT = Path(__file__).resolve().parents[2] +SECURITY_WORKFLOW = ROOT / ".github" / "workflows" / "af01-security.yml" + + +class Af01SecurityCoverageTests(unittest.TestCase): + @staticmethod + def pull_request_children(text: str) -> list[str]: + lines = text.splitlines() + try: + start = lines.index(" pull_request:") + except ValueError as error: + raise AssertionError("af01-security must have a pull_request trigger") from error + + children: list[str] = [] + for line in lines[start + 1 :]: + if line and not line.startswith(" "): + break + if line.startswith(" ") and not line.startswith(" ") and line.strip(): + break + if line.strip(): + children.append(line.strip()) + return children + + def test_stack_b_security_gate_is_universal_for_pull_requests(self) -> None: + text = SECURITY_WORKFLOW.read_text(encoding="utf-8") + children = self.pull_request_children(text) + self.assertFalse( + any(child.startswith(("paths:", "paths-ignore:")) for child in children), + "AF-01 Stack B must remain universal so policy/config/action metadata changes cannot bypass it", + ) + + covered_surfaces = ( + "deny.toml", + ".github/workflow-trust-policy.json", + ".github/workflows/af01-security.yml", + ".github/scripts/audit_workflow_trust.py", + "action.yml", + "nested/action.yaml", + ) + for surface in covered_surfaces: + with self.subTest(surface=surface): + self.assertEqual(children, [], f"universal pull_request coverage must include {surface}") + + def test_action_metadata_discovery_covers_both_supported_filenames(self) -> None: + workflows, actions = AUDIT.discover_security_files( + [ + ".github/workflows/af01-security.yml", + "action.yml", + "nested/action.yaml", + "nested/not-an-action.yml", + ] + ) + self.assertEqual(workflows, [".github/workflows/af01-security.yml"]) + self.assertEqual(actions, ["action.yml", "nested/action.yaml"]) + + +if __name__ == "__main__": + unittest.main() From 66ba48fa7aaa895c8b3cd3d7fefbb82ec55abac7 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:30:58 +0300 Subject: [PATCH 19/30] fix(af01): harden Stack B scanner execution --- .github/workflows/af01-security.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index 2be0d49a..57076fcb 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -97,10 +97,10 @@ jobs: - name: Audit exact Cargo.lock run: | set +e - cargo audit --file Cargo.lock --json > af01-cargo-audit.json + cargo audit --file Cargo.lock --json > af01-rustsec-audit.json audit_status=$? set -e - printf '%s\n' "$audit_status" > af01-cargo-audit.exit + printf '%s\n' "$audit_status" > af01-rustsec-audit.exit - name: Record cargo-audit and advisory database identity if: always() run: | @@ -113,8 +113,8 @@ jobs: from pathlib import Path db = Path.home() / ".cargo" / "advisory-db" - exit_path = Path("af01-cargo-audit.exit") - result_path = Path("af01-cargo-audit.json") + exit_path = Path("af01-rustsec-audit.exit") + result_path = Path("af01-rustsec-audit.json") if not exit_path.is_file() or not result_path.is_file() or not (db / ".git").is_dir(): raise SystemExit("cargo-audit evidence or advisory database identity is missing") @@ -132,7 +132,7 @@ jobs: "head_sha": os.environ["GITHUB_SHA"], "schema": 1, } - Path("af01-cargo-audit-proof.json").write_text( + Path("af01-rustsec-audit-proof.json").write_text( json.dumps(proof, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) @@ -142,11 +142,11 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: af01-cargo-audit + name: af01-rustsec-audit path: | - af01-cargo-audit.json - af01-cargo-audit.exit - af01-cargo-audit-proof.json + af01-rustsec-audit.json + af01-rustsec-audit.exit + af01-rustsec-audit-proof.json if-no-files-found: error retention-days: 7 - name: Enforce cargo-audit result @@ -156,7 +156,7 @@ jobs: from pathlib import Path import sys - path = Path("af01-cargo-audit.exit") + path = Path("af01-rustsec-audit.exit") if not path.is_file(): raise SystemExit("cargo-audit exit evidence is missing") sys.exit(int(path.read_text(encoding="utf-8").strip())) @@ -203,7 +203,6 @@ jobs: persona: regular min-severity: medium version: 1.29.0 - token: "" advanced-security: false color: false annotations: false From 7d4dcaab27a96f360e4b3683d6e35b93e3650ca8 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:35:22 +0300 Subject: [PATCH 20/30] fix(af01): bind Stack B evidence to exact source --- .github/workflows/af01-security.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/af01-security.yml b/.github/workflows/af01-security.yml index 57076fcb..0cb0859f 100644 --- a/.github/workflows/af01-security.yml +++ b/.github/workflows/af01-security.yml @@ -9,6 +9,9 @@ on: permissions: contents: read +env: + AF01_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + jobs: dependency-inventory: runs-on: ubuntu-24.04 @@ -16,11 +19,13 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: + ref: ${{ env.AF01_SOURCE_SHA }} persist-credentials: false - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 - name: Generate exact locked dependency inventory run: | set -euo pipefail + test "$(git rev-parse HEAD)" = "$AF01_SOURCE_SHA" cargo metadata --locked --format-version 1 > /tmp/commandf-dependency-metadata.json python3 .github/scripts/summarize_cargo_metadata.py \ < /tmp/commandf-dependency-metadata.json \ @@ -39,10 +44,12 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: + ref: ${{ env.AF01_SOURCE_SHA }} persist-credentials: false - name: Record cargo-deny proof identity run: | set -euo pipefail + test "$(git rev-parse HEAD)" = "$AF01_SOURCE_SHA" python3 - <<'PY' import hashlib import json @@ -60,7 +67,7 @@ jobs: "checks": ["advisories", "bans", "licenses", "sources"], "command": "cargo deny --all-features check advisories bans licenses sources", "deny_toml_sha256": sha256("deny.toml"), - "head_sha": os.environ["GITHUB_SHA"], + "head_sha": os.environ["AF01_SOURCE_SHA"], "schema": 1, } Path("af01-cargo-deny-proof.json").write_text( @@ -90,8 +97,11 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: + ref: ${{ env.AF01_SOURCE_SHA }} persist-credentials: false - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 + - name: Assert exact source checkout + run: test "$(git rev-parse HEAD)" = "$AF01_SOURCE_SHA" - name: Install pinned cargo-audit run: cargo install cargo-audit --version 0.22.2 --locked - name: Audit exact Cargo.lock @@ -129,7 +139,7 @@ jobs: "cargo_lock_sha256": hashlib.sha256(Path("Cargo.lock").read_bytes()).hexdigest(), "command": "cargo audit --file Cargo.lock --json", "exit_code": int(exit_path.read_text(encoding="utf-8").strip()), - "head_sha": os.environ["GITHUB_SHA"], + "head_sha": os.environ["AF01_SOURCE_SHA"], "schema": 1, } Path("af01-rustsec-audit-proof.json").write_text( @@ -168,10 +178,12 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 with: + ref: ${{ env.AF01_SOURCE_SHA }} persist-credentials: false - name: Record zizmor policy identity run: | set -euo pipefail + test "$(git rev-parse HEAD)" = "$AF01_SOURCE_SHA" python3 - <<'PY' import json import os @@ -182,7 +194,7 @@ jobs: "action_release": "v0.6.2", "advanced_security": False, "collect": "all", - "head_sha": os.environ["GITHUB_SHA"], + "head_sha": os.environ["AF01_SOURCE_SHA"], "min_severity": "medium", "online_audits": False, "schema": 1, From 0667067608f34539da32a641f0b6a72d62d4be41 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:36:42 +0300 Subject: [PATCH 21/30] docs(af01): record Stack B policy calibration --- .../stack-b-dependency-inventory.md | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md index 12366767..2106ee65 100644 --- a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md +++ b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md @@ -2,7 +2,7 @@ Status: `T020_COMPLETE_POLICY_INPUT` -This document records the exact dependency/license/source evidence inspected before creating `deny.toml`. It is the policy input for AF-01 T021; it is not itself a scanner PASS. +This document records the exact dependency/license/source evidence inspected before creating `deny.toml`, plus the observed Stack B scanner calibration used to freeze the initial policies. It is not the final T028 exact-head qualification record. ## Canonical input @@ -90,14 +90,16 @@ Zlib No broader license family, wildcard, or blanket approval is authorized by this inventory. -### Legacy metadata exception boundary +### Legacy metadata boundary Two exact packages report the legacy non-SPDX expression `MIT/Apache-2.0` in Cargo metadata: - `filetime@0.2.29` - `version_check@0.9.5` -T021 must not convert this into a global acceptance rule. If cargo-deny 0.20.2 does not normalize the legacy expression itself, any remediation must be package-scoped and evidence-backed (for example, an exact package exception or license clarification tied to upstream license material). The policy must remain narrower than accepting arbitrary malformed license expressions. +`cargo-deny 0.20.2` accepted the locked graph under the narrow atom allowlist without a package-specific legacy-expression waiver. No global malformed-license exception or package exception was added. + +The two workspace crates are unpublished (`publish = false`). `deny.toml` therefore ignores private workspace-crate license declarations while continuing to enforce every third-party dependency license. This is a repository-owned-boundary decision, not permission to ignore a private third-party registry. ## Duplicate/version inventory @@ -111,13 +113,16 @@ windows-sys: 0.52.0, 0.61.2 These are current transitive graph facts, not silently approved permanent exceptions. -T021 policy intent: +T021 policy intent and implemented boundary: - keep duplicate versions visible and machine-diagnosable; - use `multiple-versions = "warn"` initially rather than creating opaque skip lists for the current graph; -- set wildcard dependency requirements to fail closed; +- keep external wildcard dependency requirements fail-closed; +- permit the repository-owned unpublished workspace path edge with `allow-wildcard-paths = true` because the local path requirement has no registry version requirement to pin; - do not add `skip` or `skip-tree` entries unless a later exact finding proves a narrow necessity and records a reason/revisit condition. +`allow-wildcard-paths = true` does not authorize wildcard version requirements for crates.io, git, or alternate registries. Source policy remains independently fail-closed. + ## Source policy intent Observed source authority is only: @@ -125,7 +130,7 @@ Observed source authority is only: - local workspace/path packages; and - crates.io via `https://github.com/rust-lang/crates.io-index`. -T021 must therefore enforce: +T021 therefore enforces: ```text unknown registry: deny @@ -134,7 +139,7 @@ allowed registry: crates.io only allowed git sources: none ``` -A future git dependency, alternate registry, or additional source authority must require a repository diff and explicit policy review; it must not be admitted by a wildcard source rule. +A future git dependency, alternate registry, or additional source authority requires a repository diff and explicit policy review; it cannot be admitted by a wildcard source rule. ## Advisory policy intent @@ -143,23 +148,50 @@ At the policy layer: - RustSec advisories are not blanket-ignored; - `advisories.ignore` starts empty; - current/future advisory waivers are governed by T024 rather than handwritten anonymous ignore entries; -- yanked or vulnerable dependency evidence must remain visible to the scanner gates; -- scanner transport/tooling failures must be CI failures, not PASS-equivalent outcomes. +- yanked or vulnerable dependency evidence remains visible to the scanner gates; +- scanner transport/tooling failures are CI failures, not PASS-equivalent outcomes. -T023 remains responsible for an independent cargo-audit view so cargo-deny is not the only advisory signal. +T023 supplies an independent `cargo-audit` view so cargo-deny is not the only advisory signal. ## License policy intent -T021 should allow only the observed SPDX atoms listed above and use cargo-deny 0.20.2's exact configuration semantics. The policy must: +T021 allows only the observed SPDX atoms listed above and uses cargo-deny 0.20.2's exact configuration semantics. The policy: + +- has no wildcard license approval; +- ignores only the unpublished workspace crates as first-party license subjects while retaining third-party dependency checks; +- preserves `confidence-threshold = 0.8`; +- has no package/version license exception or skip list; +- fails a new dependency whose license cannot be matched to the checked-in policy. + +## Stack B scanner calibration + +The following run is calibration evidence, not the final T028 head qualification: + +```text +PR head: 66ba48fa7aaa895c8b3cd3d7fefbb82ec55abac7 +PR head tree: 53196a991a7ee579da0d72f4f0b8c5373ba7698a +GitHub PR merge-ref tree: 53196a991a7ee579da0d72f4f0b8c5373ba7698a +workflow: af01-security +run: 33050044070 +``` + +The PR head and GitHub's temporary merge ref had the same tree, so the scanner inputs were byte-identical. The workflow was subsequently hardened to checkout and attest `AF01_SOURCE_SHA` explicitly because the original proof JSON labeled GitHub's temporary `GITHUB_SHA` as `head_sha`. + +Observed calibration results: + +- dependency inventory: `SUCCESS`; +- cargo-deny action `v2.1.1` at commit `3c6349835b2b7b196a839186cb8b78e02f7b5f25`, cargo-deny `0.20.2`: `SUCCESS` for advisories, bans, licenses, and sources; +- RustSec cargo-audit `0.22.2`: exit `0` against exact `Cargo.lock`; +- RustSec advisory database origin: `https://github.com/RustSec/advisory-db.git`; +- observed advisory database commit: `a7bfe16948bf6f3ee25bdee4822209f87da21b80`; +- observed `Cargo.lock` SHA-256: `0c58bb1b2a78ad5ed7e196ef20622fb5673536146ab6e0f787eb2d5f6517cf66`; +- zizmor action `v0.6.2` at commit `3dc1ecc9bcb9e94e9b2c709687979e1298497054`, zizmor `1.29.0`, `min-severity=medium`, online audits disabled: `SUCCESS` with no blocking medium/high finding; +- security waivers: zero. -- avoid wildcard license approval; -- avoid ignoring private/workspace packages as a shortcut unless repository semantics require it; -- preserve `confidence-threshold = 0.8` unless exact scanner evidence justifies a narrower change; -- scope any legacy-license remediation to the exact affected package/version; -- fail on any new dependency whose license cannot be matched to the checked-in policy. +This observed baseline freezes the initial zizmor gate at `medium`; it does not authorize lowering the threshold around a future finding. T026 therefore has no current high/medium finding to disposition. Any later finding must be fixed or explicitly dispositioned under the AF-01 waiver policy without silently weakening the gate. ## T020 decision `T020 = COMPLETE` -The current graph, source authority, license surface, and duplicate families are now documented from exact locked evidence. T021 may create `deny.toml` from this inventory. No cargo-deny, cargo-audit, advisory-waiver, or zizmor PASS is claimed by this document. +The current graph, source authority, license surface, duplicate families, and initial scanner calibration are documented. T021–T027 implement the checked-in dependency/workflow security gates and waiver/coverage policy. Final Stack B PASS remains governed by T028 exact-head workflow evidence and T029 exact-head independent reviews plus canonical merge truth. From e8e71acde7147ae2a3b2b02d7fc3e46008151b83 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:37:12 +0300 Subject: [PATCH 22/30] docs(af01): reconcile Stack A and Stack B task state --- .../tasks.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/specs/015-af-01-trusted-development-baseline/tasks.md b/specs/015-af-01-trusted-development-baseline/tasks.md index d5999196..f88317db 100644 --- a/specs/015-af-01-trusted-development-baseline/tasks.md +++ b/specs/015-af-01-trusted-development-baseline/tasks.md @@ -29,22 +29,22 @@ Depends on T005. - [x] **T014** Harden `.github/workflows/ci.yml` to full-SHA external Actions, credentialless checkout, explicit machine-checkable least permissions, fixed supported runner label, bounded timeout, and preserved existing semantic/test steps. - [x] **T015** Reconcile every other existing workflow and repository Action metadata file to the AF-01 baseline, including permission declarations and proof-critical container digest identity, without changing its product/oracle/proof semantics or path-filter authority except where later universal required-check aggregation is explicitly introduced. - [x] **T016** Add a regression that discovers both `action.yml` and `action.yaml` anywhere in the tracked tree and fails if a future workflow, Action metadata file, permission grant, external Action ref, checkout credential setting, or proof-critical container identity escapes AF-01 trust auditing. -- [ ] **T017** Run mandatory workspace gates and every path-applicable existing proof/oracle workflow on the exact Stack A head. -- [ ] **T018** Request CodeRabbit and Qodo on exact Stack A head; disposition every substantive returned finding and require zero unresolved material review threads. -- [ ] **T019** Merge Stack A only from its exact qualified head and record canonical merge/main/tree. +- [x] **T017** Run mandatory workspace gates and every path-applicable existing proof/oracle workflow on the exact Stack A head. +- [x] **T018** Request CodeRabbit and Qodo on exact Stack A head; disposition every substantive returned finding and require zero unresolved material review threads. +- [x] **T019** Merge Stack A only from its exact qualified head and record canonical merge/main/tree. ## Phase 2 / Stack B — dependency and CI security gates Depends on canonical T019. -- [ ] **T020** Inspect the exact current Cargo dependency graph and license/source metadata; document intended direct/transitive source and license policy before generating `deny.toml`. -- [ ] **T021** Add checked-in `deny.toml` covering licenses, bans/duplicates, advisories, and sources with narrow reviewed exceptions only. -- [ ] **T022** Add pinned `cargo-deny` execution in an independently diagnosable CI job; retain machine-readable or complete textual evidence. -- [ ] **T023** Add pinned RustSec `cargo-audit` execution against exact `Cargo.lock`; retain advisory database/tool identity where available. -- [ ] **T024** Define waiver documentation requirements for any advisory/security exception: identity, rationale, scope, compensating evidence, and revisit/removal condition. -- [ ] **T025** Add pinned `zizmor` audit over all repository workflows/actions; freeze initial severity policy from observed baseline rather than guessing around findings. -- [ ] **T026** Fix valid high/medium workflow findings or amend the plan/tasks with explicit reviewed disposition; do not lower the gate silently. -- [ ] **T027** Add regressions proving dependency/workflow security configurations and both Action metadata filename forms are included in relevant workflow path/coverage logic so policy mutations cannot bypass gates. +- [x] **T020** Inspect the exact current Cargo dependency graph and license/source metadata; document intended direct/transitive source and license policy before generating `deny.toml`. +- [x] **T021** Add checked-in `deny.toml` covering licenses, bans/duplicates, advisories, and sources with narrow reviewed exceptions only. +- [x] **T022** Add pinned `cargo-deny` execution in an independently diagnosable CI job; retain machine-readable or complete textual evidence. +- [x] **T023** Add pinned RustSec `cargo-audit` execution against exact `Cargo.lock`; retain advisory database/tool identity where available. +- [x] **T024** Define waiver documentation requirements for any advisory/security exception: identity, rationale, scope, compensating evidence, and revisit/removal condition. +- [x] **T025** Add pinned `zizmor` audit over all repository workflows/actions; freeze initial severity policy from observed baseline rather than guessing around findings. +- [x] **T026** Fix valid high/medium workflow findings or amend the plan/tasks with explicit reviewed disposition; do not lower the gate silently. +- [x] **T027** Add regressions proving dependency/workflow security configurations and both Action metadata filename forms are included in relevant workflow path/coverage logic so policy mutations cannot bypass gates. - [ ] **T028** Run mandatory workspace gates plus all path-applicable existing proof/oracle workflows on exact Stack B head. - [ ] **T029** Obtain and disposition CodeRabbit/Qodo review on exact Stack B head, merge only from exact qualified head, and record canonical merge/main/tree. From 7386ef19fc14ff7edf05ddf6b17f95e8d80d21fa Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:40:29 +0300 Subject: [PATCH 23/30] fix(af01): preserve exact resolved dependency graph --- .github/scripts/summarize_cargo_metadata.py | 222 ++++++++++++++++---- 1 file changed, 178 insertions(+), 44 deletions(-) diff --git a/.github/scripts/summarize_cargo_metadata.py b/.github/scripts/summarize_cargo_metadata.py index cdc253ef..7947dd74 100644 --- a/.github/scripts/summarize_cargo_metadata.py +++ b/.github/scripts/summarize_cargo_metadata.py @@ -6,53 +6,87 @@ import json import sys from collections import defaultdict +from typing import Any CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index" -def main() -> int: - try: - metadata = json.load(sys.stdin) - except (json.JSONDecodeError, OSError) as error: - print(json.dumps({"error": f"invalid cargo metadata: {error}", "ok": False}, sort_keys=True)) - return 1 +class InventoryError(ValueError): + """Raised when Cargo metadata cannot support a complete exact inventory.""" + + +def require_string(value: object, message: str) -> str: + if not isinstance(value, str) or not value: + raise InventoryError(message) + return value + + +def validate_manifest_dependencies(package: dict[str, Any], identity: str) -> None: + dependencies = package.get("dependencies") + if not isinstance(dependencies, list): + raise InventoryError(f"manifest dependencies for {identity} are not an array") + for index, dependency in enumerate(dependencies): + if not isinstance(dependency, dict): + raise InventoryError( + f"manifest dependency {index} for {identity} is not an object" + ) + require_string( + dependency.get("name"), + f"manifest dependency {index} for {identity} has invalid name", + ) + + +def summarize(metadata: object) -> dict[str, object]: + if not isinstance(metadata, dict): + raise InventoryError("cargo metadata root is not an object") packages = metadata.get("packages") workspace_members = metadata.get("workspace_members") + resolve = metadata.get("resolve") if not isinstance(packages, list) or not isinstance(workspace_members, list): - print(json.dumps({"error": "cargo metadata missing package/workspace arrays", "ok": False}, sort_keys=True)) - return 1 + raise InventoryError("cargo metadata missing package/workspace arrays") + if not isinstance(resolve, dict) or not isinstance(resolve.get("nodes"), list): + raise InventoryError("cargo metadata missing resolved dependency graph") + + workspace_ids: set[str] = set() + for index, member in enumerate(workspace_members): + workspace_ids.add( + require_string(member, f"workspace member {index} is not a package id") + ) - workspace_ids = set(workspace_members) - inventory: list[dict[str, object]] = [] + package_records: dict[str, dict[str, object]] = {} versions_by_name: dict[str, set[str]] = defaultdict(set) license_packages: dict[str, list[str]] = defaultdict(list) source_classes: dict[str, int] = defaultdict(int) unknown_license: list[str] = [] non_crates_io: list[str] = [] - for package in packages: + for index, package in enumerate(packages): if not isinstance(package, dict): - print(json.dumps({"error": "cargo metadata contains non-object package", "ok": False}, sort_keys=True)) - return 1 - package_id = package.get("id") - name = package.get("name") - version = package.get("version") + raise InventoryError(f"cargo metadata package {index} is not an object") + + package_id = require_string(package.get("id"), "package identity is incomplete") + name = require_string(package.get("name"), "package identity is incomplete") + version = require_string(package.get("version"), "package identity is incomplete") + if package_id in package_records: + raise InventoryError(f"duplicate package id in cargo metadata: {package_id}") + source = package.get("source") license_expr = package.get("license") - if not all(isinstance(value, str) for value in (package_id, name, version)): - print(json.dumps({"error": "package identity is incomplete", "ok": False}, sort_keys=True)) - return 1 if source is not None and not isinstance(source, str): - print(json.dumps({"error": f"invalid source for {name} {version}", "ok": False}, sort_keys=True)) - return 1 + raise InventoryError(f"invalid source for {name} {version}") if license_expr is not None and not isinstance(license_expr, str): - print(json.dumps({"error": f"invalid license for {name} {version}", "ok": False}, sort_keys=True)) - return 1 + raise InventoryError(f"invalid license for {name} {version}") identity = f"{name}@{version}" + validate_manifest_dependencies(package, identity) + workspace = package_id in workspace_ids - source_class = "workspace" if workspace else ("crates.io" if source == CRATES_IO_SOURCE else "other") + source_class = ( + "workspace" + if workspace + else ("crates.io" if source == CRATES_IO_SOURCE else "other") + ) source_classes[source_class] += 1 versions_by_name[name].add(version) if not workspace: @@ -63,27 +97,112 @@ def main() -> int: if source != CRATES_IO_SOURCE: non_crates_io.append(f"{identity}:{source or ''}") - dependencies = package.get("dependencies", []) - direct_dependency_names = sorted( - { - dependency.get("name") - for dependency in dependencies - if isinstance(dependency, dict) and isinstance(dependency.get("name"), str) - } + package_records[package_id] = { + "dependencies": [], + "license": license_expr, + "name": name, + "package_id": package_id, + "source": source, + "source_class": source_class, + "version": version, + "workspace": workspace, + } + + unknown_workspace_ids = sorted(workspace_ids - set(package_records)) + if unknown_workspace_ids: + raise InventoryError( + "workspace members reference unknown package ids: " + + ", ".join(unknown_workspace_ids) ) - inventory.append( - { - "dependencies": direct_dependency_names, - "license": license_expr, - "name": name, - "source": source, - "source_class": source_class, - "version": version, - "workspace": workspace, - } + + resolved_ids: set[str] = set() + for index, node in enumerate(resolve["nodes"]): + if not isinstance(node, dict): + raise InventoryError(f"resolved node {index} is not an object") + node_id = require_string(node.get("id"), f"resolved node {index} has invalid id") + if node_id not in package_records: + raise InventoryError(f"resolved node references unknown package id: {node_id}") + if node_id in resolved_ids: + raise InventoryError(f"duplicate resolved node for package id: {node_id}") + resolved_ids.add(node_id) + + dependency_ids = node.get("dependencies") + deps = node.get("deps") + if not isinstance(dependency_ids, list) or not isinstance(deps, list): + raise InventoryError(f"resolved node {node_id} has invalid dependency arrays") + + exact_dependency_ids: list[str] = [] + for dep_index, dependency_id in enumerate(dependency_ids): + exact_dependency_ids.append( + require_string( + dependency_id, + f"resolved dependency {dep_index} for {node_id} has invalid package id", + ) + ) + + resolved_edges: list[dict[str, object]] = [] + edge_package_ids: list[str] = [] + for dep_index, dep in enumerate(deps): + if not isinstance(dep, dict): + raise InventoryError( + f"resolved dependency edge {dep_index} for {node_id} is not an object" + ) + edge_name = require_string( + dep.get("name"), + f"resolved dependency edge {dep_index} for {node_id} has invalid name", + ) + target_id = require_string( + dep.get("pkg"), + f"resolved dependency edge {dep_index} for {node_id} has invalid package id", + ) + target = package_records.get(target_id) + if target is None: + raise InventoryError( + f"resolved dependency edge for {node_id} references unknown package id: {target_id}" + ) + edge_package_ids.append(target_id) + resolved_edges.append( + { + "name": edge_name, + "package_id": target_id, + "package_name": target["name"], + "source": target["source"], + "version": target["version"], + } + ) + + if sorted(set(exact_dependency_ids)) != sorted(set(edge_package_ids)): + raise InventoryError( + f"resolved dependency representations disagree for package id: {node_id}" + ) + + resolved_edges.sort( + key=lambda edge: ( + str(edge["name"]), + str(edge["package_name"]), + str(edge["version"]), + str(edge["source"]), + str(edge["package_id"]), + ) + ) + package_records[node_id]["dependencies"] = resolved_edges + + missing_resolved_ids = sorted(set(package_records) - resolved_ids) + if missing_resolved_ids: + raise InventoryError( + "packages missing from resolved dependency graph: " + + ", ".join(missing_resolved_ids) ) - inventory.sort(key=lambda item: (str(item["name"]), str(item["version"]), str(item["source"]))) + inventory = sorted( + package_records.values(), + key=lambda item: ( + str(item["name"]), + str(item["version"]), + str(item["source"]), + str(item["package_id"]), + ), + ) duplicates = { name: sorted(versions) for name, versions in sorted(versions_by_name.items()) @@ -93,17 +212,32 @@ def main() -> int: expression: sorted(identities) for expression, identities in sorted(license_packages.items()) } - result = { + return { "duplicates": duplicates, "licenses": licenses, "non_crates_io": sorted(non_crates_io), "ok": not unknown_license, "package_count": len(inventory), "packages": inventory, - "schema": 1, + "schema": 2, "source_classes": dict(sorted(source_classes.items())), "unknown_license": sorted(unknown_license), } + + +def main() -> int: + try: + metadata = json.load(sys.stdin) + result = summarize(metadata) + except (json.JSONDecodeError, OSError, InventoryError) as error: + print( + json.dumps( + {"error": f"invalid cargo metadata: {error}", "ok": False}, + sort_keys=True, + ) + ) + return 1 + print(json.dumps(result, indent=2, sort_keys=True)) return 0 if result["ok"] else 1 From 55a0a1e1288d4e3c89759da016d0712932c7e84a Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:40:56 +0300 Subject: [PATCH 24/30] test(af01): regress exact dependency inventory failures --- .../scripts/test_summarize_cargo_metadata.py | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 .github/scripts/test_summarize_cargo_metadata.py diff --git a/.github/scripts/test_summarize_cargo_metadata.py b/.github/scripts/test_summarize_cargo_metadata.py new file mode 100644 index 00000000..27461a4c --- /dev/null +++ b/.github/scripts/test_summarize_cargo_metadata.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + +MODULE_PATH = Path(__file__).with_name("summarize_cargo_metadata.py") +SPEC = importlib.util.spec_from_file_location("summarize_cargo_metadata_target", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +SUMMARY = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = SUMMARY +SPEC.loader.exec_module(SUMMARY) + +CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index" + + +def package( + package_id: str, + name: str, + version: str, + *, + source: str | None = CRATES_IO_SOURCE, + license_expr: str | None = "MIT", + dependencies: object | None = None, +) -> dict[str, object]: + return { + "id": package_id, + "name": name, + "version": version, + "source": source, + "license": license_expr, + "dependencies": [] if dependencies is None else dependencies, + } + + +def valid_metadata() -> dict[str, object]: + root_id = "path+file:///workspace/commandf#0.1.0" + old_id = "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17" + new_id = "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.3" + return { + "packages": [ + package( + root_id, + "commandf", + "0.1.0", + source=None, + license_expr=None, + dependencies=[ + {"name": "rand_old"}, + {"name": "rand_new"}, + ], + ), + package(old_id, "getrandom", "0.2.17"), + package(new_id, "getrandom", "0.4.3"), + ], + "workspace_members": [root_id], + "resolve": { + "nodes": [ + { + "id": root_id, + "dependencies": [old_id, new_id], + "deps": [ + {"name": "rand_old", "pkg": old_id, "dep_kinds": []}, + {"name": "rand_new", "pkg": new_id, "dep_kinds": []}, + ], + }, + {"id": old_id, "dependencies": [], "deps": []}, + {"id": new_id, "dependencies": [], "deps": []}, + ] + }, + } + + +class CargoMetadataSummaryTests(unittest.TestCase): + def test_resolved_edges_preserve_exact_selected_package_identity(self) -> None: + result = SUMMARY.summarize(valid_metadata()) + self.assertEqual(result["schema"], 2) + root = next(item for item in result["packages"] if item["name"] == "commandf") + self.assertEqual( + [ + (edge["name"], edge["package_name"], edge["version"], edge["package_id"]) + for edge in root["dependencies"] + ], + [ + ( + "rand_new", + "getrandom", + "0.4.3", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.3", + ), + ( + "rand_old", + "getrandom", + "0.2.17", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + ), + ], + ) + self.assertEqual(result["duplicates"], {"getrandom": ["0.2.17", "0.4.3"]}) + + def test_manifest_dependencies_must_be_an_array(self) -> None: + metadata = valid_metadata() + metadata["packages"][0]["dependencies"] = {"name": "hidden"} + with self.assertRaisesRegex(SUMMARY.InventoryError, "not an array"): + SUMMARY.summarize(metadata) + + def test_manifest_dependency_record_must_be_an_object(self) -> None: + metadata = valid_metadata() + metadata["packages"][0]["dependencies"] = ["hidden"] + with self.assertRaisesRegex(SUMMARY.InventoryError, "is not an object"): + SUMMARY.summarize(metadata) + + def test_manifest_dependency_name_must_be_a_string(self) -> None: + metadata = valid_metadata() + metadata["packages"][0]["dependencies"] = [{"name": None}] + with self.assertRaisesRegex(SUMMARY.InventoryError, "invalid name"): + SUMMARY.summarize(metadata) + + def test_resolved_edge_requires_known_exact_package_id(self) -> None: + metadata = valid_metadata() + root = metadata["resolve"]["nodes"][0] + missing = "registry+https://github.com/rust-lang/crates.io-index#getrandom@9.9.9" + root["dependencies"] = [missing] + root["deps"] = [{"name": "missing", "pkg": missing, "dep_kinds": []}] + with self.assertRaisesRegex(SUMMARY.InventoryError, "unknown package id"): + SUMMARY.summarize(metadata) + + def test_resolved_dependency_representations_must_agree(self) -> None: + metadata = valid_metadata() + root = metadata["resolve"]["nodes"][0] + root["dependencies"] = root["dependencies"][:1] + with self.assertRaisesRegex(SUMMARY.InventoryError, "representations disagree"): + SUMMARY.summarize(metadata) + + def test_every_package_requires_a_resolved_node(self) -> None: + metadata = valid_metadata() + metadata["resolve"]["nodes"].pop() + with self.assertRaisesRegex(SUMMARY.InventoryError, "missing from resolved dependency graph"): + SUMMARY.summarize(metadata) + + +if __name__ == "__main__": + unittest.main() From 59df8185cdb882519a768f68235d8bc7a72b9b06 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:41:24 +0300 Subject: [PATCH 25/30] test(af01): wire dependency graph regressions --- .../test_audit_workflow_trust_af01_coverage.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/scripts/test_audit_workflow_trust_af01_coverage.py b/.github/scripts/test_audit_workflow_trust_af01_coverage.py index d7136257..eb173b01 100644 --- a/.github/scripts/test_audit_workflow_trust_af01_coverage.py +++ b/.github/scripts/test_audit_workflow_trust_af01_coverage.py @@ -13,6 +13,18 @@ sys.modules[SPEC.name] = AUDIT SPEC.loader.exec_module(AUDIT) +SUMMARY_TEST_PATH = Path(__file__).with_name("test_summarize_cargo_metadata.py") +SUMMARY_TEST_SPEC = importlib.util.spec_from_file_location( + "summarize_cargo_metadata_regression_tests", SUMMARY_TEST_PATH +) +assert SUMMARY_TEST_SPEC is not None and SUMMARY_TEST_SPEC.loader is not None +SUMMARY_TESTS = importlib.util.module_from_spec(SUMMARY_TEST_SPEC) +sys.modules[SUMMARY_TEST_SPEC.name] = SUMMARY_TESTS +SUMMARY_TEST_SPEC.loader.exec_module(SUMMARY_TESTS) +# The existing universal AF-01 unittest discovery pattern loads this module. Re-export +# the inventory regression suite so exact-graph tests cannot be skipped by that gate. +CargoMetadataSummaryTests = SUMMARY_TESTS.CargoMetadataSummaryTests + ROOT = Path(__file__).resolve().parents[2] SECURITY_WORKFLOW = ROOT / ".github" / "workflows" / "af01-security.yml" @@ -49,6 +61,8 @@ def test_stack_b_security_gate_is_universal_for_pull_requests(self) -> None: ".github/workflow-trust-policy.json", ".github/workflows/af01-security.yml", ".github/scripts/audit_workflow_trust.py", + ".github/scripts/summarize_cargo_metadata.py", + ".github/scripts/test_summarize_cargo_metadata.py", "action.yml", "nested/action.yaml", ) From 45629125e8e09f7d6cd6869fee82e4ae00f8483c Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:42:30 +0300 Subject: [PATCH 26/30] docs(af01): record resolved graph remediation --- .../stack-b-dependency-inventory.md | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md index 2106ee65..6aca7c08 100644 --- a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md +++ b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md @@ -32,7 +32,7 @@ artifact name: af01-t020-dependency-inventory artifact digest: sha256:370637ff98963b3804780bec3439275ab7c1ec80c63f123722d493fe2cd54247 ``` -The artifact was generated from the locked graph and uploaded by a full-SHA-pinned `actions/upload-artifact` action. +The artifact was generated from the locked graph and uploaded by a full-SHA-pinned `actions/upload-artifact` action. That initial evidence established package/license/source counts; later reviewer hardening changed the inventory representation from manifest dependency names to exact Cargo resolved edges, so final T028 evidence must come from the hardened schema described below rather than treating this early artifact as final graph-edge proof. ## Exact graph summary @@ -51,6 +51,28 @@ Workspace crates: The current workspace direct dependency surface is declared in the root/member manifests and includes `clap`, `flate2`, `semver`, `serde`, `serde_json`, `sha2`, `tar`, `thiserror`, `ureq`, and `tempfile`, plus the local path dependency from `commandf` to `commandf-pkg`. +### Resolved-graph authority and reviewer remediation + +Qodo identified two material correctness defects in the original T020 summarizer: + +1. malformed `packages[].dependencies` records could be silently filtered from the output; and +2. dependency relationships were projected from manifest declarations and reduced to names, so two resolved versions of the same crate could not be distinguished on individual edges. + +Both findings were remediated before T028 qualification. The inventory schema is now `2` and fails closed unless: + +- every package and manifest dependency record has the required structure and string identity fields; +- `resolve.nodes` exists and contains one unique node for every package in the resolved graph; +- every resolved edge has a dependency name plus exact target package ID; +- every edge target resolves to a known package record; +- Cargo's `dependencies` and `deps[].pkg` resolved-node representations agree; and +- every package in the metadata package set is represented by a resolved node. + +Each emitted dependency edge now records the dependency edge name plus the exact selected `package_id`, resolved package name, version, and source. This preserves distinctions such as `getrandom@0.2.17` versus `getrandom@0.4.3` instead of collapsing them to `getrandom`. + +Regression coverage in `.github/scripts/test_summarize_cargo_metadata.py` includes exact multi-version edge preservation, malformed dependency-array/record/name rejection, unknown resolved target rejection, disagreement between Cargo's two resolved dependency representations, and missing resolved-node rejection. The suite is re-exported through the existing universal AF-01 workflow-trust unittest discovery surface so these regressions cannot be omitted while that gate remains authoritative. + +The original Qodo threads became resolved/outdated only after the implementation changed. T029 still requires a fresh exact-head Qodo review; this remediation record is not a substitute for that review. + ## Observed license expressions The exact locked graph contains the following distinct dependency license expressions: @@ -179,7 +201,7 @@ The PR head and GitHub's temporary merge ref had the same tree, so the scanner i Observed calibration results: -- dependency inventory: `SUCCESS`; +- dependency inventory: `SUCCESS` under the earlier representation, superseded for final resolved-edge proof by schema 2; - cargo-deny action `v2.1.1` at commit `3c6349835b2b7b196a839186cb8b78e02f7b5f25`, cargo-deny `0.20.2`: `SUCCESS` for advisories, bans, licenses, and sources; - RustSec cargo-audit `0.22.2`: exit `0` against exact `Cargo.lock`; - RustSec advisory database origin: `https://github.com/RustSec/advisory-db.git`; @@ -188,10 +210,10 @@ Observed calibration results: - zizmor action `v0.6.2` at commit `3dc1ecc9bcb9e94e9b2c709687979e1298497054`, zizmor `1.29.0`, `min-severity=medium`, online audits disabled: `SUCCESS` with no blocking medium/high finding; - security waivers: zero. -This observed baseline freezes the initial zizmor gate at `medium`; it does not authorize lowering the threshold around a future finding. T026 therefore has no current high/medium finding to disposition. Any later finding must be fixed or explicitly dispositioned under the AF-01 waiver policy without silently weakening the gate. +This observed baseline freezes the initial zizmor gate at `medium`; it does not authorize lowering the threshold around a future finding. T026 therefore has no current zizmor high/medium finding to disposition. Any later finding must be fixed or explicitly dispositioned under the AF-01 waiver policy without silently weakening the gate. ## T020 decision `T020 = COMPLETE` -The current graph, source authority, license surface, duplicate families, and initial scanner calibration are documented. T021–T027 implement the checked-in dependency/workflow security gates and waiver/coverage policy. Final Stack B PASS remains governed by T028 exact-head workflow evidence and T029 exact-head independent reviews plus canonical merge truth. +The current graph, source authority, license surface, duplicate families, resolved-edge representation, fail-closed inventory validation, and initial scanner calibration are documented. T021–T027 implement the checked-in dependency/workflow security gates and waiver/coverage policy. Final Stack B PASS remains governed by T028 exact-head workflow evidence and T029 exact-head independent reviews plus canonical merge truth. From 7086a1ab50ab32d12b033224deb4d3308b7bb951 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:56:05 +0300 Subject: [PATCH 27/30] fix(af01): pin workspace path dependency --- crates/commandf-cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/commandf-cli/Cargo.toml b/crates/commandf-cli/Cargo.toml index 98f2f8d0..d6d806f7 100644 --- a/crates/commandf-cli/Cargo.toml +++ b/crates/commandf-cli/Cargo.toml @@ -7,4 +7,4 @@ publish.workspace = true [dependencies] clap.workspace = true -commandf-pkg = { path = "../commandf-pkg" } +commandf-pkg = { path = "../commandf-pkg", version = "=0.0.0" } From fde6f63f132f8ca9157e55e514fba350d1874ef1 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:56:16 +0300 Subject: [PATCH 28/30] fix(af01): remove global path wildcard bypass --- deny.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deny.toml b/deny.toml index 6aefe771..803de375 100644 --- a/deny.toml +++ b/deny.toml @@ -34,10 +34,10 @@ registries = [] # without turning the current lockfile into an opaque skip-list contract. multiple-versions = "warn" wildcards = "deny" -# Private workspace path dependencies are repository-owned source, not external -# version authority. Keep external wildcard requirements denied while allowing -# the unpublished commandf -> commandf-pkg path edge recorded by T020. -allow-wildcard-paths = true +# Keep the wildcard rule effective for every dependency, including private path +# dependencies. The repository-owned commandf -> commandf-pkg edge carries an +# exact =0.0.0 requirement in the manifest instead of relying on a global bypass. +allow-wildcard-paths = false highlight = "all" workspace-default-features = "allow" external-default-features = "allow" From 760421197b8630b2c8ead5c9d00aff933521c345 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:56:34 +0300 Subject: [PATCH 29/30] test(af01): lock path wildcard boundary --- ...test_audit_workflow_trust_af01_coverage.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/scripts/test_audit_workflow_trust_af01_coverage.py b/.github/scripts/test_audit_workflow_trust_af01_coverage.py index eb173b01..fcf41fc4 100644 --- a/.github/scripts/test_audit_workflow_trust_af01_coverage.py +++ b/.github/scripts/test_audit_workflow_trust_af01_coverage.py @@ -3,6 +3,7 @@ import importlib.util import sys +import tomllib import unittest from pathlib import Path @@ -27,6 +28,8 @@ ROOT = Path(__file__).resolve().parents[2] SECURITY_WORKFLOW = ROOT / ".github" / "workflows" / "af01-security.yml" +DENY_POLICY = ROOT / "deny.toml" +CLI_MANIFEST = ROOT / "crates" / "commandf-cli" / "Cargo.toml" class Af01SecurityCoverageTests(unittest.TestCase): @@ -82,6 +85,22 @@ def test_action_metadata_discovery_covers_both_supported_filenames(self) -> None self.assertEqual(workflows, [".github/workflows/af01-security.yml"]) self.assertEqual(actions, ["action.yml", "nested/action.yaml"]) + def test_private_path_dependency_has_no_global_wildcard_bypass(self) -> None: + deny_policy = tomllib.loads(DENY_POLICY.read_text(encoding="utf-8")) + self.assertFalse( + deny_policy["bans"].get("allow-wildcard-paths", False), + "private path dependencies must remain subject to the wildcard requirement policy", + ) + + manifest = tomllib.loads(CLI_MANIFEST.read_text(encoding="utf-8")) + commandf_pkg = manifest["dependencies"]["commandf-pkg"] + self.assertEqual(commandf_pkg["path"], "../commandf-pkg") + self.assertEqual( + commandf_pkg["version"], + "=0.0.0", + "the intended workspace edge must carry an exact version requirement instead of a global bypass", + ) + if __name__ == "__main__": unittest.main() From cc5607bff3e7c20a069e4f7d666005e74fe75b48 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Thu, 27 Aug 2026 10:57:24 +0300 Subject: [PATCH 30/30] docs(af01): record wildcard boundary remediation --- .../stack-b-dependency-inventory.md | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md index 6aca7c08..228f5ee9 100644 --- a/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md +++ b/specs/015-af-01-trusted-development-baseline/stack-b-dependency-inventory.md @@ -73,6 +73,19 @@ Regression coverage in `.github/scripts/test_summarize_cargo_metadata.py` includ The original Qodo threads became resolved/outdated only after the implementation changed. T029 still requires a fresh exact-head Qodo review; this remediation record is not a substitute for that review. +### Private path wildcard boundary and CodeRabbit remediation + +A fresh CodeRabbit review of previously qualified head `45629125e8e09f7d6cd6869fee82e4ae00f8483c` identified a high-severity policy-scope defect: `allow-wildcard-paths = true` was a global cargo-deny exception for private path dependencies, not an exception scoped only to the intended `commandf -> commandf-pkg` workspace edge. + +The finding was fixed rather than waived: + +- `deny.toml` now sets `allow-wildcard-paths = false`, so `wildcards = "deny"` applies to private path dependencies too; +- the intended repository-owned edge is declared as `commandf-pkg = { path = "../commandf-pkg", version = "=0.0.0" }`, giving it an exact version requirement rather than relying on a global bypass; +- `.github/scripts/test_audit_workflow_trust_af01_coverage.py` parses both `deny.toml` and the CLI manifest and fails if the global bypass is re-enabled or if the intended edge loses its exact version requirement; and +- no cargo-deny skip, waiver, alternate source authority, or wildcard exception was added. + +Because this remediation changed `Cargo.toml`, all prior T028 workflow evidence and review evidence for `45629125e8e09f7d6cd6869fee82e4ae00f8483c` is stale. The replacement head must be requalified from scratch, including every proof workflow that becomes path-applicable because `Cargo.toml` changed. + ## Observed license expressions The exact locked graph contains the following distinct dependency license expressions: @@ -139,11 +152,12 @@ T021 policy intent and implemented boundary: - keep duplicate versions visible and machine-diagnosable; - use `multiple-versions = "warn"` initially rather than creating opaque skip lists for the current graph; -- keep external wildcard dependency requirements fail-closed; -- permit the repository-owned unpublished workspace path edge with `allow-wildcard-paths = true` because the local path requirement has no registry version requirement to pin; +- keep wildcard dependency requirements fail-closed for registry, git, and private path dependencies; +- express the repository-owned unpublished `commandf -> commandf-pkg` path edge with the exact requirement `=0.0.0` instead of disabling wildcard enforcement for a class of dependencies; +- keep `allow-wildcard-paths = false`; and - do not add `skip` or `skip-tree` entries unless a later exact finding proves a narrow necessity and records a reason/revisit condition. -`allow-wildcard-paths = true` does not authorize wildcard version requirements for crates.io, git, or alternate registries. Source policy remains independently fail-closed. +This boundary means a future private workspace crate cannot acquire an unversioned path dependency merely because it is private. Source policy remains independently fail-closed. ## Source policy intent @@ -216,4 +230,4 @@ This observed baseline freezes the initial zizmor gate at `medium`; it does not `T020 = COMPLETE` -The current graph, source authority, license surface, duplicate families, resolved-edge representation, fail-closed inventory validation, and initial scanner calibration are documented. T021–T027 implement the checked-in dependency/workflow security gates and waiver/coverage policy. Final Stack B PASS remains governed by T028 exact-head workflow evidence and T029 exact-head independent reviews plus canonical merge truth. +The current graph, source authority, license surface, duplicate families, resolved-edge representation, fail-closed inventory validation, exact private-path wildcard boundary, and initial scanner calibration are documented. T021–T027 implement the checked-in dependency/workflow security gates and waiver/coverage policy. Final Stack B PASS remains governed by T028 exact-head workflow evidence and T029 exact-head independent reviews plus canonical merge truth.