personal memory agent
0

Configure Feed

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

solstone / tests / test_advisory_mirror_audit.py
43 kB 1237 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import base64 7import hashlib 8import json 9import subprocess 10from collections.abc import Mapping, Sequence 11from datetime import UTC, datetime 12from pathlib import Path 13from typing import Any 14 15import pytest 16 17import scripts.advisory_mirror_audit as audit 18from scripts.transparency_signing import FakeTransparencySigner 19 20NOW = datetime(2026, 7, 24, 12, 0, tzinfo=UTC) 21RECEIPT_UTC = "2026-07-24T11:30:00Z" 22TEST_KEY_ID = "A1B2C3D4E5F60708" 23DERIVED_NAME = "rustsec-advisory-db.git-testderived" 24 25 26def _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 38def _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 62def _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 98def _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 108def _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 114def _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 122def _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 128def _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 151class 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 162class 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 218class 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 267def _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 274def _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 304def _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 315def 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 353def 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"]) 366def 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"]) 402def 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"]) 438def 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) 516def 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) 530def 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) 555def 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 565def 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 590def 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 604def 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) 625def 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 638def 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) 667def 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 677def 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 721def 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) 756def 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 761def 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 784def 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 808def 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")]) 821def 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 834def 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 861def 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 869def 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 880def 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 898def 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 910def 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 938def 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 967def 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 989def 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) 1033def 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 1116def 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 1136def 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"]) 1164def 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 1204def 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