personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(release): non-destructive, mirror-bound, signed-packet make audit

Replaces the GitHub-DB-refreshing make audit with a stable, non-destructive audit: verifies a minisign-signed freshness receipt against pinned sol trust (key ID + public-key SHA-256), materializes the RustSec advisory DB only from a locally-supplied git bundle at the receipt's signed commit, and runs the pinned cargo-deny advisory check fully offline against that isolated DB, emitting exactly one JSON success witness.

Four explicit operator inputs (AUDIT_ADVISORY_BUNDLE/RECEIPT/PUBKEY/LOCATOR); signature is derived as the adjacent <receipt>.minisig; locator is used only as cargo-deny's db-urls identity and never contacted; no GitHub fallback.

New authority module scripts/advisory_mirror_audit.py reuses the release-advisory isolation, minisign, and redaction primitives via import; the release-candidate prepare_policy_run path, PolicyRun/ledger schemas, and the 24h/14d rules are unchanged (regression-covered).

Recipe keeps the cargo-deny + minisign preflights, redirects preflight stdout to stderr, and suppresses command echo so make audit stdout is exactly the witness JSON and the private locator is never printed.

Adds a deterministic 113-test suite (fake tools/packets, real-git bundle materialization), rewrites the audit recipe contract test, and reconciles the now-false release_advisory_policy docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

+2380 -18
+2 -3
Makefile
··· 173 173 174 174 audit: 175 175 @$(REQUIRE_CARGO) 176 - @python3 scripts/check_release_preflight.py cargo-deny 177 - @cargo deny --manifest-path $(RUST_MANIFEST) fetch db || { echo "ERROR: RustSec advisory refresh failed; no current advisory result was produced. Restore network access and rerun 'make audit'." >&2; exit 1; } 178 - cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline check advisories 176 + @python3 scripts/check_release_preflight.py cargo-deny >&2 177 + @python3 scripts/advisory_mirror_audit.py --bundle "$(AUDIT_ADVISORY_BUNDLE)" --receipt "$(AUDIT_ADVISORY_RECEIPT)" --pubkey "$(AUDIT_ADVISORY_PUBKEY)" --locator "$(AUDIT_ADVISORY_LOCATOR)" 179 178 180 179 # Setup skill symlinks 181 180 skills:
+35 -1
docs/PORTING.md
··· 43 43 | Rust lint | `make check-rust-clippy` | GNU-host check | Runs the existing clippy `-D warnings` gate. | 44 44 | Rust tests | `make check-rust-test` | GNU-host check | Runs workspace Rust tests on the GNU host. | 45 45 | Rust dependency policy | `make check-rust-deny` | GNU-host check | Locked, offline bans/licenses/sources policy over the supported cargo-deny graph. | 46 - | Rust advisories | `make audit` | GNU-host check | Refreshes the advisory DB, then performs a locked offline advisory check. | 46 + | Rust advisories | `make audit` | GNU-host check | Verifies a signed advisory mirror packet, materializes its bundle locally, then performs a locked offline advisory check without refreshing or mutating the operator inputs. | 47 47 | iOS canary | `make check-rust-ios` | iOS cross-target canary | Cross-target drift evidence for eligible library crates; explicitly excludes `solstone-core-indexer-store` because the native SQLite store is not yet in the iOS gate. | 48 48 | Release candidate rail | `scripts/release.sh --candidate` / `scripts/release.sh --recover <version> <source-commit>` | Local readiness evidence | DESTRUCTIVE: `--candidate` is fresh construction; before policy or build work it deletes prior raw build/dist outputs and that version's stale payload/evidence. It binds candidate payload, ledger, and per-target install/smoke proofs, then reports canonical local readiness JSON. `--recover` is retained-byte-only, read-only validation; it preserves retained payload, ledger, and proofs and never rebuilds or refreshes. Proofs cover local candidate bytes and native smoke only; publication is temporarily locked out of this rail. | 49 + 50 + ### Signed Advisory Mirror Audit 51 + 52 + `make audit` requires four operator-provided inputs: `AUDIT_ADVISORY_BUNDLE` 53 + for the local advisory bundle, `AUDIT_ADVISORY_RECEIPT` for the freshness 54 + receipt, `AUDIT_ADVISORY_PUBKEY` for the approved minisign public key, and 55 + `AUDIT_ADVISORY_LOCATOR` for the private mirror locator. The signature selector 56 + is derived from the receipt path as `<receipt>.minisig`; there is no separate 57 + signature option. 58 + 59 + The audit is local-only. Git verifies and clones only the local bundle file, and 60 + the locator is used only as cargo-deny's advisory database identity in the 61 + offline check. It is never used as a clone source, fetched, pulled, or probed. 62 + Use a placeholder such as `PRIVATE_MIRROR_LOCATOR` in notes and logs; do not 63 + record a real private host, path, credential, or URL-derived token. 64 + 65 + The public trust pins are key ID `5FCC81CD3DE12315` and public-key SHA-256 66 + `c9fb713fe57791afbdebddde7b334e950ce1efcc167d49daf4cc1cbd930bb122`. The 67 + receipt must be canonical JSON, its adjacent minisign signature must carry the 68 + trusted comment for the same advisory commit and UTC time, and the receipt UTC 69 + is the only freshness authority. 70 + 71 + On success, stdout is exactly one compact JSON object with these fields: 72 + `product`, `advisory_cohort`, `synced_commit`, `receipt_utc`, `max_age`, 73 + `checked_at`, `cargo_lock_sha256`, `cargo_deny_version`, and `verdict`. 74 + The witness contains no paths, locators, credentials, or child process output. 75 + 76 + The audit is non-destructive: packet inputs, the source tree, ambient Cargo 77 + state, and release candidate/evidence directories are not modified. The 78 + bundle-cloned advisory database and cargo-deny config are owned temporary 79 + materialization and are removed before the success witness is emitted. 80 + If any gate fails, reacquire the signed packet from the controlled mirror 81 + process, place the adjacent signature next to the receipt, verify the public key 82 + pin, and rerun `make audit`. 49 83 50 84 ## Owner Timezone 51 85
+1079
scripts/advisory_mirror_audit.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: AGPL-3.0-only 3 + # Copyright (c) 2026 sol pbc 4 + 5 + """Signed-packet advisory mirror audit for ``make audit``.""" 6 + 7 + from __future__ import annotations 8 + 9 + import argparse 10 + import base64 11 + import hashlib 12 + import json 13 + import os 14 + import re 15 + import shutil 16 + import subprocess 17 + import sys 18 + from collections.abc import Callable, Mapping, Sequence 19 + from dataclasses import dataclass 20 + from datetime import datetime, timedelta 21 + from pathlib import Path 22 + from urllib.parse import urlparse 23 + 24 + ROOT = Path(__file__).resolve().parent.parent 25 + _SCRIPTS_DIR = Path(__file__).resolve().parent 26 + for _path in (str(ROOT), str(_SCRIPTS_DIR)): 27 + if _path not in sys.path: 28 + sys.path.insert(0, _path) 29 + 30 + from scripts.check_rust_release_manifest import ( # noqa: E402 31 + CONTROL_RE, 32 + RAW_ENV_RE, 33 + SECRET_RE, 34 + SHA256_RE, 35 + Failure, 36 + validate_public_evidence_text, 37 + ) 38 + from scripts.release_advisory_policy import ( # noqa: E402 39 + ADVISORY_TABLE_RE, 40 + ReleasePolicyError, 41 + _assert_scanned_snapshot, 42 + _cleanup_temp, 43 + _combined_release_policy_error, 44 + _count_advisories, 45 + _default_temp_path_factory, 46 + _failure, 47 + _format_utc, 48 + _parse_utc, 49 + _scanned_advisory_db, 50 + _toml_string, 51 + _unlink_path, 52 + _utc_now, 53 + _validate_advisory_count, 54 + _validate_source, 55 + advisory_check_argv, 56 + is_normalized_utc_timestamp, 57 + ) 58 + from scripts.release_tool_pins import CARGO_DENY_VERSION # noqa: E402 59 + from scripts.transparency_signing import ( # noqa: E402 60 + DriverError, 61 + LocalMinisignSigner, 62 + TransparencySigner, 63 + check_minisign_binary, 64 + ) 65 + 66 + Runner = Callable[..., subprocess.CompletedProcess[str]] 67 + Clock = Callable[[], datetime] 68 + TempPathFactory = Callable[[str], Path] 69 + PathRemover = Callable[[Path], None] 70 + 71 + PINNED_KEY_ID = "5FCC81CD3DE12315" 72 + PINNED_PUBKEY_SHA256 = ( 73 + "c9fb713fe57791afbdebddde7b334e950ce1efcc167d49daf4cc1cbd930bb122" 74 + ) 75 + ADVISORY_COHORT_ID = "sol-controlled-rustsec-mirror-v1" 76 + TRUSTED_COMMENT_SCHEME = "solpbc-advisory-mirror-v1" 77 + RECEIPT_MAX_AGE = 86400 78 + MAX_CLOCK_SKEW = timedelta(minutes=5) 79 + 80 + PRODUCT = "solstone-journal" 81 + ADVISORY_LOCATOR_TERMINALS = frozenset({"advisory-db", "rustsec-advisory-db.git"}) 82 + GIT_COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") 83 + KEY_ID_RE = re.compile(r"^[0-9A-F]{16}$") 84 + SCP_LOCATOR_RE = re.compile(r"(?:[^@:/]+@)?(?P<host>[^:/]+):(?P<path>[^:]+)") 85 + ABSOLUTE_PATH_RE = re.compile( 86 + r"(^|\s)(?:/[^ \t\r\n]+|~[^ \t\r\n]*|[A-Za-z]:[\\/][^ \t\r\n]*)" 87 + ) 88 + EMAIL_RE = re.compile(r"(?i)[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}") 89 + UUID_RE = re.compile( 90 + r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b" 91 + ) 92 + PRIVATE_HOST_RE = re.compile(r"(?i)\b(?:localhost|[A-Za-z0-9-]+\.local)\b") 93 + IP_CANDIDATE_RE = re.compile( 94 + r"\b(?:\d{1,3}\.){3}\d{1,3}\b|\b[0-9a-f:]*:[0-9a-f:]+\b", 95 + re.IGNORECASE, 96 + ) 97 + CONTROL_EXCEPT_NEWLINE_RE = re.compile(r"[\x00-\x09\x0b-\x1f\x7f]") 98 + 99 + AUDIT_REPAIR = "reacquire the signed advisory mirror packet and rerun make audit" 100 + INPUT_REPAIR = ( 101 + "set AUDIT_ADVISORY_BUNDLE, AUDIT_ADVISORY_RECEIPT, " 102 + "AUDIT_ADVISORY_PUBKEY, and AUDIT_ADVISORY_LOCATOR" 103 + ) 104 + 105 + 106 + @dataclass(frozen=True) 107 + class ReceiptAuthority: 108 + synced_commit: str 109 + utc: str 110 + max_age: int 111 + canonical_bytes: bytes 112 + trusted_comment: str 113 + 114 + 115 + @dataclass(frozen=True) 116 + class PublicKeyMinisignVerifier: 117 + public_key: Path 118 + minisign: str = "minisign" 119 + 120 + def check(self) -> None: 121 + check_minisign_binary(self.minisign) 122 + 123 + def sign_file( 124 + self, 125 + message_path: Path, 126 + signature_path: Path, 127 + *, 128 + trusted_comment: str, 129 + ) -> None: 130 + raise NotImplementedError("advisory mirror audit verifier cannot sign") 131 + 132 + def verify_file( 133 + self, 134 + message_path: Path, 135 + signature_path: Path, 136 + *, 137 + expected_trusted_comment: str, 138 + ) -> None: 139 + # LocalMinisignSigner.verify_file never reads the secret key; check() only 140 + # performs the minisign 0.12 binary preflight and never requires a secret key. 141 + LocalMinisignSigner( 142 + secret_key=Path("__verify_only_unused_secret__"), 143 + public_key=self.public_key, 144 + minisign=self.minisign, 145 + ).verify_file( 146 + message_path, 147 + signature_path, 148 + expected_trusted_comment=expected_trusted_comment, 149 + ) 150 + 151 + def trusted_comment(self, signature_path: Path) -> str: 152 + return LocalMinisignSigner( 153 + secret_key=Path("__verify_only_unused_secret__"), 154 + public_key=self.public_key, 155 + minisign=self.minisign, 156 + ).trusted_comment(signature_path) 157 + 158 + 159 + def _failure_record( 160 + error: str, 161 + *, 162 + expected: str, 163 + actual: str, 164 + repair: str = AUDIT_REPAIR, 165 + ) -> Failure: 166 + return _failure(error, expected=expected, actual=actual, repair=repair) 167 + 168 + 169 + def _raise_one( 170 + error: str, 171 + *, 172 + expected: str, 173 + actual: str, 174 + repair: str = AUDIT_REPAIR, 175 + ) -> None: 176 + raise ReleasePolicyError( 177 + [_failure_record(error, expected=expected, actual=actual, repair=repair)] 178 + ) 179 + 180 + 181 + def _redact_child_output(text: str, *, secrets: set[str]) -> str: 182 + redacted = text 183 + for secret in sorted((item for item in secrets if item), key=len, reverse=True): 184 + redacted = redacted.replace(secret, "<redacted>") 185 + redacted = RAW_ENV_RE.sub("<redacted-env>", redacted) 186 + redacted = SECRET_RE.sub("<redacted-secret>", redacted) 187 + redacted = ABSOLUTE_PATH_RE.sub( 188 + lambda match: f"{match.group(1)}<redacted-path>", redacted 189 + ) 190 + redacted = EMAIL_RE.sub("<redacted-email>", redacted) 191 + redacted = UUID_RE.sub("<redacted-uuid>", redacted) 192 + redacted = PRIVATE_HOST_RE.sub("<redacted-host>", redacted) 193 + redacted = IP_CANDIDATE_RE.sub("<redacted-ip>", redacted) 194 + redacted = CONTROL_EXCEPT_NEWLINE_RE.sub("?", redacted) 195 + if validate_public_evidence_text("child-output", redacted): 196 + return "<redacted child output>" 197 + return redacted 198 + 199 + 200 + def _convert_driver_error(exc: DriverError, *, secrets: set[str]) -> ReleasePolicyError: 201 + failures: list[Failure] = [] 202 + for failure in exc.failures: 203 + failures.append( 204 + _failure_record( 205 + failure.error, 206 + expected=failure.expected, 207 + actual=_redact_child_output(failure.actual, secrets=secrets), 208 + repair=failure.repair, 209 + ) 210 + ) 211 + return ReleasePolicyError(failures) 212 + 213 + 214 + def _safe_failure(failure: Failure) -> Failure: 215 + text = ( 216 + f"ERROR: {failure.error}\n" 217 + f"expected: {failure.expected}\n" 218 + f"actual: {failure.actual}\n" 219 + f"repair: {failure.repair}\n" 220 + ) 221 + if not validate_public_evidence_text("failure", text): 222 + return failure 223 + return _failure_record( 224 + failure.error, 225 + expected=failure.expected, 226 + actual="redacted", 227 + repair=failure.repair, 228 + ) 229 + 230 + 231 + def _format_failures(failures: Sequence[Failure]) -> None: 232 + for failure in failures: 233 + safe = _safe_failure(failure) 234 + print(f"ERROR: {safe.error}", file=sys.stderr) 235 + print(f" expected: {safe.expected}", file=sys.stderr) 236 + print(f" actual: {safe.actual}", file=sys.stderr) 237 + print(f" repair command: {safe.repair}", file=sys.stderr) 238 + 239 + 240 + def _validate_regular_input(path: Path, *, label: str) -> list[Failure]: 241 + if path.is_symlink(): 242 + return [ 243 + _failure_record( 244 + f"advisory mirror input {label} is unsafe", 245 + expected=f"{label} regular file, not a symlink", 246 + actual="symlink", 247 + repair=INPUT_REPAIR, 248 + ) 249 + ] 250 + if not path.exists(): 251 + return [ 252 + _failure_record( 253 + f"advisory mirror input {label} is missing", 254 + expected=f"{label} existing regular file", 255 + actual="missing", 256 + repair=INPUT_REPAIR, 257 + ) 258 + ] 259 + if not path.is_file(): 260 + return [ 261 + _failure_record( 262 + f"advisory mirror input {label} is unsafe", 263 + expected=f"{label} regular file", 264 + actual="not a regular file", 265 + repair=INPUT_REPAIR, 266 + ) 267 + ] 268 + return [] 269 + 270 + 271 + def _receipt_signature_path(receipt: Path) -> Path: 272 + return receipt.parent / f"{receipt.name}.minisig" 273 + 274 + 275 + def _validate_inputs( 276 + *, 277 + bundle: Path, 278 + receipt: Path, 279 + pubkey: Path, 280 + ) -> tuple[Path, list[Failure]]: 281 + signature = _receipt_signature_path(receipt) 282 + failures: list[Failure] = [] 283 + failures.extend(_validate_regular_input(bundle, label="bundle")) 284 + failures.extend(_validate_regular_input(receipt, label="receipt")) 285 + failures.extend(_validate_regular_input(pubkey, label="pubkey")) 286 + signature_failures = _validate_regular_input(signature, label="receipt signature") 287 + for failure in signature_failures: 288 + failures.append( 289 + _failure_record( 290 + "advisory mirror receipt signature is missing or unsafe", 291 + expected=failure.expected, 292 + actual=failure.actual, 293 + repair="place freshness.json.minisig next to freshness.json", 294 + ) 295 + ) 296 + return signature, failures 297 + 298 + 299 + def _has_whitespace_or_control(value: str) -> bool: 300 + return any(char.isspace() for char in value) or CONTROL_RE.search(value) is not None 301 + 302 + 303 + def _terminal_name(locator: str) -> str | None: 304 + parsed = urlparse(locator) 305 + if parsed.scheme: 306 + path = parsed.path 307 + if not path: 308 + return None 309 + return Path(path).name 310 + match = SCP_LOCATOR_RE.fullmatch(locator) 311 + if match is None: 312 + return None 313 + path = match.group("path") 314 + if not path: 315 + return None 316 + return Path(path).name 317 + 318 + 319 + def validate_locator(locator: str) -> list[Failure]: 320 + failures: list[Failure] = [] 321 + if not isinstance(locator, str) or not locator: 322 + return [ 323 + _failure_record( 324 + "advisory mirror locator is empty", 325 + expected="private advisory mirror git locator", 326 + actual="<empty>", 327 + repair=INPUT_REPAIR, 328 + ) 329 + ] 330 + if _has_whitespace_or_control(locator): 331 + failures.append( 332 + _failure_record( 333 + "advisory mirror locator contains whitespace or control characters", 334 + expected="single git locator without whitespace or controls", 335 + actual="redacted", 336 + repair=INPUT_REPAIR, 337 + ) 338 + ) 339 + if locator.endswith("/"): 340 + failures.append( 341 + _failure_record( 342 + "advisory mirror locator has a trailing slash", 343 + expected="locator without trailing slash", 344 + actual="trailing slash", 345 + repair=INPUT_REPAIR, 346 + ) 347 + ) 348 + if "?" in locator or "#" in locator: 349 + failures.append( 350 + _failure_record( 351 + "advisory mirror locator contains query or fragment", 352 + expected="locator without query or fragment", 353 + actual="query or fragment", 354 + repair=INPUT_REPAIR, 355 + ) 356 + ) 357 + if failures: 358 + return failures 359 + 360 + source_failures = _validate_source(ADVISORY_COHORT_ID, (locator,)) 361 + if source_failures: 362 + return [ 363 + _failure_record( 364 + failure.error, 365 + expected=failure.expected, 366 + actual=failure.actual, 367 + repair=INPUT_REPAIR, 368 + ) 369 + for failure in source_failures 370 + ] 371 + 372 + terminal = _terminal_name(locator) 373 + if terminal not in ADVISORY_LOCATOR_TERMINALS: 374 + failures.append( 375 + _failure_record( 376 + "advisory mirror locator terminal name is not allowed", 377 + expected=", ".join(sorted(ADVISORY_LOCATOR_TERMINALS)), 378 + actual=terminal or "<empty>", 379 + repair=INPUT_REPAIR, 380 + ) 381 + ) 382 + return failures 383 + 384 + 385 + def _canonical_receipt_bytes(*, synced_commit: str, utc: str) -> bytes: 386 + return ( 387 + f'{{"max_age":86400,"synced_commit":"{synced_commit}","utc":"{utc}"}}\n' 388 + ).encode("utf-8") 389 + 390 + 391 + def _trusted_comment(*, synced_commit: str, utc: str) -> str: 392 + return ( 393 + f"{TRUSTED_COMMENT_SCHEME} synced_commit={synced_commit} " 394 + f"utc={utc} max_age=86400" 395 + ) 396 + 397 + 398 + def _read_receipt_authority(receipt: Path) -> ReceiptAuthority: 399 + raw = receipt.read_bytes() 400 + try: 401 + text = raw.decode("utf-8") 402 + payload = json.loads(text) 403 + except (UnicodeDecodeError, json.JSONDecodeError) as exc: 404 + _raise_one( 405 + "advisory mirror receipt is not valid canonical JSON", 406 + expected="canonical UTF-8 JSON object", 407 + actual=type(exc).__name__, 408 + ) 409 + if not isinstance(payload, dict): 410 + _raise_one( 411 + "advisory mirror receipt is not a JSON object", 412 + expected="receipt JSON object", 413 + actual=type(payload).__name__, 414 + ) 415 + if set(payload) != {"max_age", "synced_commit", "utc"}: 416 + _raise_one( 417 + "advisory mirror receipt key set is invalid", 418 + expected="max_age, synced_commit, utc", 419 + actual=", ".join(sorted(str(key) for key in payload)) or "<empty>", 420 + ) 421 + max_age = payload.get("max_age") 422 + synced_commit = payload.get("synced_commit") 423 + utc = payload.get("utc") 424 + if type(max_age) is not int or max_age != RECEIPT_MAX_AGE: 425 + _raise_one( 426 + "advisory mirror receipt max_age is invalid", 427 + expected=str(RECEIPT_MAX_AGE), 428 + actual=repr(max_age), 429 + ) 430 + if ( 431 + not isinstance(synced_commit, str) 432 + or GIT_COMMIT_RE.fullmatch(synced_commit) is None 433 + ): 434 + _raise_one( 435 + "advisory mirror receipt synced_commit is invalid", 436 + expected="40 lowercase hexadecimal git commit", 437 + actual="redacted", 438 + ) 439 + if not isinstance(utc, str) or not is_normalized_utc_timestamp(utc): 440 + _raise_one( 441 + "advisory mirror receipt utc is invalid", 442 + expected="RFC3339 UTC timestamp normalized with Z", 443 + actual="redacted", 444 + ) 445 + canonical = _canonical_receipt_bytes(synced_commit=synced_commit, utc=utc) 446 + if canonical != raw: 447 + _raise_one( 448 + "advisory mirror receipt bytes are not canonical", 449 + expected='{"max_age":86400,"synced_commit":"<40hex>","utc":"<RFC3339Z>"}\\n', 450 + actual="non-canonical JSON", 451 + ) 452 + return ReceiptAuthority( 453 + synced_commit=synced_commit, 454 + utc=utc, 455 + max_age=max_age, 456 + canonical_bytes=canonical, 457 + trusted_comment=_trusted_comment(synced_commit=synced_commit, utc=utc), 458 + ) 459 + 460 + 461 + def _validate_receipt_freshness(receipt: ReceiptAuthority, *, clock: Clock) -> datetime: 462 + now = clock() 463 + receipt_time = _parse_utc(receipt.utc, label="advisory mirror receipt utc") 464 + if receipt_time - now > MAX_CLOCK_SKEW: 465 + _raise_one( 466 + "advisory mirror receipt utc is in the future", 467 + expected="receipt utc no more than 5 minutes in the future", 468 + actual=receipt.utc, 469 + repair="check the system clock, then reacquire the signed advisory mirror packet", 470 + ) 471 + if now - receipt_time > timedelta(seconds=receipt.max_age): 472 + _raise_one( 473 + "advisory mirror receipt is stale", 474 + expected="receipt utc within max_age", 475 + actual=receipt.utc, 476 + repair=AUDIT_REPAIR, 477 + ) 478 + return now 479 + 480 + 481 + def _validate_pubkey_binding( 482 + pubkey: Path, 483 + *, 484 + pinned_key_id: str, 485 + pinned_pubkey_sha256: str, 486 + ) -> None: 487 + if KEY_ID_RE.fullmatch(pinned_key_id) is None: 488 + _raise_one( 489 + "advisory mirror pinned key ID is invalid", 490 + expected="16 uppercase hexadecimal characters", 491 + actual="redacted", 492 + ) 493 + if SHA256_RE.fullmatch(pinned_pubkey_sha256) is None: 494 + _raise_one( 495 + "advisory mirror pinned public key SHA-256 is invalid", 496 + expected="64 lowercase hexadecimal characters", 497 + actual="redacted", 498 + ) 499 + raw = pubkey.read_bytes() 500 + digest = hashlib.sha256(raw).hexdigest() 501 + if digest != pinned_pubkey_sha256: 502 + _raise_one( 503 + "advisory mirror public key SHA-256 does not match the pin", 504 + expected="pinned public key SHA-256", 505 + actual="sha256 mismatch", 506 + ) 507 + lines = raw.decode("utf-8", errors="replace").splitlines() 508 + if len(lines) < 2: 509 + _raise_one( 510 + "advisory mirror public key is malformed", 511 + expected="minisign public key with base64 body on line 2", 512 + actual=f"{len(lines)} lines", 513 + ) 514 + try: 515 + decoded = base64.b64decode(lines[1], validate=True) 516 + except ValueError: 517 + _raise_one( 518 + "advisory mirror public key body is not valid base64", 519 + expected="base64 minisign public key body", 520 + actual="invalid base64", 521 + ) 522 + if len(decoded) != 42 or decoded[:2] != b"Ed": 523 + _raise_one( 524 + "advisory mirror public key body is not an Ed25519 minisign key", 525 + expected="42-byte minisign Ed public key blob", 526 + actual="invalid public key blob", 527 + ) 528 + key_id = decoded[2:10][::-1].hex().upper() 529 + if key_id != pinned_key_id: 530 + _raise_one( 531 + "advisory mirror public key ID does not match the pin", 532 + expected="pinned minisign key ID", 533 + actual="key ID mismatch", 534 + ) 535 + 536 + 537 + def _verify_signature( 538 + *, 539 + verifier: TransparencySigner, 540 + receipt: Path, 541 + signature: Path, 542 + trusted_comment: str, 543 + secrets: set[str], 544 + ) -> None: 545 + try: 546 + verifier.check() 547 + verifier.verify_file( 548 + receipt, 549 + signature, 550 + expected_trusted_comment=trusted_comment, 551 + ) 552 + except DriverError as exc: 553 + raise _convert_driver_error(exc, secrets=secrets) from exc 554 + 555 + 556 + def audit_config_bytes( 557 + base_bytes: bytes, 558 + *, 559 + db_root: Path, 560 + db_urls: Sequence[str], 561 + ) -> bytes: 562 + try: 563 + base_text = base_bytes.decode("utf-8") 564 + except UnicodeDecodeError as exc: 565 + raise ReleasePolicyError( 566 + [ 567 + _failure_record( 568 + "core deny.toml is not UTF-8", 569 + expected="UTF-8 TOML", 570 + actual=str(exc), 571 + repair="python3 scripts/check_rust_release_manifest.py", 572 + ) 573 + ] 574 + ) from exc 575 + if ADVISORY_TABLE_RE.search(base_text): 576 + raise ReleasePolicyError( 577 + [ 578 + _failure_record( 579 + "core deny.toml already defines advisories", 580 + expected="core/deny.toml without [advisories]", 581 + actual="[advisories] present", 582 + repair="python3 scripts/check_rust_release_manifest.py", 583 + ) 584 + ] 585 + ) 586 + prefix = base_bytes if base_bytes.endswith(b"\n") else base_bytes + b"\n" 587 + urls = ", ".join(_toml_string(url) for url in db_urls) 588 + block = ( 589 + f"\n[advisories]\ndb-path = {_toml_string(str(db_root))}\ndb-urls = [{urls}]\n" 590 + ) 591 + return prefix + block.encode("utf-8") 592 + 593 + 594 + def _write_audit_config( 595 + root: Path, 596 + temp_root: Path, 597 + *, 598 + db_root: Path, 599 + db_urls: Sequence[str], 600 + ) -> Path: 601 + materialized = audit_config_bytes( 602 + (root / "core" / "deny.toml").read_bytes(), 603 + db_root=db_root, 604 + db_urls=db_urls, 605 + ) 606 + temp_root.mkdir(parents=True, exist_ok=True) 607 + path = temp_root / "deny.audit-advisories.toml" 608 + path.write_bytes(materialized) 609 + return path 610 + 611 + 612 + def _run( 613 + runner: Runner, 614 + argv: Sequence[str], 615 + *, 616 + cwd: Path | None = None, 617 + env: Mapping[str, str] | None = None, 618 + ) -> subprocess.CompletedProcess[str]: 619 + kwargs: dict[str, object] = { 620 + "capture_output": True, 621 + "text": True, 622 + "check": False, 623 + } 624 + if cwd is not None: 625 + kwargs["cwd"] = cwd 626 + if env is not None: 627 + kwargs["env"] = dict(env) 628 + return runner(list(argv), **kwargs) 629 + 630 + 631 + def _cargo_env() -> dict[str, str]: 632 + env = dict(os.environ) 633 + env["CARGO_NET_OFFLINE"] = "true" 634 + return env 635 + 636 + 637 + def _looks_like_remote(value: str) -> bool: 638 + parsed = urlparse(value) 639 + return bool(parsed.scheme and parsed.scheme != "file") 640 + 641 + 642 + def _assert_local_git_argv(argv: Sequence[str], *, bundle: Path) -> None: 643 + command = list(argv) 644 + forbidden = {"fetch", "pull", "ls-remote", "remote"} 645 + if any(part in forbidden for part in command[1:]): 646 + _raise_one( 647 + "advisory mirror attempted a remote git operation", 648 + expected="local git bundle operations only", 649 + actual="forbidden git subcommand", 650 + ) 651 + if len(command) >= 2 and command[1] == "clone": 652 + source = command[2] if len(command) > 2 else "" 653 + if source != str(bundle) or _looks_like_remote(source): 654 + _raise_one( 655 + "advisory mirror attempted to clone a non-bundle source", 656 + expected="git clone from the local advisory bundle", 657 + actual="non-local clone source", 658 + ) 659 + 660 + 661 + def _run_git_checked( 662 + runner: Runner, 663 + argv: Sequence[str], 664 + *, 665 + bundle: Path, 666 + error: str, 667 + secrets: set[str], 668 + cwd: Path | None = None, 669 + ) -> subprocess.CompletedProcess[str]: 670 + _assert_local_git_argv(argv, bundle=bundle) 671 + result = _run(runner, argv, cwd=cwd) 672 + if result.returncode != 0: 673 + _raise_one( 674 + error, 675 + expected="git exit 0", 676 + actual=f"exit {result.returncode}", 677 + ) 678 + return result 679 + 680 + 681 + def _run_cargo_deny_checked( 682 + runner: Runner, 683 + argv: Sequence[str], 684 + *, 685 + cwd: Path, 686 + env: Mapping[str, str], 687 + secrets: set[str], 688 + ) -> subprocess.CompletedProcess[str]: 689 + result = _run(runner, argv, cwd=cwd, env=env) 690 + if result.returncode != 0: 691 + actual = ( 692 + result.stderr.strip() 693 + or result.stdout.strip() 694 + or f"exit {result.returncode}" 695 + ) 696 + _raise_one( 697 + "advisory mirror cargo-deny final check failed", 698 + expected="cargo-deny advisory check exit 0", 699 + actual=_redact_child_output(actual, secrets=secrets), 700 + ) 701 + return result 702 + 703 + 704 + def _run_cargo_deny_unchecked( 705 + runner: Runner, 706 + argv: Sequence[str], 707 + *, 708 + cwd: Path, 709 + env: Mapping[str, str], 710 + ) -> subprocess.CompletedProcess[str]: 711 + return _run(runner, argv, cwd=cwd, env=env) 712 + 713 + 714 + def _parse_bundle_heads(stdout: str, *, synced_commit: str) -> None: 715 + lines = stdout.splitlines() 716 + if len(lines) != 2: 717 + _raise_one( 718 + "advisory mirror bundle head set is invalid", 719 + expected="exactly HEAD and refs/heads/main", 720 + actual=f"{len(lines)} refs", 721 + ) 722 + observed: set[tuple[str, str]] = set() 723 + for line in lines: 724 + parts = line.split() 725 + if len(parts) != 2: 726 + _raise_one( 727 + "advisory mirror bundle head line is malformed", 728 + expected="<sha> <ref>", 729 + actual="malformed line", 730 + ) 731 + commit, ref = parts 732 + if GIT_COMMIT_RE.fullmatch(commit) is None: 733 + _raise_one( 734 + "advisory mirror bundle commit is invalid", 735 + expected="40 lowercase hexadecimal git commit", 736 + actual="redacted", 737 + ) 738 + observed.add((commit, ref)) 739 + expected = { 740 + (synced_commit, "HEAD"), 741 + (synced_commit, "refs/heads/main"), 742 + } 743 + if observed != expected: 744 + _raise_one( 745 + "advisory mirror bundle head set does not match the signed receipt", 746 + expected="HEAD and refs/heads/main at synced_commit", 747 + actual="head set mismatch", 748 + ) 749 + 750 + 751 + def _assert_direct_child(path: Path, parent: Path) -> None: 752 + if path.name in {"", ".", ".."}: 753 + _raise_one( 754 + "advisory mirror derived database path is invalid", 755 + expected="safe direct child basename", 756 + actual="invalid basename", 757 + ) 758 + if path.resolve(strict=False).parent != parent.resolve(strict=False): 759 + _raise_one( 760 + "advisory mirror derived database path escapes the temp parent", 761 + expected="cargo-deny database path under temp parent", 762 + actual="redacted", 763 + ) 764 + 765 + 766 + def _assert_final_scanned_snapshot(stderr: str, snapshot: Path) -> None: 767 + try: 768 + _assert_scanned_snapshot(stderr, snapshot) 769 + except ReleasePolicyError as exc: 770 + failures = [ 771 + _failure_record( 772 + failure.error, 773 + expected="materialized advisory snapshot", 774 + actual="redacted", 775 + repair=AUDIT_REPAIR, 776 + ) 777 + for failure in exc.failures 778 + ] 779 + raise ReleasePolicyError(failures) from exc 780 + 781 + 782 + def _cargo_deny_version( 783 + cargo_deny: str, 784 + *, 785 + runner: Runner, 786 + secrets: set[str], 787 + ) -> str: 788 + result = _run(runner, [cargo_deny, "--version"]) 789 + actual = ( 790 + result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}" 791 + ) 792 + parts = actual.split() 793 + if ( 794 + result.returncode != 0 795 + or len(parts) < 2 796 + or parts[0] != "cargo-deny" 797 + or parts[1] != CARGO_DENY_VERSION 798 + ): 799 + _raise_one( 800 + "advisory mirror cargo-deny version is not pinned", 801 + expected=CARGO_DENY_VERSION, 802 + actual=_redact_child_output(actual, secrets=secrets), 803 + repair=f"cargo install cargo-deny@{CARGO_DENY_VERSION} --locked --force", 804 + ) 805 + return parts[1] 806 + 807 + 808 + def _cargo_lock_sha256(root: Path) -> str: 809 + try: 810 + return hashlib.sha256((root / "core" / "Cargo.lock").read_bytes()).hexdigest() 811 + except OSError as exc: 812 + raise ReleasePolicyError( 813 + [ 814 + _failure_record( 815 + "advisory mirror cargo lock could not be read", 816 + expected="core/Cargo.lock readable", 817 + actual=type(exc).__name__, 818 + repair="restore core/Cargo.lock and rerun make audit", 819 + ) 820 + ] 821 + ) from exc 822 + 823 + 824 + def _success_bytes( 825 + *, 826 + receipt: ReceiptAuthority, 827 + checked_at: datetime, 828 + cargo_lock_sha256: str, 829 + cargo_deny_version: str, 830 + ) -> bytes: 831 + payload = { 832 + "product": PRODUCT, 833 + "advisory_cohort": ADVISORY_COHORT_ID, 834 + "synced_commit": receipt.synced_commit, 835 + "receipt_utc": receipt.utc, 836 + "max_age": receipt.max_age, 837 + "checked_at": _format_utc(checked_at), 838 + "cargo_lock_sha256": cargo_lock_sha256, 839 + "cargo_deny_version": cargo_deny_version, 840 + "verdict": "pass", 841 + } 842 + return json.dumps(payload, separators=(",", ":")).encode("utf-8") + b"\n" 843 + 844 + 845 + def _remove_tree(path: Path) -> None: 846 + shutil.rmtree(path) 847 + 848 + 849 + def _materialize_and_check( 850 + root: Path, 851 + *, 852 + bundle: Path, 853 + receipt: ReceiptAuthority, 854 + cargo_deny: str, 855 + runner: Runner, 856 + temp_root: Path, 857 + config_path: Path, 858 + secrets: set[str], 859 + ) -> None: 860 + db_parent = temp_root / "db-root" 861 + db_parent.mkdir(parents=True, exist_ok=True) 862 + throwaway = db_parent / "bundle-clone" 863 + secrets.update({str(temp_root), str(db_parent), str(throwaway)}) 864 + 865 + _run_git_checked( 866 + runner, 867 + ["git", "bundle", "verify", str(bundle)], 868 + bundle=bundle, 869 + error="advisory mirror bundle verification failed", 870 + secrets=secrets, 871 + ) 872 + heads = _run_git_checked( 873 + runner, 874 + ["git", "bundle", "list-heads", str(bundle)], 875 + bundle=bundle, 876 + error="advisory mirror bundle list-heads failed", 877 + secrets=secrets, 878 + ) 879 + _parse_bundle_heads(heads.stdout.strip(), synced_commit=receipt.synced_commit) 880 + 881 + _run_git_checked( 882 + runner, 883 + ["git", "clone", str(bundle), str(throwaway)], 884 + bundle=bundle, 885 + error="advisory mirror bundle clone failed", 886 + secrets=secrets, 887 + ) 888 + head = _run_git_checked( 889 + runner, 890 + ["git", "-C", str(throwaway), "rev-parse", "HEAD"], 891 + bundle=bundle, 892 + error="advisory mirror clone HEAD read failed", 893 + secrets=secrets, 894 + ) 895 + if head.stdout.strip() != receipt.synced_commit: 896 + _raise_one( 897 + "advisory mirror clone HEAD does not match the signed receipt", 898 + expected="cloned HEAD equals synced_commit", 899 + actual="clone HEAD mismatch", 900 + ) 901 + _validate_advisory_count(_count_advisories(throwaway)) 902 + 903 + cargo_env = _cargo_env() 904 + argv = advisory_check_argv(cargo_deny, config_path, root) 905 + discovery = _run_cargo_deny_unchecked(runner, argv, cwd=root, env=cargo_env) 906 + derived = _scanned_advisory_db(discovery.stderr) 907 + _assert_direct_child(derived, db_parent) 908 + if derived.exists(): 909 + _raise_one( 910 + "advisory mirror derived database path already exists", 911 + expected="cargo-deny derived database path absent before rename", 912 + actual="preexisting derived path", 913 + ) 914 + secrets.add(str(derived)) 915 + throwaway.rename(derived) 916 + 917 + final = _run_cargo_deny_checked( 918 + runner, 919 + argv, 920 + cwd=root, 921 + env=cargo_env, 922 + secrets=secrets, 923 + ) 924 + _assert_final_scanned_snapshot(final.stderr, derived) 925 + 926 + 927 + def audit_advisory_mirror( 928 + root: Path, 929 + *, 930 + bundle: Path, 931 + receipt: Path, 932 + pubkey: Path, 933 + locator: str, 934 + cargo_deny: str = "cargo-deny", 935 + runner: Runner = subprocess.run, 936 + verifier: TransparencySigner | None = None, 937 + minisign: str = "minisign", 938 + clock: Clock = _utc_now, 939 + temp_path_factory: TempPathFactory = _default_temp_path_factory, 940 + cleanup_unlink: PathRemover = _unlink_path, 941 + cleanup_rmdir: PathRemover = _remove_tree, 942 + pinned_key_id: str = PINNED_KEY_ID, 943 + pinned_pubkey_sha256: str = PINNED_PUBKEY_SHA256, 944 + ) -> bytes: 945 + signature, input_failures = _validate_inputs( 946 + bundle=bundle, 947 + receipt=receipt, 948 + pubkey=pubkey, 949 + ) 950 + if input_failures: 951 + raise ReleasePolicyError(input_failures) 952 + 953 + locator_failures = validate_locator(locator) 954 + if locator_failures: 955 + raise ReleasePolicyError(locator_failures) 956 + 957 + secrets = { 958 + locator, 959 + str(bundle), 960 + str(receipt), 961 + str(signature), 962 + str(pubkey), 963 + } 964 + _validate_pubkey_binding( 965 + pubkey, 966 + pinned_key_id=pinned_key_id, 967 + pinned_pubkey_sha256=pinned_pubkey_sha256, 968 + ) 969 + receipt_authority = _read_receipt_authority(receipt) 970 + active_verifier = verifier or PublicKeyMinisignVerifier(pubkey, minisign=minisign) 971 + _verify_signature( 972 + verifier=active_verifier, 973 + receipt=receipt, 974 + signature=signature, 975 + trusted_comment=receipt_authority.trusted_comment, 976 + secrets=secrets, 977 + ) 978 + checked_at = _validate_receipt_freshness(receipt_authority, clock=clock) 979 + 980 + cargo_deny_observed = _cargo_deny_version( 981 + cargo_deny, 982 + runner=runner, 983 + secrets=secrets, 984 + ) 985 + 986 + temp_root = temp_path_factory("advisory-mirror-audit") 987 + config_path: Path | None = None 988 + result: bytes | None = None 989 + primary_error: ReleasePolicyError | None = None 990 + try: 991 + db_parent = temp_root / "db-root" 992 + config_path = _write_audit_config( 993 + root, 994 + temp_root, 995 + db_root=db_parent, 996 + db_urls=(locator,), 997 + ) 998 + secrets.update({str(temp_root), str(config_path)}) 999 + _materialize_and_check( 1000 + root, 1001 + bundle=bundle, 1002 + receipt=receipt_authority, 1003 + cargo_deny=cargo_deny, 1004 + runner=runner, 1005 + temp_root=temp_root, 1006 + config_path=config_path, 1007 + secrets=secrets, 1008 + ) 1009 + result = _success_bytes( 1010 + receipt=receipt_authority, 1011 + checked_at=checked_at, 1012 + cargo_lock_sha256=_cargo_lock_sha256(root), 1013 + cargo_deny_version=cargo_deny_observed, 1014 + ) 1015 + except ReleasePolicyError as exc: 1016 + primary_error = exc 1017 + finally: 1018 + cleanup_error: ReleasePolicyError | None = None 1019 + try: 1020 + _cleanup_temp( 1021 + temp_root, 1022 + config_path, 1023 + unlink_path=cleanup_unlink, 1024 + remove_dir=cleanup_rmdir, 1025 + ) 1026 + except ReleasePolicyError as exc: 1027 + cleanup_error = exc 1028 + combined = _combined_release_policy_error(primary_error, cleanup_error) 1029 + if combined is not None: 1030 + raise combined 1031 + if result is None: 1032 + raise AssertionError("advisory mirror audit did not produce a result") 1033 + return result 1034 + 1035 + 1036 + def _raw_arg_failures(args: argparse.Namespace) -> list[Failure]: 1037 + failures: list[Failure] = [] 1038 + for name in ("bundle", "receipt", "pubkey", "locator"): 1039 + value = getattr(args, name) 1040 + if value == "": 1041 + failures.append( 1042 + _failure_record( 1043 + f"advisory mirror input {name} is empty", 1044 + expected=f"non-empty {name}", 1045 + actual="<empty>", 1046 + repair=INPUT_REPAIR, 1047 + ) 1048 + ) 1049 + return failures 1050 + 1051 + 1052 + def main(argv: Sequence[str] | None = None) -> int: 1053 + parser = argparse.ArgumentParser() 1054 + parser.add_argument("--bundle", required=True) 1055 + parser.add_argument("--receipt", required=True) 1056 + parser.add_argument("--pubkey", required=True) 1057 + parser.add_argument("--locator", required=True) 1058 + args = parser.parse_args(list(argv) if argv is not None else None) 1059 + 1060 + try: 1061 + raw_failures = _raw_arg_failures(args) 1062 + if raw_failures: 1063 + raise ReleasePolicyError(raw_failures) 1064 + output = audit_advisory_mirror( 1065 + ROOT, 1066 + bundle=Path(args.bundle), 1067 + receipt=Path(args.receipt), 1068 + pubkey=Path(args.pubkey), 1069 + locator=args.locator, 1070 + ) 1071 + except ReleasePolicyError as exc: 1072 + _format_failures(exc.failures) 1073 + return 1 1074 + sys.stdout.buffer.write(output) 1075 + return 0 1076 + 1077 + 1078 + if __name__ == "__main__": 1079 + raise SystemExit(main())
+3 -1
scripts/release_advisory_policy.py
··· 24 24 second run is intentional: cargo-deny 0.20.2 does not write ``.git/FETCH_HEAD`` on the 25 25 first clone into an empty db root, but it does on subsequent fetches. Do not run 26 26 manual ``git fetch`` or ``git reset``. ``make audit`` is not this acquisition 27 - operation; it uses cargo-deny's default db path, not a controlled release db root. 27 + operation. ``make audit`` is the separate signed-packet mirror-bound advisory 28 + audit implemented by ``scripts/advisory_mirror_audit.py``; this module remains 29 + the release-candidate advisory acquisition and receipt path. 28 30 """ 29 31 30 32 from __future__ import annotations
+1237
tests/test_advisory_mirror_audit.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import base64 7 + import hashlib 8 + import json 9 + import subprocess 10 + from collections.abc import Mapping, Sequence 11 + from datetime import UTC, datetime 12 + from pathlib import Path 13 + from typing import Any 14 + 15 + import pytest 16 + 17 + import scripts.advisory_mirror_audit as audit 18 + from scripts.transparency_signing import FakeTransparencySigner 19 + 20 + NOW = datetime(2026, 7, 24, 12, 0, tzinfo=UTC) 21 + RECEIPT_UTC = "2026-07-24T11:30:00Z" 22 + TEST_KEY_ID = "A1B2C3D4E5F60708" 23 + DERIVED_NAME = "rustsec-advisory-db.git-testderived" 24 + 25 + 26 + def _run_git(repo: Path, argv: Sequence[str]) -> str: 27 + result = subprocess.run( 28 + ["git", *argv], 29 + cwd=repo, 30 + capture_output=True, 31 + text=True, 32 + check=False, 33 + ) 34 + assert result.returncode == 0, result.stderr 35 + return result.stdout.strip() 36 + 37 + 38 + def _audit_root(tmp_path: Path) -> Path: 39 + root = tmp_path / "root" 40 + (root / "core").mkdir(parents=True) 41 + (root / "core" / "deny.toml").write_text( 42 + '[licenses]\nallow = ["MIT"]\n', encoding="utf-8" 43 + ) 44 + (root / "core" / "Cargo.lock").write_text("fixture lock\n", encoding="utf-8") 45 + (root / "core" / "Cargo.toml").write_text( 46 + "[workspace]\nmembers = []\n", 47 + encoding="utf-8", 48 + ) 49 + (root / "target" / "release-evidence").mkdir(parents=True) 50 + (root / "target" / "release-evidence" / "preserve.txt").write_text( 51 + "release evidence\n", 52 + encoding="utf-8", 53 + ) 54 + (root / "dist" / "release-candidate").mkdir(parents=True) 55 + (root / "dist" / "release-candidate" / "preserve.txt").write_text( 56 + "release candidate\n", 57 + encoding="utf-8", 58 + ) 59 + return root 60 + 61 + 62 + def _advisory_repo( 63 + tmp_path: Path, *, advisory_count: int = 1 64 + ) -> tuple[Path, str, Path]: 65 + repo = tmp_path / f"repo-{advisory_count}" 66 + repo.mkdir() 67 + _run_git(repo, ["init", "-b", "main"]) 68 + _run_git(repo, ["config", "user.name", "Audit Test"]) 69 + _run_git(repo, ["config", "user.email", "audit-test@example.invalid"]) 70 + if advisory_count: 71 + for index in range(advisory_count): 72 + path = repo / "crates" / f"probe{index}" / f"RUSTSEC-2026-{index:04d}.md" 73 + path.parent.mkdir(parents=True) 74 + path.write_text( 75 + "```toml\n" 76 + "[advisory]\n" 77 + f'id = "RUSTSEC-2026-{index:04d}"\n' 78 + f'package = "probe{index}"\n' 79 + 'date = "2026-01-01"\n' 80 + 'url = "https://example.invalid/RUSTSEC-2026-0001"\n' 81 + 'categories = ["unmaintained"]\n' 82 + "keywords = []\n\n" 83 + "[versions]\n" 84 + "patched = []\n" 85 + "```\n", 86 + encoding="utf-8", 87 + ) 88 + else: 89 + (repo / "README.md").write_text("empty advisory db\n", encoding="utf-8") 90 + _run_git(repo, ["add", "."]) 91 + _run_git(repo, ["commit", "-m", "fixture advisory db"]) 92 + commit = _run_git(repo, ["rev-parse", "HEAD"]) 93 + bundle = tmp_path / f"advisory-{advisory_count}.bundle" 94 + _run_git(repo, ["bundle", "create", str(bundle), "HEAD", "refs/heads/main"]) 95 + return repo, commit, bundle 96 + 97 + 98 + def _pubkey_bytes(key_id: str = TEST_KEY_ID) -> bytes: 99 + raw_id = bytes.fromhex(key_id)[::-1] 100 + blob = b"Ed" + raw_id + (b"\x11" * 32) 101 + return ( 102 + f"untrusted comment: minisign public key {key_id}\n".encode("ascii") 103 + + base64.b64encode(blob) 104 + + b"\n" 105 + ) 106 + 107 + 108 + def _write_pubkey(path: Path, *, key_id: str = TEST_KEY_ID) -> str: 109 + raw = _pubkey_bytes(key_id) 110 + path.write_bytes(raw) 111 + return hashlib.sha256(raw).hexdigest() 112 + 113 + 114 + def _receipt_bytes( 115 + commit: str, utc: str = RECEIPT_UTC, *, max_age: int = 86400 116 + ) -> bytes: 117 + return ( 118 + f'{{"max_age":{max_age},"synced_commit":"{commit}","utc":"{utc}"}}\n' 119 + ).encode("utf-8") 120 + 121 + 122 + def _trusted_comment(commit: str, utc: str = RECEIPT_UTC) -> str: 123 + return ( 124 + f"{audit.TRUSTED_COMMENT_SCHEME} synced_commit={commit} utc={utc} max_age=86400" 125 + ) 126 + 127 + 128 + def _write_packet( 129 + tmp_path: Path, 130 + *, 131 + commit: str, 132 + utc: str = RECEIPT_UTC, 133 + key_id: str = TEST_KEY_ID, 134 + signer: FakeTransparencySigner | None = None, 135 + trusted_comment: str | None = None, 136 + ) -> tuple[Path, Path, Path, str, FakeTransparencySigner]: 137 + pubkey = tmp_path / "pub.key" 138 + pubkey_sha = _write_pubkey(pubkey, key_id=key_id) 139 + receipt = tmp_path / "freshness.json" 140 + receipt.write_bytes(_receipt_bytes(commit, utc)) 141 + signature = receipt.parent / f"{receipt.name}.minisig" 142 + fake = signer or FakeTransparencySigner() 143 + fake.sign_file( 144 + receipt, 145 + signature, 146 + trusted_comment=trusted_comment or _trusted_comment(commit, utc), 147 + ) 148 + return receipt, signature, pubkey, pubkey_sha, fake 149 + 150 + 151 + class NoCallRunner: 152 + events: list[list[str]] 153 + 154 + def __init__(self) -> None: 155 + self.events = [] 156 + 157 + def __call__(self, argv, **kwargs) -> subprocess.CompletedProcess[str]: 158 + self.events.append(list(argv)) 159 + raise AssertionError(f"unexpected command: {argv}") 160 + 161 + 162 + class HybridRunner: 163 + def __init__( 164 + self, 165 + *, 166 + derived_name: str = DERIVED_NAME, 167 + discovery_exit: int = 1, 168 + final_exit: int = 0, 169 + final_stderr_extra: str = "", 170 + final_scanned_path: Path | None = None, 171 + version: str = audit.CARGO_DENY_VERSION, 172 + ) -> None: 173 + self.derived_name = derived_name 174 + self.discovery_exit = discovery_exit 175 + self.final_exit = final_exit 176 + self.final_stderr_extra = final_stderr_extra 177 + self.final_scanned_path = final_scanned_path 178 + self.version = version 179 + self.events: list[list[str]] = [] 180 + self.cargo_envs: list[Mapping[str, str]] = [] 181 + self.cargo_cwds: list[Path | None] = [] 182 + self.config_bytes: bytes | None = None 183 + self.check_count = 0 184 + 185 + def __call__(self, argv, **kwargs) -> subprocess.CompletedProcess[str]: 186 + command = list(argv) 187 + self.events.append(command) 188 + if command[0] == "git": 189 + return subprocess.run(command, **kwargs) 190 + if command[0] == "cargo-deny": 191 + if command == ["cargo-deny", "--version"]: 192 + return subprocess.CompletedProcess( 193 + command, 0, f"cargo-deny {self.version}\n", "" 194 + ) 195 + if command[-2:] == ["check", "advisories"]: 196 + self.check_count += 1 197 + self.cargo_envs.append(kwargs.get("env", {})) 198 + self.cargo_cwds.append(kwargs.get("cwd")) 199 + config_path = Path(command[command.index("--config") + 1]) 200 + self.config_bytes = config_path.read_bytes() 201 + db_parent = _config_db_path(config_path) 202 + scanned = self.final_scanned_path if self.check_count == 2 else None 203 + if scanned is None: 204 + scanned = db_parent / self.derived_name 205 + stderr = ( 206 + f"2026-07-24 [DEBUG] Opening advisory database at '{scanned}'\n" 207 + ) 208 + if self.check_count == 1: 209 + return subprocess.CompletedProcess( 210 + command, self.discovery_exit, "", stderr 211 + ) 212 + stderr += self.final_stderr_extra 213 + return subprocess.CompletedProcess(command, self.final_exit, "", stderr) 214 + raise AssertionError(f"unexpected cargo-deny command: {command}") 215 + raise AssertionError(f"unexpected command: {command}") 216 + 217 + 218 + class FakeGitRunner: 219 + def __init__( 220 + self, 221 + *, 222 + heads_commit: str, 223 + clone_commit: str | None = None, 224 + fail: str | None = None, 225 + ) -> None: 226 + self.heads_commit = heads_commit 227 + self.clone_commit = clone_commit or heads_commit 228 + self.fail = fail 229 + self.events: list[list[str]] = [] 230 + 231 + def __call__(self, argv, **kwargs) -> subprocess.CompletedProcess[str]: 232 + command = list(argv) 233 + self.events.append(command) 234 + if self.fail and self.fail in " ".join(command): 235 + return subprocess.CompletedProcess(command, 1, "", "failed") 236 + if command == ["cargo-deny", "--version"]: 237 + return subprocess.CompletedProcess(command, 0, "cargo-deny 0.20.2\n", "") 238 + if command[:3] == ["git", "bundle", "verify"]: 239 + return subprocess.CompletedProcess(command, 0, "", "") 240 + if command[:3] == ["git", "bundle", "list-heads"]: 241 + return subprocess.CompletedProcess( 242 + command, 243 + 0, 244 + f"{self.heads_commit} HEAD\n{self.heads_commit} refs/heads/main\n", 245 + "", 246 + ) 247 + if command[:2] == ["git", "clone"]: 248 + Path(command[3]).mkdir(parents=True) 249 + return subprocess.CompletedProcess(command, 0, "", "") 250 + if command[:3] == ["git", "-C", command[2]] and command[-2:] == [ 251 + "rev-parse", 252 + "HEAD", 253 + ]: 254 + return subprocess.CompletedProcess(command, 0, self.clone_commit + "\n", "") 255 + if command[-2:] == ["check", "advisories"]: 256 + config_path = Path(command[command.index("--config") + 1]) 257 + db_parent = _config_db_path(config_path) 258 + return subprocess.CompletedProcess( 259 + command, 260 + 1, 261 + "", 262 + f"Opening advisory database at '{db_parent / DERIVED_NAME}'\n", 263 + ) 264 + raise AssertionError(f"unexpected command: {command}") 265 + 266 + 267 + def _config_db_path(config_path: Path) -> Path: 268 + import tomllib 269 + 270 + parsed = tomllib.loads(config_path.read_text(encoding="utf-8")) 271 + return Path(parsed["advisories"]["db-path"]) 272 + 273 + 274 + def _invoke_green( 275 + tmp_path: Path, 276 + *, 277 + runner: HybridRunner | None = None, 278 + advisory_count: int = 1, 279 + utc: str = RECEIPT_UTC, 280 + ) -> tuple[bytes, HybridRunner, Path, Path, Path, Path]: 281 + root = _audit_root(tmp_path) 282 + _repo, commit, bundle = _advisory_repo(tmp_path, advisory_count=advisory_count) 283 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 284 + tmp_path, 285 + commit=commit, 286 + utc=utc, 287 + ) 288 + active_runner = runner or HybridRunner() 289 + output = audit.audit_advisory_mirror( 290 + root, 291 + bundle=bundle, 292 + receipt=receipt, 293 + pubkey=pubkey, 294 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 295 + runner=active_runner, 296 + verifier=fake, 297 + clock=lambda: NOW, 298 + pinned_key_id=TEST_KEY_ID, 299 + pinned_pubkey_sha256=pubkey_sha, 300 + ) 301 + return output, active_runner, root, bundle, receipt, pubkey 302 + 303 + 304 + def _inventory(root: Path) -> dict[str, tuple[str, int]]: 305 + result: dict[str, tuple[str, int]] = {} 306 + if not root.exists(): 307 + return result 308 + for path in sorted(item for item in root.rglob("*") if item.is_file()): 309 + rel = path.relative_to(root).as_posix() 310 + raw = path.read_bytes() 311 + result[rel] = (hashlib.sha256(raw).hexdigest(), len(raw)) 312 + return result 313 + 314 + 315 + def test_green_packet_emits_exact_success_json_and_uses_bound_snapshot( 316 + tmp_path: Path, 317 + ) -> None: 318 + output, runner, root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) 319 + 320 + payload = json.loads(output) 321 + assert list(payload) == [ 322 + "product", 323 + "advisory_cohort", 324 + "synced_commit", 325 + "receipt_utc", 326 + "max_age", 327 + "checked_at", 328 + "cargo_lock_sha256", 329 + "cargo_deny_version", 330 + "verdict", 331 + ] 332 + assert payload["product"] == "solstone-journal" 333 + assert payload["advisory_cohort"] == audit.ADVISORY_COHORT_ID 334 + assert payload["receipt_utc"] == RECEIPT_UTC 335 + assert payload["max_age"] == 86400 336 + assert payload["checked_at"] == "2026-07-24T12:00:00Z" 337 + assert payload["cargo_deny_version"] == "0.20.2" 338 + assert ( 339 + payload["cargo_lock_sha256"] 340 + == hashlib.sha256((root / "core" / "Cargo.lock").read_bytes()).hexdigest() 341 + ) 342 + expected = json.dumps(payload, separators=(",", ":")).encode("utf-8") + b"\n" 343 + assert output == expected 344 + assert output.count(b"\n") == 1 345 + assert runner.check_count == 2 346 + assert all(env.get("CARGO_NET_OFFLINE") == "true" for env in runner.cargo_envs) 347 + assert runner.config_bytes is not None 348 + config_text = runner.config_bytes.decode("utf-8") 349 + assert "git-fetch-with-cli" not in config_text 350 + assert "maximum-db-staleness" not in config_text 351 + 352 + 353 + def test_green_packet_with_real_git_bundle_materialization(tmp_path: Path) -> None: 354 + output, runner, _root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) 355 + 356 + assert json.loads(output)["verdict"] == "pass" 357 + assert any(command[:3] == ["git", "bundle", "verify"] for command in runner.events) 358 + assert any( 359 + command[:3] == ["git", "bundle", "list-heads"] for command in runner.events 360 + ) 361 + assert any(command[:2] == ["git", "clone"] for command in runner.events) 362 + assert runner.check_count == 2 363 + 364 + 365 + @pytest.mark.parametrize("missing", ["bundle", "receipt", "pubkey", "locator"]) 366 + def test_required_inputs_fail_before_git_or_cargo(tmp_path: Path, missing: str) -> None: 367 + root = _audit_root(tmp_path) 368 + _repo, commit, bundle = _advisory_repo(tmp_path) 369 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 370 + tmp_path, commit=commit 371 + ) 372 + if missing == "bundle": 373 + bundle = tmp_path / "missing.bundle" 374 + elif missing == "receipt": 375 + receipt = tmp_path / "missing.json" 376 + elif missing == "pubkey": 377 + pubkey = tmp_path / "missing.pub" 378 + locator = ( 379 + "" 380 + if missing == "locator" 381 + else "ssh://mirror.example.invalid/rustsec-advisory-db.git" 382 + ) 383 + runner = NoCallRunner() 384 + 385 + with pytest.raises(audit.ReleasePolicyError): 386 + audit.audit_advisory_mirror( 387 + root, 388 + bundle=bundle, 389 + receipt=receipt, 390 + pubkey=pubkey, 391 + locator=locator, 392 + runner=runner, 393 + verifier=fake, 394 + clock=lambda: NOW, 395 + pinned_key_id=TEST_KEY_ID, 396 + pinned_pubkey_sha256=pubkey_sha, 397 + ) 398 + assert runner.events == [] 399 + 400 + 401 + @pytest.mark.parametrize("kind", ["missing", "symlink", "directory"]) 402 + def test_adjacent_signature_is_required_and_regular_before_git_or_cargo( 403 + tmp_path: Path, 404 + kind: str, 405 + ) -> None: 406 + root = _audit_root(tmp_path) 407 + _repo, commit, bundle = _advisory_repo(tmp_path) 408 + receipt, signature, pubkey, pubkey_sha, fake = _write_packet( 409 + tmp_path, commit=commit 410 + ) 411 + signature.unlink() 412 + if kind == "symlink": 413 + target = tmp_path / "sig-target" 414 + target.write_text("signature", encoding="utf-8") 415 + signature.symlink_to(target) 416 + elif kind == "directory": 417 + signature.mkdir() 418 + runner = NoCallRunner() 419 + 420 + with pytest.raises(audit.ReleasePolicyError): 421 + audit.audit_advisory_mirror( 422 + root, 423 + bundle=bundle, 424 + receipt=receipt, 425 + pubkey=pubkey, 426 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 427 + runner=runner, 428 + verifier=fake, 429 + clock=lambda: NOW, 430 + pinned_key_id=TEST_KEY_ID, 431 + pinned_pubkey_sha256=pubkey_sha, 432 + ) 433 + assert runner.events == [] 434 + 435 + 436 + @pytest.mark.parametrize("target_name", ["bundle", "receipt", "pubkey"]) 437 + @pytest.mark.parametrize("kind", ["missing", "symlink", "directory"]) 438 + def test_unsafe_input_paths_and_symlinks_fail_before_git_or_cargo( 439 + tmp_path: Path, 440 + target_name: str, 441 + kind: str, 442 + ) -> None: 443 + root = _audit_root(tmp_path) 444 + _repo, commit, bundle = _advisory_repo(tmp_path) 445 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 446 + tmp_path, commit=commit 447 + ) 448 + paths = {"bundle": bundle, "receipt": receipt, "pubkey": pubkey} 449 + target = paths[target_name] 450 + if target.exists() or target.is_symlink(): 451 + target.unlink() 452 + if kind == "symlink": 453 + link_target = tmp_path / f"{target_name}-target" 454 + link_target.write_text("target", encoding="utf-8") 455 + target.symlink_to(link_target) 456 + elif kind == "directory": 457 + target.mkdir() 458 + runner = NoCallRunner() 459 + 460 + with pytest.raises(audit.ReleasePolicyError): 461 + audit.audit_advisory_mirror( 462 + root, 463 + bundle=paths["bundle"], 464 + receipt=paths["receipt"], 465 + pubkey=paths["pubkey"], 466 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 467 + runner=runner, 468 + verifier=fake, 469 + clock=lambda: NOW, 470 + pinned_key_id=TEST_KEY_ID, 471 + pinned_pubkey_sha256=pubkey_sha, 472 + ) 473 + assert runner.events == [] 474 + 475 + 476 + @pytest.mark.parametrize( 477 + ("locator", "accepted"), 478 + [ 479 + ("https://github.com/rustsec/advisory-db", False), 480 + ("https://github.com/rustsec/advisory-db.git", False), 481 + ("https://github.com/rustsec/advisory-db/", False), 482 + ("https://github.com/rustsec/advisory-db.git/", False), 483 + ("github.com:rustsec/advisory-db.git", False), 484 + ("github.com:rustsec/advisory-db.git/", False), 485 + ("ssh://github.com/rustsec/advisory-db.git", False), 486 + ("ssh://github.com:22/rustsec/advisory-db.git", False), 487 + ("git://github.com/rustsec/advisory-db.git", False), 488 + ("http://github.com/rustsec/advisory-db", False), 489 + ("git+ssh://github.com/rustsec/advisory-db.git", False), 490 + ("https://raw.github.com/rustsec/advisory-db", False), 491 + ("https://mirror.github.com/rustsec/advisory-db", False), 492 + ("https://foo.github.com/rustsec/advisory-db.git", False), 493 + ("https://github.com/rustsec/advisory-db?x=1", False), 494 + ("https://github.com/rustsec/advisory-db#frag", False), 495 + ("github.com/rustsec/advisory-db.git", False), 496 + ("", False), 497 + (" ", False), 498 + ("https://mirror.example.invalid/rustsec/advisory-db", True), 499 + ("https://mirror.example.invalid/rustsec/advisory-db.git", False), 500 + ("https://mirror.example.invalid/rustsec/rustsec-advisory-db.git", True), 501 + ("https://mirror.example.invalid/rustsec/rustsec-advisory-db", False), 502 + ("https://mirror.example.invalid/rustsec/advisory-db/", False), 503 + ("https://mirror.example.invalid/rustsec/rustsec-advisory-db.git/", False), 504 + ("https://mirror.example.invalid/rustsec/advisory-db?x=1", False), 505 + ("https://mirror.example.invalid/rustsec/advisory-db#frag", False), 506 + ("ssh://git@mirror.example.invalid/rustsec/advisory-db", True), 507 + ("git@mirror.example.invalid:rustsec/advisory-db", True), 508 + ("git@mirror.example.invalid:rustsec/rustsec-advisory-db.git", True), 509 + ("git@mirror.example.invalid:rustsec/advisory-db.git", False), 510 + ("https://mirror.example.invalid/rustsec/advisory-db\n", False), 511 + ("https://mirror.example.invalid/rustsec/advisory-db\t", False), 512 + ("https://mirror.example.invalid/rustsec/advisory-db\x00", False), 513 + ("https://github.com.evil/rustsec/advisory-db", True), 514 + ], 515 + ) 516 + def test_validate_locator_q3_oracle(locator: str, accepted: bool) -> None: 517 + assert (audit.validate_locator(locator) == []) is accepted 518 + 519 + 520 + @pytest.mark.parametrize( 521 + "raw", 522 + [ 523 + b'{"synced_commit":"{commit}","max_age":86400,"utc":"2026-07-24T11:30:00Z"}\n', 524 + b'{ "max_age": 86400, "synced_commit": "{commit}", "utc": "2026-07-24T11:30:00Z" }\n', 525 + b'{"max_age":86400,"synced_commit":"{commit}","utc":"2026-07-24T11:30:00Z"}', 526 + b'{"max_age":86400,"synced_commit":"{commit}","utc":"2026-07-24T11:30:00Z"}\n\n', 527 + b'{"max_age":86400,"synced_commit":"{commit}","utc":"2026-07-24T11:30:00Z","x":1}\n', 528 + ], 529 + ) 530 + def test_receipt_body_requires_canonical_bytes(tmp_path: Path, raw: bytes) -> None: 531 + _repo, commit, _bundle = _advisory_repo(tmp_path) 532 + receipt = tmp_path / "freshness.json" 533 + receipt.write_bytes(raw.replace(b"{commit}", commit.encode("ascii"))) 534 + 535 + with pytest.raises(audit.ReleasePolicyError): 536 + audit._read_receipt_authority(receipt) 537 + 538 + 539 + @pytest.mark.parametrize( 540 + "payload", 541 + [ 542 + {"max_age": 1, "synced_commit": "a" * 40, "utc": RECEIPT_UTC}, 543 + {"max_age": True, "synced_commit": "a" * 40, "utc": RECEIPT_UTC}, 544 + {"max_age": "86400", "synced_commit": "a" * 40, "utc": RECEIPT_UTC}, 545 + {"max_age": 86400, "synced_commit": "a" * 64, "utc": RECEIPT_UTC}, 546 + {"max_age": 86400, "synced_commit": "A" * 40, "utc": RECEIPT_UTC}, 547 + {"max_age": 86400, "synced_commit": "a" * 40, "utc": "2026-99-99T00:00:00Z"}, 548 + { 549 + "max_age": 86400, 550 + "synced_commit": "a" * 40, 551 + "utc": "2026-07-24T11:30:00+00:00", 552 + }, 553 + ], 554 + ) 555 + def test_receipt_fields_are_strict(tmp_path: Path, payload: dict[str, Any]) -> None: 556 + receipt = tmp_path / "freshness.json" 557 + receipt.write_text( 558 + json.dumps(payload, separators=(",", ":")) + "\n", encoding="utf-8" 559 + ) 560 + 561 + with pytest.raises(audit.ReleasePolicyError): 562 + audit._read_receipt_authority(receipt) 563 + 564 + 565 + def test_trusted_comment_mismatch_fails(tmp_path: Path) -> None: 566 + root = _audit_root(tmp_path) 567 + _repo, commit, bundle = _advisory_repo(tmp_path) 568 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 569 + tmp_path, 570 + commit=commit, 571 + trusted_comment="wrong", 572 + ) 573 + 574 + with pytest.raises(audit.ReleasePolicyError) as exc: 575 + audit.audit_advisory_mirror( 576 + root, 577 + bundle=bundle, 578 + receipt=receipt, 579 + pubkey=pubkey, 580 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 581 + runner=NoCallRunner(), 582 + verifier=fake, 583 + clock=lambda: NOW, 584 + pinned_key_id=TEST_KEY_ID, 585 + pinned_pubkey_sha256=pubkey_sha, 586 + ) 587 + assert "trusted comment mismatch" in exc.value.failures[0].error 588 + 589 + 590 + def test_pubkey_sha256_mismatch_fails_before_minisign(tmp_path: Path) -> None: 591 + _repo, commit, _bundle = _advisory_repo(tmp_path) 592 + _receipt, _signature, pubkey, _pubkey_sha, _fake = _write_packet( 593 + tmp_path, commit=commit 594 + ) 595 + 596 + with pytest.raises(audit.ReleasePolicyError): 597 + audit._validate_pubkey_binding( 598 + pubkey, 599 + pinned_key_id=TEST_KEY_ID, 600 + pinned_pubkey_sha256="0" * 64, 601 + ) 602 + 603 + 604 + def test_pubkey_key_id_mismatch_fails_before_minisign(tmp_path: Path) -> None: 605 + pubkey = tmp_path / "pub.key" 606 + pubkey_sha = _write_pubkey(pubkey) 607 + 608 + with pytest.raises(audit.ReleasePolicyError): 609 + audit._validate_pubkey_binding( 610 + pubkey, 611 + pinned_key_id="0000000000000000", 612 + pinned_pubkey_sha256=pubkey_sha, 613 + ) 614 + 615 + 616 + @pytest.mark.parametrize( 617 + "raw", 618 + [ 619 + b"untrusted comment\nnot base64\n", 620 + b"untrusted comment\nRWQ=\n", 621 + b"untrusted comment\n" + base64.b64encode(b"XX" + b"\x00" * 40) + b"\n", 622 + b"only one line\n", 623 + ], 624 + ) 625 + def test_pubkey_blob_shape_is_strict(tmp_path: Path, raw: bytes) -> None: 626 + pubkey = tmp_path / "pub.key" 627 + pubkey.write_bytes(raw) 628 + pubkey_sha = hashlib.sha256(raw).hexdigest() 629 + 630 + with pytest.raises(audit.ReleasePolicyError): 631 + audit._validate_pubkey_binding( 632 + pubkey, 633 + pinned_key_id=TEST_KEY_ID, 634 + pinned_pubkey_sha256=pubkey_sha, 635 + ) 636 + 637 + 638 + def test_signature_mutation_fails(tmp_path: Path) -> None: 639 + root = _audit_root(tmp_path) 640 + _repo, commit, bundle = _advisory_repo(tmp_path) 641 + receipt, signature, pubkey, pubkey_sha, fake = _write_packet( 642 + tmp_path, commit=commit 643 + ) 644 + signature.write_text( 645 + signature.read_text(encoding="utf-8").replace("=", "A", 1), encoding="utf-8" 646 + ) 647 + 648 + with pytest.raises(audit.ReleasePolicyError): 649 + audit.audit_advisory_mirror( 650 + root, 651 + bundle=bundle, 652 + receipt=receipt, 653 + pubkey=pubkey, 654 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 655 + runner=NoCallRunner(), 656 + verifier=fake, 657 + clock=lambda: NOW, 658 + pinned_key_id=TEST_KEY_ID, 659 + pinned_pubkey_sha256=pubkey_sha, 660 + ) 661 + 662 + 663 + @pytest.mark.parametrize( 664 + "utc", 665 + ["2026-07-24T12:06:00Z", "2026-07-23T11:59:59Z"], 666 + ) 667 + def test_receipt_future_and_stale_times_fail(tmp_path: Path, utc: str) -> None: 668 + _repo, commit, _bundle = _advisory_repo(tmp_path) 669 + receipt = tmp_path / "freshness.json" 670 + receipt.write_bytes(_receipt_bytes(commit, utc)) 671 + authority = audit._read_receipt_authority(receipt) 672 + 673 + with pytest.raises(audit.ReleasePolicyError): 674 + audit._validate_receipt_freshness(authority, clock=lambda: NOW) 675 + 676 + 677 + def test_minisign_preflight_uses_product_binary_check( 678 + tmp_path: Path, 679 + monkeypatch: pytest.MonkeyPatch, 680 + ) -> None: 681 + calls: list[tuple[str, object]] = [] 682 + 683 + def check(minisign: str) -> str: 684 + calls.append(("check", minisign)) 685 + return "minisign 0.12" 686 + 687 + class Local: 688 + def __init__( 689 + self, *, secret_key: Path, public_key: Path, minisign: str 690 + ) -> None: 691 + calls.append(("init", (secret_key, public_key, minisign))) 692 + 693 + def verify_file( 694 + self, 695 + message_path: Path, 696 + signature_path: Path, 697 + *, 698 + expected_trusted_comment: str, 699 + ) -> None: 700 + calls.append( 701 + ("verify", (message_path, signature_path, expected_trusted_comment)) 702 + ) 703 + 704 + monkeypatch.setattr(audit, "check_minisign_binary", check) 705 + monkeypatch.setattr(audit, "LocalMinisignSigner", Local) 706 + verifier = audit.PublicKeyMinisignVerifier( 707 + tmp_path / "pub.key", minisign="minisign-test" 708 + ) 709 + verifier.check() 710 + verifier.verify_file( 711 + tmp_path / "freshness.json", 712 + tmp_path / "freshness.json.minisig", 713 + expected_trusted_comment="comment", 714 + ) 715 + 716 + assert calls[0] == ("check", "minisign-test") 717 + assert calls[1][0] == "init" 718 + assert calls[2][0] == "verify" 719 + 720 + 721 + def test_bundle_verify_failure_stops_before_clone_and_cargo(tmp_path: Path) -> None: 722 + root = _audit_root(tmp_path) 723 + _repo, commit, bundle = _advisory_repo(tmp_path) 724 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 725 + tmp_path, commit=commit 726 + ) 727 + runner = FakeGitRunner(heads_commit=commit, fail="bundle verify") 728 + 729 + with pytest.raises(audit.ReleasePolicyError): 730 + audit.audit_advisory_mirror( 731 + root, 732 + bundle=bundle, 733 + receipt=receipt, 734 + pubkey=pubkey, 735 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 736 + runner=runner, 737 + verifier=fake, 738 + clock=lambda: NOW, 739 + pinned_key_id=TEST_KEY_ID, 740 + pinned_pubkey_sha256=pubkey_sha, 741 + ) 742 + assert not any(command[:2] == ["git", "clone"] for command in runner.events) 743 + assert not any(command[-2:] == ["check", "advisories"] for command in runner.events) 744 + 745 + 746 + @pytest.mark.parametrize( 747 + "stdout", 748 + [ 749 + "a" * 40 + " HEAD\n", 750 + "a" * 40 + " refs/heads/main\n", 751 + "a" * 40 + " HEAD\n" + "a" * 40 + " refs/heads/main\n" + "a" * 40 + " refs/x\n", 752 + "b" * 40 + " HEAD\n" + "a" * 40 + " refs/heads/main\n", 753 + "malformed\n" + "a" * 40 + " refs/heads/main\n", 754 + ], 755 + ) 756 + def test_bundle_heads_must_be_exact_head_and_main(stdout: str) -> None: 757 + with pytest.raises(audit.ReleasePolicyError): 758 + audit._parse_bundle_heads(stdout, synced_commit="a" * 40) 759 + 760 + 761 + def test_clone_head_must_match_receipt_commit(tmp_path: Path) -> None: 762 + root = _audit_root(tmp_path) 763 + _repo, commit, bundle = _advisory_repo(tmp_path) 764 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 765 + tmp_path, commit=commit 766 + ) 767 + runner = FakeGitRunner(heads_commit=commit, clone_commit="b" * 40) 768 + 769 + with pytest.raises(audit.ReleasePolicyError): 770 + audit.audit_advisory_mirror( 771 + root, 772 + bundle=bundle, 773 + receipt=receipt, 774 + pubkey=pubkey, 775 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 776 + runner=runner, 777 + verifier=fake, 778 + clock=lambda: NOW, 779 + pinned_key_id=TEST_KEY_ID, 780 + pinned_pubkey_sha256=pubkey_sha, 781 + ) 782 + 783 + 784 + def test_zero_advisory_clone_fails_before_cargo(tmp_path: Path) -> None: 785 + root = _audit_root(tmp_path) 786 + _repo, commit, bundle = _advisory_repo(tmp_path, advisory_count=0) 787 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 788 + tmp_path, commit=commit 789 + ) 790 + runner = HybridRunner() 791 + 792 + with pytest.raises(audit.ReleasePolicyError): 793 + audit.audit_advisory_mirror( 794 + root, 795 + bundle=bundle, 796 + receipt=receipt, 797 + pubkey=pubkey, 798 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 799 + runner=runner, 800 + verifier=fake, 801 + clock=lambda: NOW, 802 + pinned_key_id=TEST_KEY_ID, 803 + pinned_pubkey_sha256=pubkey_sha, 804 + ) 805 + assert runner.check_count == 0 806 + 807 + 808 + def test_discovery_run_nonzero_is_expected_when_debug_line_present( 809 + tmp_path: Path, 810 + ) -> None: 811 + output, runner, _root, _bundle, _receipt, _pubkey = _invoke_green( 812 + tmp_path, 813 + runner=HybridRunner(discovery_exit=23), 814 + ) 815 + 816 + assert json.loads(output)["verdict"] == "pass" 817 + assert runner.check_count == 2 818 + 819 + 820 + @pytest.mark.parametrize("scanned", [Path("/outside/db"), Path("nested/child")]) 821 + def test_discovery_path_must_be_direct_child_of_temp_parent( 822 + tmp_path: Path, 823 + scanned: Path, 824 + ) -> None: 825 + parent = tmp_path / "parent" 826 + parent.mkdir() 827 + if not scanned.is_absolute(): 828 + scanned = parent / scanned 829 + 830 + with pytest.raises(audit.ReleasePolicyError): 831 + audit._assert_direct_child(scanned, parent) 832 + 833 + 834 + def test_discovered_path_must_not_preexist(tmp_path: Path) -> None: 835 + root = _audit_root(tmp_path) 836 + _repo, commit, bundle = _advisory_repo(tmp_path) 837 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 838 + tmp_path, commit=commit 839 + ) 840 + temp_root = tmp_path / "temp" 841 + preexisting = temp_root / "db-root" / DERIVED_NAME 842 + preexisting.mkdir(parents=True) 843 + 844 + with pytest.raises(audit.ReleasePolicyError): 845 + audit.audit_advisory_mirror( 846 + root, 847 + bundle=bundle, 848 + receipt=receipt, 849 + pubkey=pubkey, 850 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 851 + runner=HybridRunner(), 852 + verifier=fake, 853 + clock=lambda: NOW, 854 + temp_path_factory=lambda _label: temp_root, 855 + cleanup_rmdir=lambda path: None, 856 + pinned_key_id=TEST_KEY_ID, 857 + pinned_pubkey_sha256=pubkey_sha, 858 + ) 859 + 860 + 861 + def test_alternate_or_ambient_database_substitution_is_rejected(tmp_path: Path) -> None: 862 + runner = HybridRunner(final_scanned_path=tmp_path / "other-db") 863 + 864 + with pytest.raises(audit.ReleasePolicyError) as exc: 865 + _invoke_green(tmp_path, runner=runner) 866 + assert exc.value.failures[0].actual == "redacted" 867 + 868 + 869 + def test_runner_never_sees_remote_git_or_cargo_fetch_operations(tmp_path: Path) -> None: 870 + _output, runner, _root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) 871 + flattened = [" ".join(command) for command in runner.events] 872 + 873 + assert not any("fetch db" in item for item in flattened) 874 + assert not any(item.startswith("git fetch") for item in flattened) 875 + assert not any(item.startswith("git pull") for item in flattened) 876 + assert not any(item.startswith("git ls-remote") for item in flattened) 877 + assert not any("github.com" in item for item in flattened) 878 + 879 + 880 + def test_final_cargo_deny_failure_is_redacted_and_no_success(tmp_path: Path) -> None: 881 + runner = HybridRunner( 882 + final_exit=1, 883 + final_stderr_extra=( 884 + "/private/path TOKEN=abc ghp_abcdefghijklmnopqrst localhost " 885 + "ssh://mirror.example.invalid/rustsec-advisory-db.git" 886 + ), 887 + ) 888 + 889 + with pytest.raises(audit.ReleasePolicyError) as exc: 890 + _invoke_green(tmp_path, runner=runner) 891 + text = "\n".join(failure.actual for failure in exc.value.failures) 892 + assert "mirror.example.invalid" not in text 893 + assert "/private/path" not in text 894 + assert "TOKEN=" not in text 895 + assert "ghp_" not in text 896 + 897 + 898 + def test_child_output_redaction_masks_locator_temp_path_and_token_canaries() -> None: 899 + redacted = audit._redact_child_output( 900 + "secret /tmp/private TOKEN=value ghp_abcdefghijklmnopqrst host.local", 901 + secrets={"/tmp/private"}, 902 + ) 903 + 904 + assert "/tmp/private" not in redacted 905 + assert "TOKEN=" not in redacted 906 + assert "ghp_" not in redacted 907 + assert audit.validate_public_evidence_text("child-output", redacted) == [] 908 + 909 + 910 + def test_cleanup_failure_suppresses_success_and_combines_errors(tmp_path: Path) -> None: 911 + def fail_cleanup(_path: Path) -> None: 912 + raise OSError("cleanup failed") 913 + 914 + root = _audit_root(tmp_path / "cleanup") 915 + _repo, commit, bundle = _advisory_repo(tmp_path / "cleanup") 916 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 917 + tmp_path / "cleanup", commit=commit 918 + ) 919 + with pytest.raises(audit.ReleasePolicyError) as cleanup_exc: 920 + audit.audit_advisory_mirror( 921 + root, 922 + bundle=bundle, 923 + receipt=receipt, 924 + pubkey=pubkey, 925 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 926 + runner=HybridRunner(), 927 + verifier=fake, 928 + clock=lambda: NOW, 929 + cleanup_rmdir=fail_cleanup, 930 + pinned_key_id=TEST_KEY_ID, 931 + pinned_pubkey_sha256=pubkey_sha, 932 + ) 933 + assert any( 934 + "cleanup failed" in failure.error for failure in cleanup_exc.value.failures 935 + ) 936 + 937 + 938 + def test_cleanup_failure_combines_with_primary_error(tmp_path: Path) -> None: 939 + def fail_cleanup(_path: Path) -> None: 940 + raise OSError("cleanup failed") 941 + 942 + root = _audit_root(tmp_path) 943 + _repo, commit, bundle = _advisory_repo(tmp_path) 944 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 945 + tmp_path, commit=commit 946 + ) 947 + 948 + with pytest.raises(audit.ReleasePolicyError) as exc: 949 + audit.audit_advisory_mirror( 950 + root, 951 + bundle=bundle, 952 + receipt=receipt, 953 + pubkey=pubkey, 954 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 955 + runner=HybridRunner(final_exit=1), 956 + verifier=fake, 957 + clock=lambda: NOW, 958 + cleanup_rmdir=fail_cleanup, 959 + pinned_key_id=TEST_KEY_ID, 960 + pinned_pubkey_sha256=pubkey_sha, 961 + ) 962 + errors = [failure.error for failure in exc.value.failures] 963 + assert "advisory mirror cargo-deny final check failed" in errors 964 + assert any("cleanup failed" in error for error in errors) 965 + 966 + 967 + def test_exact_success_schema_and_witness_binding(tmp_path: Path) -> None: 968 + output, _runner, root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) 969 + payload = json.loads(output) 970 + 971 + assert payload == { 972 + "product": "solstone-journal", 973 + "advisory_cohort": audit.ADVISORY_COHORT_ID, 974 + "synced_commit": payload["synced_commit"], 975 + "receipt_utc": RECEIPT_UTC, 976 + "max_age": 86400, 977 + "checked_at": "2026-07-24T12:00:00Z", 978 + "cargo_lock_sha256": hashlib.sha256( 979 + (root / "core" / "Cargo.lock").read_bytes() 980 + ).hexdigest(), 981 + "cargo_deny_version": "0.20.2", 982 + "verdict": "pass", 983 + } 984 + serialized = json.dumps(payload, separators=(",", ":")).encode("utf-8") + b"\n" 985 + assert output == serialized 986 + assert b"mirror.example.invalid" not in output 987 + 988 + 989 + def test_success_inventory_is_non_destructive( 990 + tmp_path: Path, 991 + monkeypatch: pytest.MonkeyPatch, 992 + ) -> None: 993 + root = _audit_root(tmp_path) 994 + packet_root = tmp_path / "packet" 995 + packet_root.mkdir() 996 + cargo_home = tmp_path / "cargo-home" 997 + cargo_home.mkdir() 998 + (cargo_home / "preserve.txt").write_text("ambient cargo\n", encoding="utf-8") 999 + monkeypatch.setenv("CARGO_HOME", str(cargo_home)) 1000 + temp_root = tmp_path / "audit-temp" 1001 + _repo, commit, bundle = _advisory_repo(packet_root) 1002 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 1003 + packet_root, commit=commit 1004 + ) 1005 + before_root = _inventory(root) 1006 + before_packet = _inventory(packet_root) 1007 + before_cargo = _inventory(cargo_home) 1008 + 1009 + audit.audit_advisory_mirror( 1010 + root, 1011 + bundle=bundle, 1012 + receipt=receipt, 1013 + pubkey=pubkey, 1014 + locator="ssh://mirror.example.invalid/rustsec-advisory-db.git", 1015 + runner=HybridRunner(), 1016 + verifier=fake, 1017 + clock=lambda: NOW, 1018 + temp_path_factory=lambda _label: temp_root, 1019 + pinned_key_id=TEST_KEY_ID, 1020 + pinned_pubkey_sha256=pubkey_sha, 1021 + ) 1022 + 1023 + assert _inventory(root) == before_root 1024 + assert _inventory(packet_root) == before_packet 1025 + assert _inventory(cargo_home) == before_cargo 1026 + assert not temp_root.exists() 1027 + 1028 + 1029 + @pytest.mark.parametrize( 1030 + "stage", 1031 + ["input", "locator", "pubkey", "signature", "time", "bundle", "cargo", "cleanup"], 1032 + ) 1033 + def test_failure_inventory_is_non_destructive( 1034 + tmp_path: Path, 1035 + monkeypatch: pytest.MonkeyPatch, 1036 + stage: str, 1037 + ) -> None: 1038 + root = _audit_root(tmp_path) 1039 + packet_root = tmp_path / "packet" 1040 + packet_root.mkdir() 1041 + cargo_home = tmp_path / "cargo-home" 1042 + cargo_home.mkdir() 1043 + (cargo_home / "preserve.txt").write_text("ambient cargo\n", encoding="utf-8") 1044 + monkeypatch.setenv("CARGO_HOME", str(cargo_home)) 1045 + temp_root = tmp_path / "audit-temp" 1046 + _repo, commit, bundle = _advisory_repo(packet_root) 1047 + receipt, _signature, pubkey, pubkey_sha, fake = _write_packet( 1048 + packet_root, commit=commit 1049 + ) 1050 + locator = "ssh://mirror.example.invalid/rustsec-advisory-db.git" 1051 + runner: object = HybridRunner(final_exit=1) 1052 + pinned_pubkey_sha = pubkey_sha 1053 + cleanup_rmdir = audit._remove_tree 1054 + if stage == "input": 1055 + bundle = packet_root / "missing.bundle" 1056 + runner = NoCallRunner() 1057 + elif stage == "locator": 1058 + locator = "" 1059 + runner = NoCallRunner() 1060 + elif stage == "pubkey": 1061 + pinned_pubkey_sha = "0" * 64 1062 + runner = NoCallRunner() 1063 + elif stage == "signature": 1064 + (receipt.parent / f"{receipt.name}.minisig").write_text( 1065 + "bad signature\n", 1066 + encoding="utf-8", 1067 + ) 1068 + runner = NoCallRunner() 1069 + elif stage == "time": 1070 + utc = "2026-07-24T12:06:00Z" 1071 + receipt.write_bytes(_receipt_bytes(commit, utc)) 1072 + fake.sign_file( 1073 + receipt, 1074 + receipt.parent / f"{receipt.name}.minisig", 1075 + trusted_comment=_trusted_comment(commit, utc), 1076 + ) 1077 + runner = NoCallRunner() 1078 + elif stage == "bundle": 1079 + runner = FakeGitRunner(heads_commit=commit, fail="bundle verify") 1080 + elif stage == "cleanup": 1081 + 1082 + def fail_cleanup(_path: Path) -> None: 1083 + raise OSError("cleanup failed") 1084 + 1085 + runner = HybridRunner() 1086 + cleanup_rmdir = fail_cleanup 1087 + before_root = _inventory(root) 1088 + before_packet = _inventory(packet_root) 1089 + before_cargo = _inventory(cargo_home) 1090 + 1091 + with pytest.raises(audit.ReleasePolicyError): 1092 + audit.audit_advisory_mirror( 1093 + root, 1094 + bundle=bundle, 1095 + receipt=receipt, 1096 + pubkey=pubkey, 1097 + locator=locator, 1098 + runner=runner, 1099 + verifier=fake, 1100 + clock=lambda: NOW, 1101 + temp_path_factory=lambda _label: temp_root, 1102 + cleanup_rmdir=cleanup_rmdir, 1103 + pinned_key_id=TEST_KEY_ID, 1104 + pinned_pubkey_sha256=pinned_pubkey_sha, 1105 + ) 1106 + 1107 + assert _inventory(root) == before_root 1108 + assert _inventory(packet_root) == before_packet 1109 + assert _inventory(cargo_home) == before_cargo 1110 + if stage == "cleanup": 1111 + assert temp_root.exists() 1112 + else: 1113 + assert not temp_root.exists() 1114 + 1115 + 1116 + def test_audit_config_bytes_omits_fetch_head_and_staleness_fields( 1117 + tmp_path: Path, 1118 + ) -> None: 1119 + cfg = audit.audit_config_bytes( 1120 + b'[licenses]\nallow = ["MIT"]\n', 1121 + db_root=tmp_path / "db-root", 1122 + db_urls=("ssh://mirror.example.invalid/rustsec-advisory-db.git",), 1123 + ).decode("utf-8") 1124 + 1125 + assert "git-fetch-with-cli" not in cfg 1126 + assert "maximum-db-staleness" not in cfg 1127 + assert "[advisories]" in cfg 1128 + with pytest.raises(audit.ReleasePolicyError): 1129 + audit.audit_config_bytes( 1130 + b'[advisories]\ndb-path = "x"\n', 1131 + db_root=tmp_path / "db-root", 1132 + db_urls=("ssh://mirror.example.invalid/rustsec-advisory-db.git",), 1133 + ) 1134 + 1135 + 1136 + def test_main_prints_success_bytes_only_on_success( 1137 + monkeypatch: pytest.MonkeyPatch, 1138 + capsys: pytest.CaptureFixture[str], 1139 + ) -> None: 1140 + monkeypatch.setattr( 1141 + audit, "audit_advisory_mirror", lambda *args, **kwargs: b'{"ok":1}\n' 1142 + ) 1143 + 1144 + result = audit.main( 1145 + [ 1146 + "--bundle", 1147 + "bundle", 1148 + "--receipt", 1149 + "receipt", 1150 + "--pubkey", 1151 + "pubkey", 1152 + "--locator", 1153 + "ssh://mirror.example.invalid/rustsec-advisory-db.git", 1154 + ] 1155 + ) 1156 + 1157 + captured = capsys.readouterr() 1158 + assert result == 0 1159 + assert captured.out == '{"ok":1}\n' 1160 + assert captured.err == "" 1161 + 1162 + 1163 + @pytest.mark.parametrize("empty_name", ["bundle", "receipt", "pubkey", "locator"]) 1164 + def test_main_empty_inputs_fail_before_audit_orchestration( 1165 + monkeypatch: pytest.MonkeyPatch, 1166 + capsys: pytest.CaptureFixture[str], 1167 + empty_name: str, 1168 + ) -> None: 1169 + calls = [] 1170 + 1171 + def unexpected(*args, **kwargs): 1172 + calls.append((args, kwargs)) 1173 + raise AssertionError("audit should not run") 1174 + 1175 + monkeypatch.setattr(audit, "audit_advisory_mirror", unexpected) 1176 + values = { 1177 + "bundle": "bundle", 1178 + "receipt": "receipt", 1179 + "pubkey": "pubkey", 1180 + "locator": "ssh://mirror.example.invalid/rustsec-advisory-db.git", 1181 + } 1182 + values[empty_name] = "" 1183 + 1184 + result = audit.main( 1185 + [ 1186 + "--bundle", 1187 + values["bundle"], 1188 + "--receipt", 1189 + values["receipt"], 1190 + "--pubkey", 1191 + values["pubkey"], 1192 + "--locator", 1193 + values["locator"], 1194 + ] 1195 + ) 1196 + 1197 + captured = capsys.readouterr() 1198 + assert result == 1 1199 + assert calls == [] 1200 + assert captured.out == "" 1201 + assert f"input {empty_name} is empty" in captured.err 1202 + 1203 + 1204 + def test_main_prints_redacted_failures_only_on_error( 1205 + monkeypatch: pytest.MonkeyPatch, 1206 + capsys: pytest.CaptureFixture[str], 1207 + ) -> None: 1208 + def fail(*args, **kwargs): 1209 + raise audit.ReleasePolicyError( 1210 + [ 1211 + audit._failure_record( 1212 + "failed", 1213 + expected="public", 1214 + actual="/private/path TOKEN=value", 1215 + ) 1216 + ] 1217 + ) 1218 + 1219 + monkeypatch.setattr(audit, "audit_advisory_mirror", fail) 1220 + result = audit.main( 1221 + [ 1222 + "--bundle", 1223 + "bundle", 1224 + "--receipt", 1225 + "receipt", 1226 + "--pubkey", 1227 + "pubkey", 1228 + "--locator", 1229 + "ssh://mirror.example.invalid/rustsec-advisory-db.git", 1230 + ] 1231 + ) 1232 + 1233 + captured = capsys.readouterr() 1234 + assert result == 1 1235 + assert captured.out == "" 1236 + assert "/private/path" not in captured.err 1237 + assert "TOKEN=" not in captured.err
+24 -13
tests/test_rust_policy_baseline.py
··· 64 64 assert command in block 65 65 66 66 67 - def test_audit_recipe_refreshes_then_checks_offline_fail_closed() -> None: 67 + def test_audit_recipe_uses_signed_packet_without_fetch_db() -> None: 68 68 block = _makefile_block("audit", "skills") 69 - fetch = "cargo deny --manifest-path $(RUST_MANIFEST) fetch db" 70 - check = ( 71 - "cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline " 72 - "check advisories" 73 - ) 74 69 required_commands = [ 75 70 "scripts/check_release_preflight.py cargo-deny", 76 - fetch, 77 - check, 71 + "scripts/advisory_mirror_audit.py", 72 + "AUDIT_ADVISORY_BUNDLE", 73 + "AUDIT_ADVISORY_RECEIPT", 74 + "AUDIT_ADVISORY_PUBKEY", 75 + "AUDIT_ADVISORY_LOCATOR", 76 + "--bundle", 77 + "--receipt", 78 + "--pubkey", 79 + "--locator", 78 80 ] 79 81 80 82 assert required_commands 81 83 for command in required_commands: 82 84 assert command in block 83 - assert block.index(fetch) < block.index(check) 85 + assert "fetch db" not in block 84 86 85 - fetch_line = next(line for line in block.splitlines() if fetch in line) 86 - assert "no current advisory result was produced" in fetch_line 87 - assert "ERROR: RustSec advisory refresh failed" in fetch_line 88 - assert "exit 1" in fetch_line 87 + # make audit stdout must be exactly the witness JSON; the private locator must never be echoed. 88 + preflight_line = next( 89 + line 90 + for line in block.splitlines() 91 + if "check_release_preflight.py cargo-deny" in line 92 + ) 93 + audit_line = next( 94 + line 95 + for line in block.splitlines() 96 + if "scripts/advisory_mirror_audit.py" in line 97 + ) 98 + assert ">&2" in preflight_line 99 + assert audit_line.lstrip("\t").startswith("@") 89 100 90 101 91 102 def test_release_candidate_driver_binds_policy_before_artifact_construction() -> None: