diff --git a/.github/workflows/authcontract-gate.yml b/.github/workflows/authcontract-gate.yml index 0f53c6f..b2591e3 100644 --- a/.github/workflows/authcontract-gate.yml +++ b/.github/workflows/authcontract-gate.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout PR test-merge composition - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Default ref for a pull_request-triggered workflow is GitHub's # own ephemeral merge commit (refs/pull//merge, i.e. @@ -38,7 +38,7 @@ jobs: - name: Fetch current base run: git fetch origin "${{ github.event.pull_request.base.ref }}" - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acfd72d..8483c24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,16 @@ jobs: matrix: python-version: ["3.10", "3.12"] steps: - # Actions are pinned to immutable commit SHAs, not to mutable major tags: - # a tag can be repointed by its publisher, so `@v4` does not identify the - # code that will actually run. Each SHA below is the exact commit its - # named tag resolved to when it was pinned. - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - # -c constraints.txt fixes the resolved dependency versions, so CI - # measures the same dependency set the benchmarks were bound to. - - run: pip install -e ".[test]" -c constraints.txt + # The no-network reinstall later in `make ci` must find the declared build backend + # locally. Python 3.12+ runner virtual environments do not guarantee setuptools. + - run: python -m pip install "setuptools>=61,<85" + - run: python -m pip install -e ".[test]" -c constraints.txt - name: Record the exact installed dependency set run: pip freeze --exclude-editable - - run: pytest -q - - name: Public falsification harness + - run: make ci + - name: AC-039 public falsification harness run: python3 falsify.py diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d860808..57cd406 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,64 +1,45 @@ name: Security -# Automated dependency-advisory monitoring (AC-039). -# -# Audits the CONTROLLED dependency set in constraints.txt — the exact versions -# CI installs and the benchmarks are bound to — against the PyPI/OSV advisory -# databases via pip-audit, the PyPA-maintained tool for this ecosystem. -# -# The schedule matters: an advisory can be published against an unchanged -# dependency set, so a check that only ran on push would go stale silently. -# -# SCOPE, stated exactly: this audits THIRD-PARTY DEPENDENCIES only. It does not -# analyse AuthContract's own source for security defects, and a green run is -# not an audit. See SECURITY.md §5. - +# Automated dependency-advisory monitoring (AC-039). This workflow audits +# third-party dependencies only; a green run is not a security audit. on: push: pull_request: schedule: - # Weekly, so a newly published advisory against an unchanged dependency set - # surfaces without waiting for the next commit. - cron: "17 6 * * 1" permissions: contents: read jobs: - dependency-advisories: - name: Dependency advisory audit + dependency-review: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + + advisory-scan: + name: Dependency advisory audit and SBOM runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" - - name: Install the auditor at a pinned version run: pip install "pip-audit==2.10.1" - - name: Audit the controlled dependency set run: | - # --no-deps audits exactly the pinned closure in constraints.txt - # rather than re-resolving it, so the audited set is the installed - # set. --strict fails if any listed dependency could not be audited, - # so "no findings" cannot mean "nothing was checked". - # - # There is deliberately no severity threshold and no ignore list: - # ANY known advisory against a pinned dependency fails this job. That - # is stricter than the HIGH/CRITICAL floor it has to satisfy. A - # finding is never suppressed to make this workflow green — it is - # fixed by upgrading, or adjudicated as an explicit exception by a - # human, in the open. set +e pip-audit -r constraints.txt --no-deps --strict \ --format json --output pip-audit.json rc=$? set -e - echo "--- pip-audit report (inspectable evidence) ---" cat pip-audit.json - echo - echo "--- summary ---" python3 - <<'PY' import json, sys @@ -75,7 +56,6 @@ jobs: report = json.load(open("pip-audit.json")) deps = report.get("dependencies", []) audited = {norm(d["name"]) for d in deps} - findings = [d for d in deps if d.get("vulns")] print(f"pinned: {len(pins)} audited: {len(audited)}") for d in findings: @@ -83,12 +63,6 @@ jobs: fixed = ", ".join(v.get("fix_versions") or []) or "no fix listed" print(f"VULNERABLE {d['name']}=={d['version']} {v['id']} (fix: {fixed})") print(f"{len(findings)} dependency/dependencies with known advisories") - - # Coverage assertion. The auditor drops a pin it cannot resolve in the - # advisory service and still exits 0, so "no known vulnerabilities" - # can otherwise mean "this package was never checked". Treat an - # unaudited pin as a failure, not as a pass. There is deliberately no - # allowlist here: the fix is to pin a version with advisory coverage. unaudited = sorted(set(pins) - audited) if unaudited: print("UNAUDITED PINS (advisory coverage missing):") @@ -98,3 +72,7 @@ jobs: print("coverage OK: every pinned dependency was audited") PY exit $rc + - name: Install project for candidate SBOM + run: pip install -e ".[test]" -c constraints.txt + - name: Generate candidate SBOM + run: make sbom diff --git a/.gitignore b/.gitignore index 4e33d94..29743a4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ *.pyc *.egg-info/ .pytest_cache/ +.venv/ +build/ diff --git a/AGENTS.md b/AGENTS.md index feb539f..7af9a4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,20 @@ disposition, exit `0`. Run this rather than assuming the refusal paths work. **Not on PyPI.** `pip install authcontract` will not work. Install from source only. +## 4.1 Real repository skills + +Agents may use only these implemented workflows: + +- `make test` — execute the committed test suite; +- `make falsify` — exercise the bounded PASS/refusal/tamper cases; +- `make no-network` — run the local install/import/test/CLI network guard; +- `make sbom` — generate the bounded candidate SBOM; and +- the six CLI commands listed below. + +None of these commands publishes, deploys, attests a release, or adjudicates its +own result. Producers must state `NOT SELF-ADJUDICATED` and stop for independent +verification. + ## 5. Supported CLI commands Exactly six. Any other subcommand does not exist. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fe8d2b..068014c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,55 +2,39 @@ ## Current status: no unsolicited contribution workflow is established -This is stated plainly rather than left ambiguous, because an unstated process -wastes a contributor's time. - -There is **no** established process for unsolicited pull requests. Specifically, -none of the following exists: a contributor licence agreement, a review-time -commitment, a maintainer rota, a triage service level, a governance model, a -code of conduct process, or a merge policy for outside contributions. - -Nothing here promises any of those will exist. This is an experimental -reference implementation maintained as research and engineering evidence, not a -community-maintained project. - -**Contribution is also constrained by licensing.** No license is declared (see -[`README.md`](README.md) § License), so the terms under which a contribution -could be accepted and redistributed are themselves unsettled. That is an owner -decision, not a process gap someone can work around. +There is no established process for unsolicited pull requests, contributor +licence agreement, review-time commitment, maintainer rota, governance model, +or merge policy for outside contributions. AuthContract is experimental +research and engineering evidence, not a community-maintained project. No +license is declared, so contribution acceptance and redistribution terms are +also unsettled owner decisions. ## What is genuinely useful right now -**Open an issue.** Issues are read, and they are the reliable path. - -The most valuable thing you can send is a **falsification**: a case where -AuthContract's documented behaviour and its actual behaviour disagree. +Open an issue first. Include the exact commit SHA, OS, Python version, and a +minimal reproducer against the committed synthetic fixtures. Falsifications of +documented behaviour are especially useful: -- Run `python3 falsify.py` — the public falsification harness — and include its - output if a case failed. -- Include your OS, Python version, and the exact commit SHA. -- Reproduce against the committed fixtures in `fixtures/` where possible. No - credentials or network are needed. -- If a claim in `README.md`, `AGENTS.md`, or any document under `docs/` does not - hold, say which sentence and what you observed instead. A documented claim - that turns out to be false is a defect, and it is recorded rather than quietly - edited away. +- `make ci` runs the full producer verification surface; +- `make falsify` exercises the bounded Wave 1 harness; and +- `python3 falsify.py` exercises the AC-039 public harness. -For a **suspected security vulnerability**, do not open a public issue — follow -[`SECURITY.md`](SECURITY.md) instead. +For suspected vulnerabilities, do not open a public issue; follow +[`SECURITY.md`](SECURITY.md). -## Before writing code +If a code change is agreed, create a focused pull request rather than pushing +to `main`, include a regression test, preserve negative tests, report literal +command output at the exact commit, identify producer and proposed independent +verifier, classify claims as proved, measured, argued, or assumed, and state +`NOT SELF-ADJUDICATED`. -**Open an issue first.** A pull request that arrives without prior discussion -may sit unreviewed, and given the licensing situation above it may not be -mergeable at all. That is a genuine risk of wasted effort, so it is said up -front rather than discovered afterwards. +AI-assisted contributions should add these trailers when applicable: -If a change is agreed, the practical expectations are the same ones this -repository applies to itself: +```text +Agent-Assisted-By: +Veraxis-Skill: +Agent-Execution-ID: +``` -- The full suite passes: `pytest -q` → 342 passed. -- The falsification harness passes: `python3 falsify.py`. -- A fix comes with a regression test that **fails without the fix**. -- Tests are not weakened, skipped, or deleted to make CI green. -- No claim is added that the repository's own measurements do not support. +Trailers are supplemental provenance. They do not establish authorship, +authority, independent verification, acceptance, or a licence grant. diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000..c29e202 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,24 @@ +# Dependency policy + +AuthContract has one runtime dependency: `rfc8785`, which supplies the JSON +Canonicalization Scheme used in identity-bearing digests. Because a +canonicalization change can change artifact identity, the supported range is +bounded to the audited `0.1` line: `>=0.1.2,<0.2`. + +The test extra bounds pytest to `>=7,<10`. Build tooling is declared separately +in `pyproject.toml`; it is not a runtime dependency. + +This repository uses ranges for compatibility testing rather than claiming a +single universal lock across Python 3.10 and 3.12. Evidence must record the +resolved environment for the exact run. Pull requests receive dependency-diff +review and an advisory scan through `.github/workflows/security.yml`. +The scan upgrades its own `pip` environment before auditing and skips the local +editable AuthContract package, which is not a published PyPI dependency. + +`make sbom` records the installed AuthContract and runtime dependency versions +in a deterministic CycloneDX document for the current candidate. It does not +attest a release, include operating-system packages, or establish that a +dependency is vulnerability-free. + +Dependency updates that could affect canonical bytes, digests, exit behavior, +or receipt verification require the positive and negative verification suite. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..195edb1 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: test falsify no-network sbom ci + +PYTHON ?= python3 + +test: + $(PYTHON) -m pytest -q + +falsify: + $(PYTHON) scripts/falsify.py + +no-network: + $(PYTHON) scripts/verify_no_network.py + +sbom: + $(PYTHON) scripts/generate_sbom.py --output build/authcontract.cdx.json + +ci: test falsify no-network sbom + diff --git a/README.md b/README.md index 26c0497..7bd44d2 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ pip install -e ".[test]" Confirm the install: ```bash -pytest -q # expect: 342 passed +make test # expect: 342 passed +make falsify # expect: 4/4 bounded outcomes observed ``` > Not on PyPI. Install from source, as above. @@ -300,6 +301,9 @@ Measured evidence and its limits: [`docs/BENCHMARKS-AC-039.md`](docs/BENCHMARKS- | Report a suspected vulnerability | [`SECURITY.md`](SECURITY.md) | | Understand the contribution situation | [`CONTRIBUTING.md`](CONTRIBUTING.md) | | Understand the terminology | [`docs/DEVELOPER-LANGUAGE.md`](docs/DEVELOPER-LANGUAGE.md) | +| Review dependency and SBOM policy | [`DEPENDENCIES.md`](DEPENDENCIES.md) | +| Review security reporting | [`SECURITY.md`](SECURITY.md) | +| Review compatibility policy | [`VERSIONING.md`](VERSIONING.md) | | See how this compares to other systems | [`docs/SOTA.md`](docs/SOTA.md) | | Understand the full conceptual model | keep reading below | | Use AuthContract from an AI coding agent | [`AGENTS.md`](AGENTS.md) | @@ -318,7 +322,8 @@ Measured evidence and its limits: [`docs/BENCHMARKS-AC-039.md`](docs/BENCHMARKS- contributor licence or review policy exists. Issues are the reliable path today. If you are considering a substantive contribution, open an issue first so it isn't wasted effort. [`CONTRIBUTING.md`](CONTRIBUTING.md) states exactly what -does and does not exist. +does and does not exist, including evidence, role-separation, and +agent-provenance expectations for an owner-agreed change. **Security.** Do not report a suspected vulnerability through a public issue. [`SECURITY.md`](SECURITY.md) sets out the triage policy and the current state of diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..e173d48 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,46 @@ +# Versioning and compatibility + +AuthContract is currently version `0.0.1`: experimental and pre-1.0. The +repository has no published package or release compatibility promise. Pin an +exact commit SHA when reproducing or integrating it. + +## Versioned surfaces + +The following are public integration surfaces at a pinned commit, but may +change incompatibly before 1.0: + +- the six CLI subcommands and their flags; +- exit codes (`0` for PASS/ALLOW, `1` for refusal or error); +- JSON `status`, `decision`, and `reason_code` values; +- the Python functions and result objects documented in `AGENTS.md`; +- contract artifact, runtime-fact, action, and receipt JSON structures; and +- canonicalization and digest rules used to bind those structures. + +Human-readable messages are never a compatibility interface. Consumers should +branch on structured fields, while still pinning the exact commit because the +reason-code set is not yet frozen. + +## Change rules + +Before 1.0, a change to a CLI name or flag, exit semantics, reason code, Python +signature, required JSON field, canonicalization rule, or digest scope is a +breaking change. Such a change must: + +1. be explicit in the pull request and documentation; +2. update positive and negative fixtures and tests; +3. identify affected receipts and artifacts; +4. avoid silently reinterpreting an existing digest; and +5. use a new artifact/schema/version identity when old and new bytes could + otherwise be confused. + +Adding an optional field is compatible only when older consumers safely ignore +it and its presence cannot widen authority. A new refusal condition is treated +as behaviorally consequential even when it fails closed. + +## Releases and artifacts + +No distributable release or attestation is established. Source installs are the +only supported installation route. An SBOM may be generated for a candidate +commit with `make sbom`; it is evidence about resolved package metadata, not a +release attestation or security guarantee. + diff --git a/docs/AGENT-OBSERVABILITY.md b/docs/AGENT-OBSERVABILITY.md new file mode 100644 index 0000000..c7d832e --- /dev/null +++ b/docs/AGENT-OBSERVABILITY.md @@ -0,0 +1,46 @@ +# Agent observability and provenance + +AuthContract has no telemetry client, analytics endpoint, hosted agent gateway, +or MCP server. Local reads and reasoning are dark unless a contributor records +them; this repository must not imply otherwise. + +## Observable events + +GitHub can attribute commits, pull requests, reviews, comments, checks, and +workflow runs to the authenticated GitHub actor. Those events establish +transport attribution only. They do not prove that an actor held institutional +authority, that a model performed the work claimed, or that a review was +independent. + +Contributors may add commit trailers: + +```text +Agent-Assisted-By: +Veraxis-Skill: +Agent-Execution-ID: +``` + +Trailers are supplemental provenance, not authorization or adjudication. +Producer, verifier, and adjudicator must remain distinct. + +## Dark local activity + +File reads, prompts, local model reasoning, and commands outside an attributable +system are not observable from Git history. Do not reconstruct or claim those +events without literal evidence. A missing trailer does not prove that no agent +was used; a trailer does not prove the described execution occurred. + +## No outbound analytics + +Run `make no-network` to install the local project without an index and exercise +package import, the full tests, and a CLI command with Python socket connections +blocked. A PASS is bounded to the exercised Python processes at the tested +commit. It does not inspect operating-system traffic from unrelated tools or +prove facts about a future binary. + +## Unimplemented remote surfaces + +A hosted AuthContract verifier, Veraxis gateway, remote context service, and MCP +server are **NOT IMPLEMENTED** in this repository. No URL, execution identifier, +or remote-observability claim should be invented for them. + diff --git a/docs/SDLC-V1.2-STATUS.md b/docs/SDLC-V1.2-STATUS.md new file mode 100644 index 0000000..9ec9cd4 --- /dev/null +++ b/docs/SDLC-V1.2-STATUS.md @@ -0,0 +1,24 @@ +# SDLC v1.2 producer status + +Baseline: `ce783851897b8ddbbe92fae2b098b8bee8e88f57`. + +This matrix uses the canonical public-release gate definitions in owner-authorized +`CURRENT-SDLC.md` v1.2. It reports literal producer evidence, does not accept its own +claims, and is **NOT SELF-ADJUDICATED**. + +| Gate | Canonical gate | Disposition | Literal evidence / limitation | +|---|---|---|---| +| E | Human Repository Usability | PASS | The README first screen states purpose and maturity and provides copy/paste clean-clone, install, meaningful valid/refusal CLI, expected output, integration, and boundary paths. `make ci` exercises the documented implementation. | +| F | Agent Usability | PASS | `AGENTS.md` gives real install, verification, falsification, CLI, evidence-reading, and boundary instructions without inventing interfaces. | +| G | Adoption Readiness | NOT ESTABLISHED | The README provides a truthful first-run and integration surface, but no adoption/conversion result is established; no star prompt or repository CTA is treated as adoption proof. | +| H | Supply-Chain & Release Integrity | PASS | Consequential Actions are immutable-SHA pinned; PR dependency review, advisory scanning, and SBOM generation run in CI. No package/release artifact is published, so artifact digest, provenance, and attestation are not applicable to the current source-only state and are not claimed. | +| I | Security & Vulnerability Management | NOT ESTABLISHED | `SECURITY.md` states supported scope, triage expectations, scanner limits, and that a verified private disclosure route is not established. Dependency review and `pip-audit` provide bounded dependency evidence; a scanner result is not represented as an audit. | +| J | API & Versioning Integrity | PASS | `VERSIONING.md` declares the pre-1.0 Python API/import, CLI, exit, reason-code, artifact, receipt, and compatibility surfaces. | +| K | Machine-Readable Discovery & Licensing | NOT ESTABLISHED | `pyproject.toml` provides truthful package metadata, but the repository grants no license and declares no SPDX license identity. No grant is invented. | +| L | Public Falsification Completeness | PASS | `make falsify` publicly exercises one valid decision and three meaningful refusal/tamper paths: unclassified action, stale fact, and receipt mismatch (4/4). | +| M | Agent Interaction Observability | NOT ESTABLISHED | `docs/AGENT-OBSERVABILITY.md` truthfully documents GitHub-attributable versus dark local activity and the no-hidden-telemetry/no-network boundary. No approved ingestion pipeline, hosted gateway, or MCP observability implementation is established. | + +## Independent Adjudication + +Independent Adjudication remains pending for the designated independent reviewer and owner. +GitHub CI success is evidence, not acceptance. **CI GREEN IS NOT ACCEPTANCE.** diff --git a/pyproject.toml b/pyproject.toml index 49484b8..6e4d9e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "authcontract" version = "0.0.1" description = "AuthContract reference implementation — canonical digest and runtime fact admissibility" requires-python = ">=3.10" -dependencies = ["rfc8785>=0.1.2"] +dependencies = ["rfc8785>=0.1.2,<0.2"] # Machine-readable licensing state (AC-039). # @@ -29,7 +29,7 @@ classifiers = [ authcontract = "authcontract.cli:main" [project.optional-dependencies] -test = ["pytest>=7.0"] +test = ["pytest>=7.0,<10"] [tool.setuptools.packages.find] include = ["authcontract*"] diff --git a/scripts/falsify.py b/scripts/falsify.py new file mode 100644 index 0000000..bbf96a8 --- /dev/null +++ b/scripts/falsify.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Run the bounded public AuthContract falsification cases.""" + +from __future__ import annotations + +import json +import subprocess +import sys +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def run_case(name: str, args: list[str], exit_code: int, status: str, reason: str) -> None: + result = subprocess.run( + [sys.executable, "-m", "authcontract", *args], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + try: + payload = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise AssertionError(f"{name}: stdout was not JSON: {result.stdout!r}") from exc + actual = (result.returncode, payload.get("status"), payload.get("reason_code")) + expected = (exit_code, status, reason) + if actual != expected: + raise AssertionError(f"{name}: expected {expected!r}, got {actual!r}; stderr={result.stderr!r}") + print(f"PASS {name}: exit={exit_code} status={status} reason_code={reason}") + + +def main() -> int: + artifact = "fixtures/banking_payment_specimen.json" + valid_action = "fixtures/actions/send_payment_valid.json" + valid_facts = "fixtures/runtime/facts_valid.json" + + run_case( + "valid decision", + ["run-specimen", artifact, valid_action, valid_facts, "--execution-result", "SIMULATED_SUCCESS"], + 0, + "PASS", + "OK", + ) + run_case( + "unclassified-action refusal", + [ + "run-specimen", + artifact, + "fixtures/actions/send_payment_unknown_action_type.json", + valid_facts, + "--execution-result", + "SIMULATED_SUCCESS", + ], + 1, + "REFUSED", + "RUN_UNCLASSIFIED_ACTION", + ) + run_case( + "stale-fact refusal", + [ + "run-specimen", + artifact, + valid_action, + "fixtures/runtime/facts_stale.json", + "--execution-result", + "SIMULATED_SUCCESS", + ], + 1, + "REFUSED", + "RUN_FACT_STALE", + ) + + receipt = json.loads((ROOT / "fixtures/runtime/receipt_valid.json").read_text()) + receipt["decision"] = "REFUSE" + with tempfile.TemporaryDirectory(prefix="authcontract-falsify-") as temp_dir: + tampered = Path(temp_dir) / "receipt-tampered.json" + tampered.write_text(json.dumps(receipt), encoding="utf-8") + run_case( + "receipt-tamper refusal", + ["verify-receipt", str(tampered), artifact, valid_action, valid_facts], + 1, + "REFUSED", + "VEIP_RECEIPT_MISMATCH", + ) + + print("PASS bounded falsification harness: 4/4 expected outcomes observed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/generate_sbom.py b/scripts/generate_sbom.py new file mode 100644 index 0000000..4c9473d --- /dev/null +++ b/scripts/generate_sbom.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Generate a deterministic, bounded CycloneDX SBOM from installed metadata.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.metadata +import json +from pathlib import Path + + +def component(name: str) -> dict[str, object]: + distribution = importlib.metadata.distribution(name) + canonical = distribution.metadata["Name"] or name + version = distribution.version + return { + "type": "library", + "bom-ref": f"pkg:pypi/{canonical.lower()}@{version}", + "name": canonical, + "version": version, + "purl": f"pkg:pypi/{canonical.lower()}@{version}", + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + components = sorted([component("authcontract"), component("rfc8785")], key=lambda item: str(item["name"])) + serial_material = json.dumps(components, sort_keys=True, separators=(",", ":")).encode() + serial = hashlib.sha256(serial_material).hexdigest() + document = { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": f"urn:uuid:{serial[:8]}-{serial[8:12]}-{serial[12:16]}-{serial[16:20]}-{serial[20:32]}", + "version": 1, + "metadata": {"component": next(item for item in components if item["name"] == "authcontract")}, + "components": components, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"wrote {args.output} with {len(components)} components") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/no_network/sitecustomize.py b/scripts/no_network/sitecustomize.py new file mode 100644 index 0000000..dbb35cb --- /dev/null +++ b/scripts/no_network/sitecustomize.py @@ -0,0 +1,15 @@ +"""Fail closed if Python code attempts an outbound socket connection.""" + +from __future__ import annotations + +import socket + + +def _blocked(*_args: object, **_kwargs: object) -> None: + raise RuntimeError("outbound network disabled by AuthContract verification guard") + + +socket.create_connection = _blocked # type: ignore[assignment] +socket.socket.connect = _blocked # type: ignore[assignment] +socket.socket.connect_ex = _blocked # type: ignore[assignment] + diff --git a/scripts/verify_no_network.py b/scripts/verify_no_network.py new file mode 100644 index 0000000..8b6b091 --- /dev/null +++ b/scripts/verify_no_network.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Exercise install/import/tests/CLI with outbound Python sockets blocked.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +GUARD = ROOT / "scripts/no_network" + + +def run(label: str, command: list[str]) -> None: + env = os.environ.copy() + env["PYTHONPATH"] = str(GUARD) + os.pathsep + env.get("PYTHONPATH", "") + result = subprocess.run(command, cwd=ROOT, env=env, check=False) + if result.returncode != 0: + raise SystemExit(f"FAIL {label}: exit {result.returncode}") + print(f"PASS {label}: no outbound Python socket used") + + +def main() -> int: + run( + "offline editable install", + [sys.executable, "-m", "pip", "install", "--no-index", "--no-deps", "--no-build-isolation", "-e", "."], + ) + run("package import", [sys.executable, "-c", "import authcontract"]) + run("test suite", [sys.executable, "-m", "pytest", "-q"]) + run("CLI", [sys.executable, "-m", "authcontract", "verify", "fixtures/valid.json"]) + print("PASS no-network guard: install/import/tests/CLI completed without outbound Python sockets") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +