diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83c788b..5b9421f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,63 +4,301 @@ on: pull_request: push: +permissions: + contents: read + jobs: - test: + hoxline-trust-boundaries: runs-on: ubuntu-latest + env: + HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA: 5c6127f5acc1031bae2528df3ce1f197da882100 + HAWKINS_HOXLINE_EVENT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} strategy: + fail-fast: false matrix: python-version: ["3.11", "3.12"] steps: - - uses: actions/checkout@v4 + - name: Check out Hoxline PR revision + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: org/hoxline + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Check out command center + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: HawkinsOperations/.github + ref: 5c6127f5acc1031bae2528df3ce1f197da882100 + path: org/.github + fetch-depth: 0 + persist-credentials: false + + - name: Resolve reviewed sibling revisions + id: reviewed + shell: bash + run: | + python - <<'PY' + from pathlib import Path + import json + import os + + manifest_path = Path("org/.github/governance/CONVERGENCE_SOURCE_MANIFEST.json") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + entries = { + item["repository"]: item + for item in manifest.get("repositories", []) + if isinstance(item, dict) and isinstance(item.get("repository"), str) + } + required = { + "hawkinsoperations-detections": "detections", + "hawkinsoperations-validation": "validation", + "hawkinsoperations-platform": "platform", + "hawkinsoperations-proof": "proof", + "hawkinsoperations-website": "website", + } + if set(entries) != { + ".github", + *required, + "hoxline", + }: + raise SystemExit(f"reviewed manifest must contain exactly seven repositories; got {sorted(entries)}") + with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: + for repository, output_name in required.items(): + revision = entries[repository].get("revision") + if not isinstance(revision, str) or len(revision) != 40: + raise SystemExit(f"{repository} lacks an immutable reviewed revision") + int(revision, 16) + output.write(f"{output_name}={revision}\n") + PY + + - name: Check out detection authority + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: HawkinsOperations/hawkinsoperations-detections + ref: ${{ steps.reviewed.outputs.detections }} + path: org/hawkinsoperations-detections + fetch-depth: 0 + persist-credentials: false + + - name: Check out validation authority + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: HawkinsOperations/hawkinsoperations-validation + ref: ${{ steps.reviewed.outputs.validation }} + path: org/hawkinsoperations-validation + fetch-depth: 0 + persist-credentials: false + + - name: Check out platform authority + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: HawkinsOperations/hawkinsoperations-platform + ref: ${{ steps.reviewed.outputs.platform }} + path: org/hawkinsoperations-platform + fetch-depth: 0 + persist-credentials: false + + - name: Check out proof authority + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: HawkinsOperations/hawkinsoperations-proof + ref: ${{ steps.reviewed.outputs.proof }} + path: org/hawkinsoperations-proof + fetch-depth: 0 + persist-credentials: false + + - name: Check out website rendering contract + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: HawkinsOperations/hawkinsoperations-website + ref: ${{ steps.reviewed.outputs.website }} + path: org/hawkinsoperations-website + fetch-depth: 0 + persist-credentials: false + + - name: Verify and record exact seven-repository checkout + working-directory: org/hoxline + shell: bash + run: | + python - <<'PY' + from pathlib import Path + import json + import os + import subprocess - - uses: actions/setup-python@v5 + root = Path("..").resolve() + manifest = json.loads( + (root / ".github" / "governance" / "CONVERGENCE_SOURCE_MANIFEST.json").read_text( + encoding="utf-8" + ) + ) + reviewed = { + item["repository"]: item.get("revision") + for item in manifest["repositories"] + if item["repository"] not in {".github", "hoxline"} + } + expected = [ + ".github", + "hawkinsoperations-detections", + "hawkinsoperations-validation", + "hawkinsoperations-platform", + "hawkinsoperations-proof", + "hawkinsoperations-website", + "hoxline", + ] + immutable = { + ".github": os.environ["HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA"], + **reviewed, + "hoxline": os.environ["HAWKINS_HOXLINE_EVENT_SHA"], + } + actual = sorted(path.name for path in root.iterdir() if (path / ".git").exists()) + if actual != sorted(expected): + raise SystemExit(f"exact seven-repository checkout required; expected={sorted(expected)}, actual={actual}") + for name in expected: + sha = subprocess.check_output( + ["git", "-C", str(root / name), "rev-parse", "HEAD"], + text=True, + ).strip() + print(f"{name}={sha}") + if sha != immutable[name]: + raise SystemExit( + f"{name} checkout mismatch: expected {immutable[name]}, got {sha}" + ) + PY + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} - - name: Install package and test dependencies + - name: Install package and declared test dependencies + working-directory: org/hoxline run: | - python -m pip install --upgrade pip python -m pip install -e ".[test]" - - name: Run tests - run: python -m pytest + - name: Compile + working-directory: org/hoxline + run: python -B -m compileall src tests + + - name: Reject retired tracked vocabulary + working-directory: org/hoxline + run: | + python -B - <<'PY' + from pathlib import Path + from hoxline.review_engine import verify_tracked_vocabulary + + errors = verify_tracked_vocabulary(Path(".")) + if errors: + raise SystemExit("\n".join(errors)) + print("Tracked vocabulary verification: PASS") + PY + + - name: Run unittest suite + working-directory: org/hoxline + run: python -B -m unittest discover -s tests + + - name: Run full pytest suite including hostile replay cases + working-directory: org/hoxline + run: python -B -m pytest - - name: Passing example succeeds + - name: Generate Case Growth pair from exact seven checked sources + working-directory: org/hoxline + run: >- + python -B -m hoxline.cli case-growth index + --repo-root .. + --format json + --paired-output-base "${{ runner.temp }}/current-case-growth-index" + + - name: Verify checked Case Growth pair + working-directory: org/hoxline + run: >- + python -B -m hoxline.cli case-growth verify + --repo-root .. + --snapshot examples/case-growth/current-case-growth-index.json + + - name: Diff checked Case Growth pair + working-directory: org/hoxline + run: >- + python -B -m hoxline.cli case-growth diff + --repo-root .. + --snapshot examples/case-growth/current-case-growth-index.json + --format json + + - name: Run expanded batch and verify complete replay + working-directory: org/hoxline + run: | + python -B -m hoxline review batch run \ + --index examples/review/multi-artifact-review-index-v1.json \ + --output "${{ runner.temp }}/hoxline-batch" \ + --force + python -B -m hoxline review batch verify \ + --run "${{ runner.temp }}/hoxline-batch/batch-machine-state.json" + + - name: Run one-command reviewer demo + working-directory: org/hoxline + run: >- + python -B -m hoxline demo quickstart + --output "${{ runner.temp }}/hoxline-demo" + --force + + - name: Passing Claim Firewall example succeeds + working-directory: org/hoxline run: python -m claimfirewall scan examples/pass.md --policy policy/blocked_claims.yml - - name: Failing example fails + - name: Failing Claim Firewall example is rejected + working-directory: org/hoxline run: | - if python -m claimfirewall scan examples/fail.md --policy policy/blocked_claims.yml; then - echo "Expected failing example to report blocked claims" - exit 1 - fi + python - <<'PY' + import subprocess + import sys + + result = subprocess.run( + [ + sys.executable, + "-m", + "claimfirewall", + "scan", + "examples/fail.md", + "--policy", + "policy/blocked_claims.yml", + ], + check=False, + ) + if result.returncode != 1: + raise SystemExit(f"expected blocked example exit 1, got {result.returncode}") + PY - name: Safe context docs succeed + working-directory: org/hoxline run: python -m claimfirewall scan README.md CLAIM_BOUNDARY.md --policy policy/blocked_claims.yml - - name: JSON output is valid and contains findings + - name: Reject legacy product naming + working-directory: org/hoxline run: | - set +e - python -m claimfirewall scan examples/fail.md --policy policy/blocked_claims.yml --format json > findings.json - status=$? - set -e - if [ "$status" -ne 1 ]; then - echo "Expected JSON scan to exit 1" - exit 1 - fi - python -m json.tool findings.json > /dev/null python - <<'PY' - import json from pathlib import Path - data = json.loads(Path("findings.json").read_text()) - if not data.get("findings"): - raise SystemExit("Expected JSON findings") + import re + + pattern = re.compile(r"c[l]aimlint", re.IGNORECASE) + hits = [] + for path in Path(".").rglob("*"): + if not path.is_file() or ".git" in path.parts: + continue + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + if pattern.search(text): + hits.append(path.as_posix()) + if hits: + raise SystemExit(f"legacy product naming found: {hits}") PY - - name: Old name search + - name: Verify no tracked drift + working-directory: org/hoxline run: | - if rg -n "c[l]aimlint|C[l]aimlint|C[L]AIMLINT" .; then - echo "Old naming found" - exit 1 - fi + git diff --check + test -z "$(git status --porcelain --untracked-files=no)" diff --git a/README.md b/README.md index 655b412..2552612 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ python -B -m hoxline demo quickstart --output .hoxline/demo-runs/self-test --for python -B -m hoxline demo verify --input .hoxline/demo-runs/self-test/run-summary.json ``` -The command writes `.hoxline/demo-runs//` with `intake.json`, `evidence-graph.json`, `telemetry-contract-check.json`, `validation-result.json`, `synthetic-signal.json`, `enrichment.json`, `triage-summary.md`, `proofcard.json`, `proofcard.md`, `claim-authority.json`, `reviewer-pack.md`, and `run-summary.json`. +The command writes `.hoxline/demo-runs//` with `intake.json`, `evidence-graph.json`, `telemetry-contract-check.json`, `validation-result.json`, `controlled-test-signal.json`, `enrichment.json`, `triage-summary.md`, `proofcard.json`, `proofcard.md`, `claim-authority.json`, `reviewer-pack.md`, and `run-summary.json`. -What it proves: Hoxline can carry a synthetic HO-DET-010 fixture through intake, evidence graph, telemetry contract check, controlled validation, fixture-only signal simulation, enrichment, triage, ProofCard, Claim Authority, blocked claims, and reviewer packaging. +What it proves: Hoxline can carry a controlled-test HO-DET-010 fixture through intake, evidence graph, telemetry contract check, controlled validation, fixture-only signal simulation, enrichment, triage, ProofCard, Claim Authority, blocked claims, and reviewer packaging. What it does not prove: live runtime behavior, public signal observation, public-safe status, production readiness, SOCaaS deployment, customer deployment, autonomous SOC operation, AI approval, analyst approval, final authorization, or case closure. The demo does not touch endpoints, users, groups, Wazuh, Splunk, Cribl, private infrastructure, ledgers, or website proof state. @@ -46,7 +46,7 @@ python -B -m hoxline review verify --run .hoxline/runs//machine-state.js The command writes `.hoxline/runs//` with `artifact-manifest.json`, stage outputs, `proofcard.json`, `proofcard.md`, `claim-authority.json`, `reviewer-pack.md`, `machine-state.json`, and `run-summary.json`. -What it proves: Hoxline can take a public sanitized synthetic artifact manifest, run deterministic local review stages, write replayable machine state, generate reviewer artifacts, and block unsupported claims. +What it proves: Hoxline can take a public sanitized controlled-test artifact manifest, run deterministic local review stages, write replayable machine state, generate reviewer artifacts, and block unsupported claims. What it does not prove: live runtime behavior, public signal observation, public-safe status, production readiness, SOCaaS deployment, customer deployment, autonomous SOC operation, AI approval, analyst approval, final authorization, or case closure. @@ -141,7 +141,7 @@ References are carried from `hawkinsoperations-platform#64` and `hawkinsoperatio Hoxline also supports private runtime candidate review for artifacts whose source, telemetry contract, validation, private signal, packet verification, and scheduled collector inclusion have been established internally but are not public-safe proof. -Separate from the one-command fixture demo, HO-DET-010 also has private runtime-candidate context that is not published here. The public demo uses only synthetic fixture records and must not be confused with private runtime candidate evidence. HO-DET-010 remains `NOT_PUBLIC_SAFE`; human review is required; AI has no disposition authority; no public proof, ledger append, website proof promotion, production, customer, SOCaaS, fleet, analyst-approved, AI-approved, or case-closure claim is made. +Separate from the one-command fixture demo, HO-DET-010 also has private runtime-candidate context that is not published here. The public demo uses only controlled-test fixture records and must not be confused with private runtime candidate evidence. HO-DET-010 remains `NOT_PUBLIC_SAFE`; human review is required; AI has no disposition authority; no public proof, ledger append, website proof promotion, production, customer, SOCaaS, fleet, analyst-approved, AI-approved, or case-closure claim is made. ## Claim Firewall Claim Firewall is the first Claim Authority enforcement capability inside Hoxline. diff --git a/docs/demo/HOXLINE_ONE_COMMAND_REVIEWER_DEMO_V0.md b/docs/demo/HOXLINE_ONE_COMMAND_REVIEWER_DEMO_V0.md index 876002e..3d3b214 100644 --- a/docs/demo/HOXLINE_ONE_COMMAND_REVIEWER_DEMO_V0.md +++ b/docs/demo/HOXLINE_ONE_COMMAND_REVIEWER_DEMO_V0.md @@ -12,7 +12,7 @@ ai_disposition_authority: false ## Purpose -This demo lets a reviewer clone Hoxline, run one command from the repo root, and see the governed ProofOps loop in about 30 seconds. It uses a bundled synthetic HO-DET-010 fixture for a local Administrators membership-change pattern. It does not create users, change groups, touch endpoints, connect to Wazuh, publish private evidence, or claim live runtime proof. +This demo lets a reviewer clone Hoxline, run one command from the repo root, and see the governed ProofOps loop in about 30 seconds. It uses a bundled controlled-test HO-DET-010 fixture for a local Administrators membership-change pattern. It does not create users, change groups, touch endpoints, connect to Wazuh, publish private evidence, or claim live runtime proof. ## Command @@ -37,7 +37,7 @@ The command writes `.hoxline/demo-runs//` with: - `evidence-graph.json` - `telemetry-contract-check.json` - `validation-result.json` -- `synthetic-signal.json` +- `controlled-test-signal.json` - `enrichment.json` - `triage-summary.md` - `proofcard.json` @@ -64,7 +64,7 @@ The command writes `.hoxline/demo-runs//` with: ## Supported Artifact -The demo supports `HO-DET-010` with a synthetic local Administrators membership-change fixture: +The demo supports `HO-DET-010` with a controlled-test local Administrators membership-change fixture: - positive fixture: `examples/demo/ho-det-010-safe-fixture.json` - negative fixture: `examples/demo/ho-det-010-safe-negative-fixture.json` @@ -73,7 +73,7 @@ The telemetry contract represents Windows Security EventChannel assumptions for ## What It Proves -- Hoxline can generate the reviewer path locally from synthetic fixtures approved for public demo use. +- Hoxline can generate the reviewer path locally from controlled-test fixtures approved for public demo use. - The demo produces structured records for intake, graph linkage, telemetry assumptions, validation, signal simulation, enrichment, triage, ProofCard, Claim Authority, and reviewer packaging. - Claim Authority allows bounded demo wording and blocks unsupported public claims. diff --git a/docs/gauntlet/HOXLINE_GAUNTLET_METRICS_V0.md b/docs/gauntlet/HOXLINE_GAUNTLET_METRICS_V0.md index 88eee19..65915ac 100644 --- a/docs/gauntlet/HOXLINE_GAUNTLET_METRICS_V0.md +++ b/docs/gauntlet/HOXLINE_GAUNTLET_METRICS_V0.md @@ -6,9 +6,9 @@ Proof ceiling: `CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY`. ## What Is Measured -The metrics engine evaluates the controlled synthetic event fixture for the browser-cache / script-interpreter detection-review scenario. It emits numeric JSON for: +The metrics engine evaluates the controlled controlled-test event fixture for the browser-cache / script-interpreter detection-review scenario. It emits numeric JSON for: -* synthetic event volume +* controlled-test event volume * expected positive and negative counts * true positive, true negative, false positive, and false negative counts * precision, recall, F1, and false-positive rate @@ -22,7 +22,7 @@ The metrics engine evaluates the controlled synthetic event fixture for the brow | Metric | Value | Meaning | | --- | ---: | --- | -| events_total | 12 | synthetic events evaluated | +| events_total | 12 | controlled-test events evaluated | | expected_positive | 4 | events expected to match the controlled review rule | | expected_negative | 8 | events expected not to match the controlled review rule | | true_positive | 4 | expected positive events matched | @@ -64,7 +64,7 @@ proof_ceiling: CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY ## What The Numbers Prove -The numbers prove that Hoxline can run a deterministic controlled-fixture evaluation for this one synthetic detection-review artifact and emit measurable JSON. They also show that the fixture has all required telemetry fields, that Claim Authority blocks unsupported wording in the bad release note, and that the ProofCard has all required sections. +The numbers prove that Hoxline can run a deterministic controlled-fixture evaluation for this one controlled-test detection-review artifact and emit measurable JSON. They also show that the fixture has all required telemetry fields, that Claim Authority blocks unsupported wording in the bad release note, and that the ProofCard has all required sections. ## What The Numbers Do Not Prove @@ -74,7 +74,7 @@ This artifact does not prove runtime evidence, signal evidence, customer deploym ```powershell python -B -m hoxline.cli gauntlet metrics ` - --events examples/gauntlet/synthetic-events.json ` + --events examples/gauntlet/controlled-test-events.json ` --artifact examples/gauntlet/sample-artifact.json ` --proofcard examples/gauntlet/sample-proofcard.json ` --claim-output examples/gauntlet/sample-claim-authority-output.json ` @@ -85,7 +85,7 @@ To write the report: ```powershell python -B -m hoxline.cli gauntlet metrics ` - --events examples/gauntlet/synthetic-events.json ` + --events examples/gauntlet/controlled-test-events.json ` --artifact examples/gauntlet/sample-artifact.json ` --proofcard examples/gauntlet/sample-proofcard.json ` --claim-output examples/gauntlet/sample-claim-authority-output.json ` diff --git a/docs/gauntlet/HOXLINE_GAUNTLET_V0.md b/docs/gauntlet/HOXLINE_GAUNTLET_V0.md index 6dc32f1..5d403c7 100644 --- a/docs/gauntlet/HOXLINE_GAUNTLET_V0.md +++ b/docs/gauntlet/HOXLINE_GAUNTLET_V0.md @@ -4,7 +4,7 @@ This gauntlet is a controlled product demo artifact for Hoxline. It shows how an Artifact ID: `HOX-GAUNTLET-001` -Scenario: an AI assistant drafts a synthetic Splunk/SOC detection-review artifact and release note for a browser-cache / ClickFix-style payload extraction detection idea. The fixture is sanitized, contains no malware code, contains no exploit instructions, and does not depend on live telemetry. +Scenario: an AI assistant drafts a controlled-test Splunk/SOC detection-review artifact and release note for a browser-cache / ClickFix-style payload extraction detection idea. The fixture is sanitized, contains no malware code, contains no exploit instructions, and does not depend on live telemetry. Proof ceiling: `CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY`. @@ -31,7 +31,7 @@ AI-assisted security work ## Three-Minute Reviewer Path 1. Read this page for the boundary and stage map. -2. Open `examples/gauntlet/sample-artifact.json` for the synthetic detection artifact and telemetry contract. +2. Open `examples/gauntlet/sample-artifact.json` for the controlled-test detection artifact and telemetry contract. 3. Compare `examples/gauntlet/bad-release-note.md` with `examples/gauntlet/safe-release-note.md`. 4. Open `examples/gauntlet/sample-evidence-graph.json`, `examples/gauntlet/sample-promotion-state.json`, `examples/gauntlet/sample-proofcard.json`, and `examples/gauntlet/sample-claim-authority-output.json`. 5. Open `docs/gauntlet/HOXLINE_GAUNTLET_METRICS_V0.md` for the numeric Work Impact Metrics v0 output. @@ -55,7 +55,7 @@ Purpose: demonstrate how Hoxline governs an AI-assisted Splunk/SOC detection dra The sample artifact has: -* A synthetic Splunk-style detection review object. +* A controlled-test Splunk-style detection review object. * A telemetry contract describing required fields and fixture-only scope. * Controlled validation with deterministic positive and negative fixture expectations. * Runtime candidate state recorded as `NOT_PROMOTED`. @@ -70,7 +70,7 @@ The sample artifact has: | 1 | AI-assisted security work | Work is marked `ai_assisted=true`. | Intake required. | | 2 | Artifact Intake | Artifact identity, source-control path, scope, and proposed claims are recorded. | Evidence graph node created. | | 3 | Evidence Graph | Artifact, telemetry contract, validation, runtime candidate, signal observation, review, ProofCard, and claim decision nodes are linked. | Traceable state exists. | -| 4 | Telemetry Contract Check | Status is `PASSED_SYNTHETIC_CONTRACT`. | Required synthetic fields are declared. | +| 4 | Telemetry Contract Check | Status is `CONTROLLED_TEST_VALIDATED`. | Required controlled-test fields are declared. | | 5 | Controlled Validation | Status is `PASSED_CONTROLLED_FIXTURES`. | Fixture-only validation supports the safe claim. | | 6 | Runtime Candidate Ledger | Candidate state is `NOT_PROMOTED`. | Runtime proof remains unavailable. | | 7 | Signal Observation | Signal state is `NOT_OBSERVED`. | Signal proof remains unavailable. | @@ -86,7 +86,7 @@ The sample artifact has: * `examples/gauntlet/sample-evidence-graph.json` * `examples/gauntlet/sample-proofcard.json` * `examples/gauntlet/sample-claim-authority-output.json` -* `examples/gauntlet/synthetic-events.json` +* `examples/gauntlet/controlled-test-events.json` * `examples/gauntlet/expected-detection-results.json` * `examples/gauntlet/sample-work-impact-metrics.json` * `examples/gauntlet/bad-release-note.md` @@ -96,10 +96,10 @@ The sample JSON files keep runtime observation, signal observation, external pro ## Work Impact Metrics -`HOX-GAUNTLET-001` now emits numeric Work Impact Metrics v0 for the controlled fixture: 12 synthetic events, 4 expected positives, 8 expected negatives, 4 true positives, 8 true negatives, 0 false positives, 0 false negatives, 1.0 precision, 1.0 recall, 1.0 F1, 0.0 false-positive rate, 100.0% telemetry coverage, 7 claims scanned, 1 claim allowed, 6 claims blocked, and 100.0% ProofCard completeness. +`HOX-GAUNTLET-001` now emits numeric Work Impact Metrics v0 for the controlled fixture: 12 controlled-test events, 4 expected positives, 8 expected negatives, 4 true positives, 8 true negatives, 0 false positives, 0 false negatives, 1.0 precision, 1.0 recall, 1.0 F1, 0.0 false-positive rate, 100.0% telemetry coverage, 7 claims scanned, 1 claim allowed, 6 claims blocked, and 100.0% ProofCard completeness. Run: ```powershell -python -B -m hoxline.cli gauntlet metrics --events examples/gauntlet/synthetic-events.json --artifact examples/gauntlet/sample-artifact.json --proofcard examples/gauntlet/sample-proofcard.json --claim-output examples/gauntlet/sample-claim-authority-output.json --format json +python -B -m hoxline.cli gauntlet metrics --events examples/gauntlet/controlled-test-events.json --artifact examples/gauntlet/sample-artifact.json --proofcard examples/gauntlet/sample-proofcard.json --claim-output examples/gauntlet/sample-claim-authority-output.json --format json ``` diff --git a/docs/review-engine/HOXLINE_REVIEW_ENGINE_V1.md b/docs/review-engine/HOXLINE_REVIEW_ENGINE_V1.md index 68db2a7..26b2ca4 100644 --- a/docs/review-engine/HOXLINE_REVIEW_ENGINE_V1.md +++ b/docs/review-engine/HOXLINE_REVIEW_ENGINE_V1.md @@ -50,7 +50,7 @@ Required fields include `manifest_version`, `artifact_id`, `artifact_name`, `art 2. `evidence_graph` 3. `telemetry_contract_check` 4. `controlled_validation` -5. `synthetic_signal` +5. `controlled_test_signal` 6. `enrichment` 7. `triage` 8. `proofcard` @@ -72,7 +72,7 @@ No proof-boundary violation is warning-only. Violations produce `final_status=BL ## Generated Outputs -PASS runs write `artifact-manifest.json`, `intake.json`, `evidence-graph.json`, `telemetry-contract-check.json`, `validation-result.json`, `synthetic-signal.json`, `enrichment.json`, `triage-summary.md`, `proofcard.json`, `proofcard.md`, `claim-authority.json`, `reviewer-pack.md`, `machine-state.json`, and `run-summary.json`. +PASS runs write `artifact-manifest.json`, `intake.json`, `evidence-graph.json`, `telemetry-contract-check.json`, `validation-result.json`, `controlled-test-signal.json`, `enrichment.json`, `triage-summary.md`, `proofcard.json`, `proofcard.md`, `claim-authority.json`, `reviewer-pack.md`, `machine-state.json`, and `run-summary.json`. BLOCKED runs write sanitized `artifact-manifest.json`, `machine-state.json`, `blocked-review.md`, and `run-summary.json` when safe. @@ -82,7 +82,7 @@ BLOCKED runs write sanitized `artifact-manifest.json`, `machine-state.json`, `bl ## Hostile Fixture Behavior -Synthetic hostile manifests under `examples/review/hostile/` intentionally request unsafe claims, omit telemetry, point to missing fixtures, or include private/raw-like fields. They are expected to block. They are not evidence and are not runtime material. +Controlled-test hostile manifests under `examples/review/hostile/` intentionally request unsafe claims, omit telemetry, point to missing fixtures, or include private/raw-like fields. They are expected to block. They are not evidence and are not runtime material. ## Clean-Room Expectation @@ -110,19 +110,19 @@ A batch exits zero only when actual artifact outcomes match `expected_pass_artif ## Hostile Batch Behavior -Synthetic hostile indexes under `examples/review/hostile-batch/` cover duplicate artifact IDs, missing manifests, expectation mismatches, unsafe batch status, private-marker attempts, and production wording. They are expected to block fail-closed. +Controlled-test hostile indexes under `examples/review/hostile-batch/` cover duplicate artifact IDs, missing manifests, expectation mismatches, unsafe batch status, private-marker attempts, and production wording. They are expected to block fail-closed. ## Adding The Next Artifact Safely -Add a manifest only when source-controlled metadata exists or when the manifest is explicitly synthetic and fixture-only. Add positive and negative synthetic fixtures under `examples/review/fixtures/`, list every blocked claim class, keep all governance flags bounded, add the artifact to the index, and add a hostile case for the most likely unsafe claim. If the artifact cannot satisfy telemetry or fixture gates, list it as expected BLOCKED instead of pretending it is review-passable. +Add a manifest only when source-controlled metadata exists or when the manifest is explicitly controlled-test and fixture-only. Add positive and negative controlled-test fixtures under `examples/review/fixtures/`, list every blocked claim class, keep all governance flags bounded, add the artifact to the index, and add a hostile case for the most likely unsafe claim. If the artifact cannot satisfy telemetry or fixture gates, list it as expected BLOCKED instead of pretending it is review-passable. ## Future Detection Plug-In Path -To add a future detection, create a synthetic fixture manifest with telemetry assumptions, allowed example fixture paths, expected event/rule metadata, requested bounded claim wording, blocked claim classes, and explicit proof/runtime/signal boundaries. The engine should block until every required field and gate is satisfied. +To add a future detection, create a controlled-test fixture manifest with telemetry assumptions, allowed example fixture paths, expected event/rule metadata, requested bounded claim wording, blocked claim classes, and explicit proof/runtime/signal boundaries. The engine should block until every required field and gate is satisfied. ## What It Proves -It proves Hoxline can deterministically review a public sanitized synthetic artifact manifest through machine-checkable stages, generate reviewer artifacts, emit replayable machine state, and block unsupported claims. +It proves Hoxline can deterministically review a public sanitized controlled-test artifact manifest through machine-checkable stages, generate reviewer artifacts, emit replayable machine state, and block unsupported claims. ## What It Does Not Prove diff --git a/docs/reviewer/HOXLINE_REVIEWER_START_HERE.md b/docs/reviewer/HOXLINE_REVIEWER_START_HERE.md index e4376b7..0872c65 100644 --- a/docs/reviewer/HOXLINE_REVIEWER_START_HERE.md +++ b/docs/reviewer/HOXLINE_REVIEWER_START_HERE.md @@ -123,7 +123,7 @@ Current private scheduled collector scope: - HO-DET-011 - HO-DET-012 -HO-DET-010 has private runtime-candidate context outside this public fixture demo. Do not confuse the one-command synthetic fixture output with private runtime candidate evidence. HO-DET-010 remains `NOT_PUBLIC_SAFE`, `human_review_required=true`, and `ai_disposition_authority=false` pending governed review. +HO-DET-010 has private runtime-candidate context outside this public fixture demo. Do not confuse the one-command controlled-test fixture output with private runtime candidate evidence. HO-DET-010 remains `NOT_PUBLIC_SAFE`, `human_review_required=true`, and `ai_disposition_authority=false` pending governed review. Do not copy private packet contents, raw Wazuh alerts, endpoint logs, command lines, generated credentials, private payloads, execution identifiers, or private telemetry into public documentation. ## Website Route diff --git a/examples/case-growth/current-case-growth-index.json b/examples/case-growth/current-case-growth-index.json index a21c93e..c17be19 100644 --- a/examples/case-growth/current-case-growth-index.json +++ b/examples/case-growth/current-case-growth-index.json @@ -1,71 +1,310 @@ { - "schema_version": "case-growth-index-v0", - "generated_at": "2026-06-27T10:05:45Z", - "repo_root": "C:\\Raylee\\Repo\\HawkinsOperations", + "schema_version": "case-growth-index-v1", + "generated_at": "2026-07-24T15:39:07Z", + "repo_root": "HawkinsOperations", "proof_ceiling": "CASE_GROWTH_INDEX_CONTROLLED_REPO_AGGREGATION_ONLY", + "historical_snapshot": false, + "current_authority": true, + "snapshot_state": { + "freshness": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "identity_model": "repo_path_git_blob_and_semantic_fingerprint_with_separate_head_observation", + "generated_consumers_are_authority": false + }, + "source_revisions": [ + { + "repository": ".github", + "authority_role": "org command-center routing", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "6e6763a81d6af09c2e4588462b56117ce82c2f88", + "source_observed_head_sha": "6e6763a81d6af09c2e4588462b56117ce82c2f88", + "current_observed_head_sha": "5c6127f5acc1031bae2528df3ce1f197da882100", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "governance/COMMAND_CENTER_INVARIANTS.json", + "authoritative_path": "governance/COMMAND_CENTER_INVARIANTS.json", + "authoritative_git_blob_sha": "623e3f9e813b0599618a7df41dee1c9a40fb7a18", + "source_git_blob_sha": "623e3f9e813b0599618a7df41dee1c9a40fb7a18", + "source_file_sha256": "4f44687328dfaeea52270d230b513fb3195e3f3f0cecd2a32962455d875e56de", + "authoritative_content_fingerprint": "45cfa989c3f742b546f7f8a497632b43c928029bcaa09e80e04c7c894dba660c", + "source_semantic_fingerprint_sha256": "45cfa989c3f742b546f7f8a497632b43c928029bcaa09e80e04c7c894dba660c", + "canonical_origin": "github.com/hawkinsoperations/.github", + "observed_origin": "github.com/hawkinsoperations/.github", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-detections", + "authority_role": "detection source truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "f8bc0a0925113ca815bf5692081b5216162cc918", + "source_observed_head_sha": "f8bc0a0925113ca815bf5692081b5216162cc918", + "current_observed_head_sha": "9e01f43fb350de3370f8c01a323dcdcdf2e33147", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "detections/DETECTION_PROMOTION_MATRIX.yml", + "authoritative_path": "detections/DETECTION_PROMOTION_MATRIX.yml", + "authoritative_git_blob_sha": "3123d4f2a9dabdaa31d7e1d20698a17e7854491c", + "source_git_blob_sha": "3123d4f2a9dabdaa31d7e1d20698a17e7854491c", + "source_file_sha256": "c25219cfa2d097ab7aa8ed22164d6ebcf807ecd6aea94c858de09b5c91ebbcfe", + "authoritative_content_fingerprint": "d5214ccc882ac68eb56b3018b3c37799b7c5851e37c7b76e34cda570f0e061f1", + "source_semantic_fingerprint_sha256": "d5214ccc882ac68eb56b3018b3c37799b7c5851e37c7b76e34cda570f0e061f1", + "canonical_origin": "github.com/hawkinsoperations/hawkinsoperations-detections", + "observed_origin": "github.com/hawkinsoperations/hawkinsoperations-detections", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-validation", + "authority_role": "controlled validation truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "ebf52f7c6c9b78de767272cc56fccdc584f5c4e0", + "source_observed_head_sha": "ebf52f7c6c9b78de767272cc56fccdc584f5c4e0", + "current_observed_head_sha": "677b704150b0f5f333c27913dd481b4be6a78ab7", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "validation/VALIDATION_REGISTRY.yml", + "authoritative_path": "validation/VALIDATION_REGISTRY.yml", + "authoritative_git_blob_sha": "6fac3ac3d048c3ef687faaf5ebef1b04e846aad1", + "source_git_blob_sha": "6fac3ac3d048c3ef687faaf5ebef1b04e846aad1", + "source_file_sha256": "147291073c429c01bb7704c89025cf133aebebd7f735be14b95bccbe93e54878", + "authoritative_content_fingerprint": "de88ff4a621256cae51ec597a2cbb73f172d16c9d6e5109bcc42ff9a3b461cc3", + "source_semantic_fingerprint_sha256": "de88ff4a621256cae51ec597a2cbb73f172d16c9d6e5109bcc42ff9a3b461cc3", + "canonical_origin": "github.com/hawkinsoperations/hawkinsoperations-validation", + "observed_origin": "github.com/hawkinsoperations/hawkinsoperations-validation", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-platform", + "authority_role": "platform contract truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "a667c4de8b478fe165c3ec612e642bbd5d879492", + "source_observed_head_sha": "a667c4de8b478fe165c3ec612e642bbd5d879492", + "current_observed_head_sha": "4716d7e65525425be4f70127cdc7f7d3de3a7b9e", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "contracts/public-status-source-contract-v1.json", + "authoritative_path": "contracts/public-status-source-contract-v1.json", + "authoritative_git_blob_sha": "5bc7b79b77150413893f3d8147ae211b98d50e5b", + "source_git_blob_sha": "5bc7b79b77150413893f3d8147ae211b98d50e5b", + "source_file_sha256": "3a1cdc74a86230fe6567ddaed396ed45662e9b4efef7e398ae522a3e018317e4", + "authoritative_content_fingerprint": "f0f16d909e06b4cf67f347c675b999de3f7f10ff7032fbd350dc5a1f54266a01", + "source_semantic_fingerprint_sha256": "f0f16d909e06b4cf67f347c675b999de3f7f10ff7032fbd350dc5a1f54266a01", + "canonical_origin": "github.com/hawkinsoperations/hawkinsoperations-platform", + "observed_origin": "github.com/hawkinsoperations/hawkinsoperations-platform", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-proof", + "authority_role": "proof and claim-boundary truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "042a918ad4a8473cd5abcfd575072fc094639682", + "source_observed_head_sha": "042a918ad4a8473cd5abcfd575072fc094639682", + "current_observed_head_sha": "77b7874dd753369792330508fa3438cf397cd050", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", + "authoritative_path": "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", + "authoritative_git_blob_sha": "623b93e6e5ac141684978ff4dcdc6ed1dec55678", + "source_git_blob_sha": "623b93e6e5ac141684978ff4dcdc6ed1dec55678", + "source_file_sha256": "a8c24bd78bd20f318d4dfb5a930ea44bc108305bcc18b23db6ff2fc11e81a52d", + "authoritative_content_fingerprint": "68e5de4749bfe34a6677331f6116fab82987c88e999ae14eaf706e9b33536170", + "source_semantic_fingerprint_sha256": "68e5de4749bfe34a6677331f6116fab82987c88e999ae14eaf706e9b33536170", + "canonical_origin": "github.com/hawkinsoperations/hawkinsoperations-proof", + "observed_origin": "github.com/hawkinsoperations/hawkinsoperations-proof", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-website", + "authority_role": "rendering-only public status contract", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "5856f8e69527b5e61c3953b88a2ad4c088268655", + "source_observed_head_sha": "5856f8e69527b5e61c3953b88a2ad4c088268655", + "current_observed_head_sha": "0e7cb554ee3fd5519142006246201e7b15f0c9b5", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "schemas/public-status-v0.schema.json", + "authoritative_path": "schemas/public-status-v0.schema.json", + "authoritative_git_blob_sha": "1f10e8c0948635eda720905c1f1e7476ef63bf3c", + "source_git_blob_sha": "1f10e8c0948635eda720905c1f1e7476ef63bf3c", + "source_file_sha256": "65e40be7013df6cd2fe4cec7e86c7f92da1ccdd068c54ae2dbf935a0d1f4a0c5", + "authoritative_content_fingerprint": "5d04c8fbce269352e341798f28afdc29720fd2ea97b80d63884cfdb32e893a11", + "source_semantic_fingerprint_sha256": "5d04c8fbce269352e341798f28afdc29720fd2ea97b80d63884cfdb32e893a11", + "canonical_origin": "github.com/hawkinsoperations/hawkinsoperations-website", + "observed_origin": "github.com/hawkinsoperations/hawkinsoperations-website", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hoxline", + "authority_role": "case-growth and fixture-review product truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "1cb97efc45ffe753389105645c25ed7fe57cf9e5", + "source_observed_head_sha": "1cb97efc45ffe753389105645c25ed7fe57cf9e5", + "current_observed_head_sha": "52867ee7e332dba3cab4d2c3e308d636ed5bb610", + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "content_addressed_authority", + "source_path": "src/hoxline/case_growth/collector.py", + "authoritative_path": "src/hoxline/case_growth/collector.py", + "authoritative_git_blob_sha": "90c809e9ccd3764d14903f647980c81341bb3c42", + "source_git_blob_sha": "90c809e9ccd3764d14903f647980c81341bb3c42", + "source_file_sha256": "98277ac2a8bb2b85b7a7cd93863cd9dc3f8aaa9d89cf31038afa6310bc98992c", + "authoritative_content_fingerprint": "98277ac2a8bb2b85b7a7cd93863cd9dc3f8aaa9d89cf31038afa6310bc98992c", + "source_semantic_fingerprint_sha256": "98277ac2a8bb2b85b7a7cd93863cd9dc3f8aaa9d89cf31038afa6310bc98992c", + "canonical_origin": "github.com/hawkinsoperations/hoxline", + "observed_origin": "github.com/hawkinsoperations/hoxline", + "repository_dirty_observed": false, + "authority_source_dirty": false, + "source_freshness_state": "CURRENT", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + } + ], + "source_manifest_digest": "f295e5268283eeed408bd42c80c38a70b438a166d7882a75d3fd2b9bd79f957e", + "contradictions": [], + "drift": [], + "next_legal_action": "none; current source-controlled inputs converge", "repos_scanned": [ { "repo": ".github", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\.github", + "path": ".github", "exists": true, - "branch": "main", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": false, "authority_boundary": "org metadata and reviewer routing only; not proof authority", - "files_scanned": 34 + "files_scanned": 37 }, { "repo": "hawkinsoperations-detections", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hawkinsoperations-detections", + "path": "hawkinsoperations-detections", "exists": true, - "branch": "main", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": false, "authority_boundary": "source package and source status authority only", - "files_scanned": 92 + "files_scanned": 93 }, { "repo": "hawkinsoperations-validation", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hawkinsoperations-validation", + "path": "hawkinsoperations-validation", "exists": true, - "branch": "main", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": false, "authority_boundary": "controlled validation authority only", - "files_scanned": 198 + "files_scanned": 202 }, { "repo": "hawkinsoperations-platform", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hawkinsoperations-platform", + "path": "hawkinsoperations-platform", "exists": true, - "branch": "main", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": false, "authority_boundary": "platform runtime-candidate, collector, receipt, and ledger contract authority only", - "files_scanned": 118 + "files_scanned": 121 }, { "repo": "hawkinsoperations-proof", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hawkinsoperations-proof", + "path": "hawkinsoperations-proof", "exists": true, - "branch": "main", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": false, "authority_boundary": "proof ceiling, proof record, ProofCard, public-safe, blocked-claim, and next-gate authority", - "files_scanned": 103 + "files_scanned": 128 }, { "repo": "hawkinsoperations-website", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hawkinsoperations-website", + "path": "hawkinsoperations-website", "exists": true, - "branch": "feature/homepage-hoxline-interaction-v2", - "dirty": true, + "branch": "feature/hoxline-case-growth-convergence-v1", + "dirty": false, "authority_boundary": "route/rendering surface only; not proof authority", - "files_scanned": 261 + "files_scanned": 274 }, { "repo": "hoxline", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline", + "path": "hoxline", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", - "dirty": true, + "branch": "feature/hoxline-case-growth-convergence-v1", + "dirty": false, "authority_boundary": "product metrics and Hoxline Gauntlet artifact authority only", - "files_scanned": 162 + "files_scanned": 181 } ], "repo_slot_accuracy": { @@ -76,41 +315,41 @@ "hawkinsoperations_github_sibling_exists": false, "wording": "seven expected repo slots evaluated; seven present local repos scanned" }, - "source_files_scanned_count": 968, - "case_ids_discovered_count": 27, + "source_files_scanned_count": 3, + "case_ids_discovered_count": 26, "summary": { - "cases_total": 27, + "cases_total": 26, "source_packages_count": 14, "controlled_validations_count": 12, "runtime_candidate_lanes_count": 5, "private_runtime_evidence_captured_count": 1, "scheduled_collector_lanes_count": 4, - "proof_records_count": 4, - "proofcards_count": 4, + "proof_records_count": 11, + "proofcards_count": 12, "claim_authority_cases_count": 26, "metrics_available_count": 1, "public_safe_cases_count": 0, "closed_cases_count": 0, - "blocked_claims_count": 243, + "blocked_claims_count": 295, "cases_with_next_gate_count": 26, - "cases_missing_proof_record_count": 23, - "cases_missing_proofcard_count": 23, - "cases_not_public_safe_count": 27, - "unknown_state_count": 1 + "cases_missing_proof_record_count": 15, + "cases_missing_proofcard_count": 14, + "cases_not_public_safe_count": 26, + "unknown_state_count": 0 }, "case_growth_health": { "validation_coverage_percent": 85.71, - "proof_record_coverage_percent": 14.81, - "proofcard_coverage_percent": 14.81, - "scheduled_collector_coverage_percent": 14.81, - "runtime_candidate_coverage_percent": 18.52, - "metrics_coverage_percent": 3.7, + "proof_record_coverage_percent": 42.31, + "proofcard_coverage_percent": 46.15, + "scheduled_collector_coverage_percent": 15.38, + "runtime_candidate_coverage_percent": 19.23, + "metrics_coverage_percent": 3.85, "public_safe_percent": 0.0, "closed_case_percent": 0.0, - "blocked_claim_density": 9.0, - "next_gate_coverage_percent": 96.3, - "missing_proof_record_percent": 85.19, - "missing_proofcard_percent": 85.19, + "blocked_claim_density": 11.35, + "next_gate_coverage_percent": 100.0, + "missing_proof_record_percent": 57.69, + "missing_proofcard_percent": 53.85, "not_public_safe_percent": 100.0, "overall_health_status": "PUBLIC_SAFE_BLOCKED", "strongest_lane": "controlled_validation", @@ -179,12 +418,12 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "separate proof scope before any live-cloud or public route approval", "evidence_confidence": "HIGH", "notes": [ "detections: Fixture validation is external validation truth; this matrix records source package enforcement only.", - "validation: Controlled CloudTrail-style fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout. This does not prove live AWS, IdP, SIEM, runtime, signal, or public-safe status.", + "validation: Controlled CloudTrail-style fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation. This does not prove live AWS, IdP, SIEM, runtime, signal, or public-safe status.", "proof: Existing proof record supports controlled fixture validation only. It does not prove live AWS runtime or signal status.", "website route/rendering mention observed at hawkinsoperations-website/components/FeaturedWork.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/artifactFamilies.ts; not treated as proof", @@ -260,12 +499,12 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "proof-record-specific human review before any public-safe, runtime, or signal promotion", "evidence_confidence": "HIGH", "notes": [ "detections: Matrix records source package enforcement only; controlled-test validation belongs to validation and proof ceilings belong to proof.", - "validation: Controlled process-creation fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout.", + "validation: Controlled process-creation fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation.", "proof: Existing proof record supports CONTROLLED_TEST_VALIDATED. Private runtime wording is boundary context only and does not create public runtime proof.", "website route/rendering mention observed at hawkinsoperations-website/PENDING_WEBSITE-LANDING-001.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/README.md; not treated as proof", @@ -290,7 +529,9 @@ "website route/rendering mention observed at hawkinsoperations-website/components/ReviewerRunPath.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/components/TruthSurfaceSeparation.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/components/claim-firewall/ClaimFirewallSimulator.tsx; not treated as proof", + "website route/rendering mention observed at hawkinsoperations-website/components/command-center/HomeAutomationCockpitV2.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/components/command-center/VerifyTerminalDrawer.tsx; not treated as proof", + "website route/rendering mention observed at hawkinsoperations-website/components/visual-intelligence/OrbitInteractionWorkbench.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/components/visual-intelligence/index.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/docs/design/HOXLINE_WEBSITE_VISUAL_SYSTEM_V0.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/docs/design/WEBSITE_PR68_ORG_SYSTEM_REVIEW.md; not treated as proof", @@ -358,7 +599,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -401,7 +642,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -444,7 +685,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -487,7 +728,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -530,7 +771,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -573,7 +814,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -616,7 +857,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -650,6 +891,7 @@ ], "runtime_candidate_status": "PRIVATE_RUNTIME_CANDIDATE", "runtime_evidence_refs": [ + "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", "hawkinsoperations-platform/.github/workflows/hoxline-schedule-gated-collection.yml" ], "scheduled_collector_status": "SCHEDULED_COLLECTOR_LANE_PRESENT_GATED", @@ -657,13 +899,15 @@ "hawkinsoperations-platform/.github/workflows/hoxline-schedule-gated-collection.yml" ], "signal_status": "NOT_PROVEN", - "signal_evidence_refs": [], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "signal_evidence_refs": [ + "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" + ], + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/HO-DET-009.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/HO-DET-009.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 10, + "blocked_claim_count": 19, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -674,18 +918,28 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "live SIEM proof", + "live Cribl proof", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T16:42:32-05:00", - "next_gate": "platform fixture support and separately approved runtime receipt only after cleanup gates pass", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "separate runtime receipt and proof review before any runtime, signal, public-safe, production, or approval wording", + "evidence_confidence": "HIGH", "notes": [ "detections: Matrix records source package enforcement only; controlled-test validation belongs to validation and no runtime, signal, or public-safe claim is made here.", - "validation: Controlled Windows local user creation fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout. This does not prove runtime-active, signal-observed, production-ready, public-safe status, or account lifecycle completeness.", + "validation: Controlled Windows local user creation fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation. This does not prove runtime-active, signal-observed, production-ready, public-safe status, or account lifecycle completeness.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform scheduled-collector context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/app/hoxline/page.tsx; not treated as proof" ] }, @@ -716,6 +970,7 @@ ], "runtime_candidate_status": "PRIVATE_RUNTIME_CANDIDATE", "runtime_evidence_refs": [ + "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", "hawkinsoperations-platform/.github/workflows/hoxline-schedule-gated-collection.yml" ], "scheduled_collector_status": "SCHEDULED_COLLECTOR_LANE_PRESENT_GATED", @@ -723,13 +978,15 @@ "hawkinsoperations-platform/.github/workflows/hoxline-schedule-gated-collection.yml" ], "signal_status": "NOT_PROVEN", - "signal_evidence_refs": [], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "signal_evidence_refs": [ + "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" + ], + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/HO-DET-010.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/HO-DET-010.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 12, + "blocked_claim_count": 20, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -742,18 +999,27 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "live SIEM proof", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T16:42:32-05:00", - "next_gate": "reviewer validates source and controlled-test validation before any private runtime gate", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "reviewer validates source and controlled-test validation before any separately approved private runtime gate", + "evidence_confidence": "HIGH", "notes": [ "detections: Source package exists for Windows local Administrators group membership changes. Runtime, signal, public-safe, production, and disposition claims remain blocked.", - "validation: Controlled Windows local Administrators group membership fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout. This does not prove runtime-active, signal-observed, production-ready, or public-safe status.", + "validation: Controlled Windows local Administrators group membership fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation. This does not prove runtime-active, signal-observed, production-ready, or public-safe status.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform private-runtime candidate context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/app/hoxline/page.tsx; not treated as proof" ] }, @@ -800,8 +1066,8 @@ ], "proof_record_status": "PROOF_RECORD_EXISTS", "proof_record_path": "proof/records/HO-DET-011.md", - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/HO-DET-011.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", "blocked_claim_count": 13, "blocked_claims": [ @@ -823,13 +1089,13 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "proof-card or public reviewer route only after separate human-approved proof scope", "evidence_confidence": "HIGH", "notes": [ "detections: Source package remains NOT_PUBLIC_SAFE; private runtime material is not promoted by this source matrix.", - "validation: Controlled Windows service-creation fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout. This does not prove runtime-active, signal-observed, production-ready, or public-safe status.", - "proof: Existing proof record supports private runtime evidence capture only. It does not promote public runtime, signal, or public-safe status.", + "validation: Controlled Windows service-creation fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation. This does not prove runtime-active, signal-observed, production-ready, or public-safe status.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Private runtime evidence capture remains non-public and this card does not create public runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/app/detections/page.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/app/hoxline/page.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/app/proof/page.tsx; not treated as proof", @@ -913,12 +1179,12 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "blocked until separate runtime or signal evidence review supports any runtime, routed-telemetry, public-safe, production, autonomous SOC, or disposition-authority promotion", "evidence_confidence": "HIGH", "notes": [ "detections: Controlled-test validation is external validation truth. The proof repo records a CONTROLLED_TEST_VALIDATED proof record for HO-DET-012; this source matrix does not promote runtime, signal, or public-safe claims.", - "validation: Controlled service-account misuse fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout.", + "validation: Controlled service-account misuse fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation.", "proof: Existing proof record supports CONTROLLED_TEST_VALIDATED for controlled scheduled-task validation only. It does not promote runtime, signal, public-safe runtime, production, autonomous SOC, AI-approved, or analyst-approved status.", "website route/rendering mention observed at hawkinsoperations-website/app/detections/page.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/app/hoxline/page.tsx; not treated as proof", @@ -974,12 +1240,12 @@ "signal_evidence_refs": [ "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" ], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/HO-DET-013.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/HO-DET-013.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 12, + "blocked_claim_count": 19, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -992,19 +1258,26 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", - "next_gate": "separate proof record or runtime/signal review before any public proof promotion", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "reviewer validates source and controlled-test validation before any separately approved private runtime gate", + "evidence_confidence": "HIGH", "notes": [ - "detections: HO-DET-013 landed as source truth only; this matrix does not expand it.", - "validation: Controlled telemetry/security-control tamper fixture validation only. Sibling source contract is checked when present and skipped only when the entire sibling source surface is absent from a single-repo CI checkout. This does not prove runtime-active, signal-observed, production-ready, public-safe status, or control coverage completeness.", - "proof: Source and controlled validation exist in their owning repos. This index does not create a proof record, runtime proof, signal proof, public-safe status, or promotion authority.", + "detections: Controlled-test validation is external validation truth; this source matrix does not promote runtime, signal, proof, or public-safe claims.", + "validation: Controlled telemetry/security-control tamper fixture validation only. The canonical detection source handoff is required and content-addressed; missing source blocks validation. This does not prove runtime-active, signal-observed, production-ready, public-safe status, or control coverage completeness.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform private-runtime candidate context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/docs/design/WEBSITE_PR68_ORG_SYSTEM_REVIEW.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/docs/planning/WEBSITE_CLAUDE_DESIGN_BRIEF.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/docs/planning/WEBSITE_MISSING_WORK_MAP.md; not treated as proof", @@ -1048,7 +1321,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -1091,7 +1364,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -1134,43 +1407,13 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ "detections: Planned index row only; no package path is claimed to exist." ] }, - { - "case_id": "HO-DET-999", - "detection_id": "HO-DET-999", - "case_kind": "detection", - "source_status": "NOT_FOUND", - "source_evidence_refs": [], - "validation_status": "NOT_FOUND", - "validation_evidence_refs": [], - "runtime_candidate_status": "NOT_INDEXED", - "runtime_evidence_refs": [], - "scheduled_collector_status": "NOT_INDEXED", - "scheduled_collector_evidence_refs": [], - "signal_status": "NOT_PROVEN", - "signal_evidence_refs": [], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, - "claim_authority_status": "NOT_INDEXED", - "blocked_claim_count": 0, - "blocked_claims": [], - "public_safe_status": "NOT_PUBLIC_SAFE", - "case_state": "UNKNOWN_WITH_REASON", - "metrics_available": false, - "metrics_refs": [], - "last_updated": "UNKNOWN_WITH_REASON: no git history available", - "next_gate": "UNKNOWN_WITH_REASON: no next gate indexed", - "evidence_confidence": "LOW", - "notes": [] - }, { "case_id": "HO-NDR-001", "detection_id": "HO-NDR-001", @@ -1221,7 +1464,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "captured cross-source corroboration evidence under separate proof scope", "evidence_confidence": "MEDIUM", "notes": [ @@ -1280,7 +1523,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T10:58:36-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "create source package under separate source-authoring approval", "evidence_confidence": "LOW", "notes": [ @@ -1343,7 +1586,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T11:11:58-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "proof and runtime remain separate; any traffic, delivery, signal, route-proof, public-safe, or production wording requires separate approval", "evidence_confidence": "MEDIUM", "notes": [ @@ -1397,7 +1640,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T11:11:58-05:00", + "last_updated": "2026-07-24T08:54:44-05:00", "next_gate": "preserve hero baseline while successor HO-DET-001 remains the current reviewed source package", "evidence_confidence": "MEDIUM", "notes": [ @@ -1478,12 +1721,12 @@ "signal_evidence_refs": [ "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" ], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/ID-DET-001.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/ID-DET-001.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 11, + "blocked_claim_count": 18, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -1495,19 +1738,26 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", - "next_gate": "proof record creation under separate proof scope", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "reviewer validates source and controlled-test validation before any separately approved identity runtime gate", + "evidence_confidence": "HIGH", "notes": [ "detections: Source package records source truth; validation and identity-provider evidence remain external gates.", "validation: Controlled identity-event fixture validation only. This does not prove live IdP, live SIEM, complete identity coverage, signal observation, or public-safe status.", - "proof: Validation status is external validation truth. Proof repo has no ID-DET-001 proof record in this phase.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform identity-lane context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/components/FeaturedWork.tsx; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/docs/design/WEBSITE_PR68_ORG_SYSTEM_REVIEW.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/docs/planning/WEBSITE_MISSING_WORK_MAP.md; not treated as proof", @@ -1555,12 +1805,12 @@ "signal_evidence_refs": [ "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" ], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/ID-DET-002.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/ID-DET-002.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 11, + "blocked_claim_count": 18, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -1572,19 +1822,26 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", - "next_gate": "proof record creation under separate proof scope", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "reviewer validates source and controlled-test validation before any separately approved identity runtime gate", + "evidence_confidence": "HIGH", "notes": [ - "detections: Source artifacts exist and validation truth is CONTROLLED_TEST_VALIDATED in hawkinsoperations-validation. Proof repo has no ID-DET-002 proof record in this phase.", + "detections: Source artifacts exist and validation truth is CONTROLLED_TEST_VALIDATED in hawkinsoperations-validation. The proof-owned current index links an ID-DET-002 record; that linkage does not raise detection authority.", "validation: Controlled identity-event fixture validation only. This does not prove live IdP, live SIEM, complete identity coverage, signal observation, or public-safe status.", - "proof: Validation status is external validation truth. Proof repo has no ID-DET-002 proof record in this phase.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform identity-lane context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/docs/planning/WEBSITE_MISSING_WORK_MAP.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/proofPackManifest.ts; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/proofRecords.ts; not treated as proof", @@ -1627,12 +1884,12 @@ "signal_evidence_refs": [ "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" ], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/ID-DET-003.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/ID-DET-003.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 11, + "blocked_claim_count": 18, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -1644,19 +1901,26 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", - "next_gate": "proof record creation under separate proof scope", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "reviewer validates source and controlled-test validation before any separately approved identity runtime gate", + "evidence_confidence": "HIGH", "notes": [ - "detections: Source artifacts exist and validation truth is CONTROLLED_TEST_VALIDATED in hawkinsoperations-validation. Proof repo has no ID-DET-003 proof record in this phase.", + "detections: Source artifacts exist and validation truth is CONTROLLED_TEST_VALIDATED in hawkinsoperations-validation. The proof-owned current index links an ID-DET-003 record; that linkage does not raise detection authority.", "validation: Controlled identity-administration fixture validation only. This does not prove live IdP, live SIEM, complete identity coverage, signal observation, or public-safe status.", - "proof: Validation status is external validation truth. Proof repo has no ID-DET-003 proof record in this phase.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform identity-lane context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/src/data/governanceSaves.ts; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/proofPackManifest.ts; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/proofRecords.ts; not treated as proof", @@ -1699,12 +1963,12 @@ "signal_evidence_refs": [ "hawkinsoperations-proof/proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml" ], - "proof_record_status": "NOT_PROVEN", - "proof_record_path": null, - "proofcard_status": "NOT_PROVEN", - "proofcard_path": null, + "proof_record_status": "PROOF_RECORD_EXISTS", + "proof_record_path": "proof/records/ID-DET-004.md", + "proofcard_status": "PROOFCARD_EXISTS", + "proofcard_path": "proof/cards/ID-DET-004.md", "claim_authority_status": "BLOCKED_CLAIMS_INDEXED", - "blocked_claim_count": 11, + "blocked_claim_count": 18, "blocked_claims": [ "runtime-active public proof", "signal-observed public proof", @@ -1716,19 +1980,26 @@ "fleet-wide", "autonomous SOC", "AI-approved disposition", - "analyst-approved disposition" + "analyst-approved disposition", + "customer deployment", + "SOCaaS deployment", + "final authorization", + "case closure", + "website rendering as proof", + "GitHub rendering as proof", + "green CI as approval" ], "public_safe_status": "NOT_PUBLIC_SAFE", "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-24T18:36:09-05:00", - "next_gate": "proof record creation under separate proof scope", - "evidence_confidence": "MEDIUM", + "last_updated": "2026-07-24T08:54:44-05:00", + "next_gate": "reviewer validates source and controlled-test validation before any separately approved identity runtime gate", + "evidence_confidence": "HIGH", "notes": [ - "detections: Source artifacts exist and validation truth is CONTROLLED_TEST_VALIDATED in hawkinsoperations-validation. Proof repo has no ID-DET-004 proof record in this phase.", + "detections: Source artifacts exist and validation truth is CONTROLLED_TEST_VALIDATED in hawkinsoperations-validation. The proof-owned current index links an ID-DET-004 record; that linkage does not raise detection authority.", "validation: Controlled identity-event fixture validation only. This does not prove live IdP, live SIEM, complete identity coverage, signal observation, or public-safe status.", - "proof: Validation status is external validation truth. Proof repo has no ID-DET-004 proof record in this phase.", + "proof: ProofCard backfill from an existing proof record and repo-visible evidence only. Platform identity-lane context remains non-promotional and this card does not create runtime, signal, public_safe, customer, production, approval, or closure claims.", "website route/rendering mention observed at hawkinsoperations-website/docs/planning/WEBSITE_MISSING_WORK_MAP.md; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/proofPackManifest.ts; not treated as proof", "website route/rendering mention observed at hawkinsoperations-website/src/data/proofRecords.ts; not treated as proof", @@ -1739,15 +2010,7 @@ ], "data_quality_notes": [ "platform lifetime ledger manifest reports closed_case_count=0", - "HO-DET-009 has controlled validation but no proof record", - "HO-DET-010 has controlled validation but no proof record", - "HO-DET-011 has proof record but no ProofCard", - "HO-DET-013 has controlled validation but no proof record", - "HOD-001 has controlled validation but no proof record", - "ID-DET-001 has controlled validation but no proof record", - "ID-DET-002 has controlled validation but no proof record", - "ID-DET-003 has controlled validation but no proof record", - "ID-DET-004 has controlled validation but no proof record" + "HOD-001 has controlled validation but no proof record" ], "boundary": { "runtime_public_proof_claimed": false, @@ -1760,5 +2023,6 @@ "final_authorization_claimed": false, "website_rendering_treated_as_proof": false, "green_ci_treated_as_approval": false - } + }, + "reproducibility_sha256": "a8b5ec50636e7b62f9a0c11e0316d62b8009b3d18e4a52faf9b72578e059f8a1" } diff --git a/examples/case-growth/current-case-growth-index.md b/examples/case-growth/current-case-growth-index.md index c264b5b..a778316 100644 --- a/examples/case-growth/current-case-growth-index.md +++ b/examples/case-growth/current-case-growth-index.md @@ -1,48 +1,70 @@ -# Hoxline Case Growth Index v0 +# Hoxline Case Growth Index v1 -Generated: `2026-06-27T10:05:59Z` +Generated: `2026-07-24T15:39:07Z` Proof ceiling: `CASE_GROWTH_INDEX_CONTROLLED_REPO_AGGREGATION_ONLY` Repo-slot accuracy: `seven expected repo slots evaluated; seven present local repos scanned` +Historical snapshot: `false` +Current authority: `true` +Source manifest digest: `f295e5268283eeed408bd42c80c38a70b438a166d7882a75d3fd2b9bd79f957e` +Reproducibility SHA-256: `a8b5ec50636e7b62f9a0c11e0316d62b8009b3d18e4a52faf9b72578e059f8a1` ## Summary | Metric | Count | | --- | ---: | -| `cases_total` | 27 | +| `cases_total` | 26 | | `source_packages_count` | 14 | | `controlled_validations_count` | 12 | | `runtime_candidate_lanes_count` | 5 | | `private_runtime_evidence_captured_count` | 1 | | `scheduled_collector_lanes_count` | 4 | -| `proof_records_count` | 4 | -| `proofcards_count` | 4 | +| `proof_records_count` | 11 | +| `proofcards_count` | 12 | | `claim_authority_cases_count` | 26 | | `metrics_available_count` | 1 | | `public_safe_cases_count` | 0 | | `closed_cases_count` | 0 | -| `blocked_claims_count` | 243 | +| `blocked_claims_count` | 295 | | `cases_with_next_gate_count` | 26 | -| `cases_missing_proof_record_count` | 23 | -| `cases_missing_proofcard_count` | 23 | -| `cases_not_public_safe_count` | 27 | -| `unknown_state_count` | 1 | +| `cases_missing_proof_record_count` | 15 | +| `cases_missing_proofcard_count` | 14 | +| `cases_not_public_safe_count` | 26 | +| `unknown_state_count` | 0 | + +## Source Revisions + +| Repository | Authority role | Authority path | Observed head | Git blob | Semantic fingerprint | Source freshness | +| --- | --- | --- | --- | --- | --- | --- | +| `.github` | `org command-center routing` | `governance/COMMAND_CENTER_INVARIANTS.json` | `6e6763a81d6af09c2e4588462b56117ce82c2f88` | `623e3f9e813b0599618a7df41dee1c9a40fb7a18` | `45cfa989c3f742b546f7f8a497632b43c928029bcaa09e80e04c7c894dba660c` | `CURRENT` | +| `hawkinsoperations-detections` | `detection source truth` | `detections/DETECTION_PROMOTION_MATRIX.yml` | `f8bc0a0925113ca815bf5692081b5216162cc918` | `3123d4f2a9dabdaa31d7e1d20698a17e7854491c` | `d5214ccc882ac68eb56b3018b3c37799b7c5851e37c7b76e34cda570f0e061f1` | `CURRENT` | +| `hawkinsoperations-validation` | `controlled validation truth` | `validation/VALIDATION_REGISTRY.yml` | `ebf52f7c6c9b78de767272cc56fccdc584f5c4e0` | `6fac3ac3d048c3ef687faaf5ebef1b04e846aad1` | `de88ff4a621256cae51ec597a2cbb73f172d16c9d6e5109bcc42ff9a3b461cc3` | `CURRENT` | +| `hawkinsoperations-platform` | `platform contract truth` | `contracts/public-status-source-contract-v1.json` | `a667c4de8b478fe165c3ec612e642bbd5d879492` | `5bc7b79b77150413893f3d8147ae211b98d50e5b` | `f0f16d909e06b4cf67f347c675b999de3f7f10ff7032fbd350dc5a1f54266a01` | `CURRENT` | +| `hawkinsoperations-proof` | `proof and claim-boundary truth` | `proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml` | `042a918ad4a8473cd5abcfd575072fc094639682` | `623b93e6e5ac141684978ff4dcdc6ed1dec55678` | `68e5de4749bfe34a6677331f6116fab82987c88e999ae14eaf706e9b33536170` | `CURRENT` | +| `hawkinsoperations-website` | `rendering-only public status contract` | `schemas/public-status-v0.schema.json` | `5856f8e69527b5e61c3953b88a2ad4c088268655` | `1f10e8c0948635eda720905c1f1e7476ef63bf3c` | `5d04c8fbce269352e341798f28afdc29720fd2ea97b80d63884cfdb32e893a11` | `CURRENT` | +| `hoxline` | `case-growth and fixture-review product truth` | `src/hoxline/case_growth/collector.py` | `1cb97efc45ffe753389105645c25ed7fe57cf9e5` | `90c809e9ccd3764d14903f647980c81341bb3c42` | `98277ac2a8bb2b85b7a7cd93863cd9dc3f8aaa9d89cf31038afa6310bc98992c` | `CURRENT` | + +## Convergence Findings + +- No missing, dangling, contradictory, or stale source-owned state detected. + +Next legal action: none; current source-controlled inputs converge ## Case Growth Health | Health metric | Value | | --- | ---: | | `validation_coverage_percent` | 85.71 | -| `proof_record_coverage_percent` | 14.81 | -| `proofcard_coverage_percent` | 14.81 | -| `scheduled_collector_coverage_percent` | 14.81 | -| `runtime_candidate_coverage_percent` | 18.52 | -| `metrics_coverage_percent` | 3.7 | +| `proof_record_coverage_percent` | 42.31 | +| `proofcard_coverage_percent` | 46.15 | +| `scheduled_collector_coverage_percent` | 15.38 | +| `runtime_candidate_coverage_percent` | 19.23 | +| `metrics_coverage_percent` | 3.85 | | `public_safe_percent` | 0.0 | | `closed_case_percent` | 0.0 | -| `blocked_claim_density` | 9.0 | -| `next_gate_coverage_percent` | 96.3 | -| `missing_proof_record_percent` | 85.19 | -| `missing_proofcard_percent` | 85.19 | +| `blocked_claim_density` | 11.35 | +| `next_gate_coverage_percent` | 100.0 | +| `missing_proof_record_percent` | 57.69 | +| `missing_proofcard_percent` | 53.85 | | `not_public_safe_percent` | 100.0 | | Assessment | Value | @@ -74,24 +96,23 @@ The health section is derived from numeric index counts only. It does not promot | `HO-DET-006` | `VALIDATION_PLANNED` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | | `HO-DET-007` | `VALIDATION_PLANNED` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | | `HO-DET-008` | `VALIDATION_PLANNED` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | -| `HO-DET-009` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_CANDIDATE` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `platform fixture support and separately approved runtime receipt only after cleanup gates pass` | -| `HO-DET-010` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_CANDIDATE` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any private runtime gate` | -| `HO-DET-011` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_EVIDENCE_CAPTURED` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `PROOF_RECORD_EXISTS` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof-card or public reviewer route only after separate human-approved proof scope` | +| `HO-DET-009` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_CANDIDATE` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `separate runtime receipt and proof review before any runtime, signal, public-safe, production, or approval wording` | +| `HO-DET-010` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_CANDIDATE` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any separately approved private runtime gate` | +| `HO-DET-011` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_EVIDENCE_CAPTURED` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof-card or public reviewer route only after separate human-approved proof scope` | | `HO-DET-012` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `PRIVATE_RUNTIME_CANDIDATE` | `SCHEDULED_COLLECTOR_LANE_PRESENT_GATED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `blocked until separate runtime or signal evidence review supports any runtime, routed-telemetry, public-safe, production, autonomous SOC, or disposition-authority promotion` | -| `HO-DET-013` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `separate proof record or runtime/signal review before any public proof promotion` | +| `HO-DET-013` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any separately approved private runtime gate` | | `HO-DET-014` | `VALIDATION_PLANNED` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | | `HO-DET-015` | `VALIDATION_PLANNED` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | | `HO-DET-016` | `VALIDATION_PLANNED` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | -| `HO-DET-999` | `NOT_FOUND` | `NOT_FOUND` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `UNKNOWN_WITH_REASON` | `UNKNOWN_WITH_REASON: no next gate indexed` | | `HO-NDR-001` | `EXTERNAL_BOUNDARY_CONTRACT` | `VALIDATION_CONTRACT_ENFORCED` | `NOT_PROVEN` | `NOT_INDEXED` | `NOT_PROVEN` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `captured cross-source corroboration evidence under separate proof scope` | | `HO-NDR-002` | `VALIDATION_PLANNED` | `NOT_FOUND` | `LISTED_ONLY` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `create source package under separate source-authoring approval` | | `HO-PIPE-001` | `SOURCE_EXISTS` | `VALIDATION_CONTRACT_ENFORCED` | `TELEMETRY_CONTRACT_ONLY` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof and runtime remain separate; any traffic, delivery, signal, route-proof, public-safe, or production wording requires separate approval` | | `HOD-001` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `preserve hero baseline while successor HO-DET-001 remains the current reviewed source package` | | `HOX-GAUNTLET-001` | `SOURCE_EXISTS` | `CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY` | `NOT_INDEXED` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | true | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `human review before runtime, signal, customer, production, public wording, or final human gate promotion` | -| `ID-DET-001` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof record creation under separate proof scope` | -| `ID-DET-002` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof record creation under separate proof scope` | -| `ID-DET-003` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof record creation under separate proof scope` | -| `ID-DET-004` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `NOT_PROVEN` | `NOT_PROVEN` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `proof record creation under separate proof scope` | +| `ID-DET-001` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any separately approved identity runtime gate` | +| `ID-DET-002` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any separately approved identity runtime gate` | +| `ID-DET-003` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any separately approved identity runtime gate` | +| `ID-DET-004` | `SOURCE_EXISTS` | `CONTROLLED_TEST_VALIDATED` | `NOT_PROVEN` | `NOT_INDEXED` | `PROOF_RECORD_EXISTS` | `PROOFCARD_EXISTS` | false | `NOT_PUBLIC_SAFE` | `BLOCKED_WAITING_NEXT_GATE` | `reviewer validates source and controlled-test validation before any separately approved identity runtime gate` | ## Boundary @@ -111,12 +132,4 @@ The health section is derived from numeric index counts only. It does not promot ## Data Quality Notes - platform lifetime ledger manifest reports closed_case_count=0 -- HO-DET-009 has controlled validation but no proof record -- HO-DET-010 has controlled validation but no proof record -- HO-DET-011 has proof record but no ProofCard -- HO-DET-013 has controlled validation but no proof record - HOD-001 has controlled validation but no proof record -- ID-DET-001 has controlled validation but no proof record -- ID-DET-002 has controlled validation but no proof record -- ID-DET-003 has controlled validation but no proof record -- ID-DET-004 has controlled validation but no proof record diff --git a/examples/case-growth/sample-case-growth-index.json b/examples/case-growth/sample-case-growth-index.json index 976e6a4..f74b1ee 100644 --- a/examples/case-growth/sample-case-growth-index.json +++ b/examples/case-growth/sample-case-growth-index.json @@ -1,68 +1,258 @@ { - "schema_version": "case-growth-index-v0", - "generated_at": "2026-06-27T10:02:21Z", - "repo_root": "tests\\fixtures\\case_growth\\org", + "schema_version": "case-growth-index-v1", + "generated_at": "2026-07-22T23:09:52Z", + "repo_root": "HawkinsOperations", "proof_ceiling": "CASE_GROWTH_INDEX_CONTROLLED_REPO_AGGREGATION_ONLY", + "historical_snapshot": false, + "current_authority": true, + "snapshot_state": { + "freshness": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "self_source_revision_semantics": "worktree-head-or-snapshot-commit-parent" + }, + "source_revisions": [ + { + "repository": ".github", + "authority_role": "org command-center routing", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "authoritative_source_at_commit", + "source_path": "scripts/verify-command-center-invariants.py", + "source_file_sha256": null, + "source_freshness_state": "MISSING_AUTHORITY_SOURCE", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": false, + "missing_source_state": true, + "dangling_reference_state": true, + "contradictions": [], + "drift": [], + "next_legal_action": "restore .github/scripts/verify-command-center-invariants.py from its owning repository" + }, + { + "repository": "hawkinsoperations-detections", + "authority_role": "detection source truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "authoritative_source_at_commit", + "source_path": "detections/DETECTION_PROMOTION_MATRIX.yml", + "source_file_sha256": "07d582de56b33c6e78d9ad8965cdfc635d87c1004ef1db75b9a7f1aa9fecbd61", + "source_freshness_state": "WORKTREE_MODIFIED", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-validation", + "authority_role": "controlled validation truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "authoritative_source_at_commit", + "source_path": "validation/VALIDATION_REGISTRY.yml", + "source_file_sha256": "2c12c3e8348ce058d3e4dd8ef7c437c3c761cf73b540233f49d4c7f1dea045be", + "source_freshness_state": "WORKTREE_MODIFIED", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-platform", + "authority_role": "platform contract truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "authoritative_source_at_commit", + "source_path": "contracts/public-status-source-contract-v1.json", + "source_file_sha256": null, + "source_freshness_state": "MISSING_AUTHORITY_SOURCE", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": false, + "missing_source_state": true, + "dangling_reference_state": true, + "contradictions": [], + "drift": [], + "next_legal_action": "restore hawkinsoperations-platform/contracts/public-status-source-contract-v1.json from its owning repository" + }, + { + "repository": "hawkinsoperations-proof", + "authority_role": "proof and claim-boundary truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "authoritative_source_at_commit", + "source_path": "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", + "source_file_sha256": "1ff1ec190570218bae05656abae8c619edc451b27700de3cf6da9ca8de364d08", + "source_freshness_state": "WORKTREE_MODIFIED", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": true, + "missing_source_state": false, + "dangling_reference_state": false, + "contradictions": [], + "drift": [], + "next_legal_action": "none; preserve source ownership" + }, + { + "repository": "hawkinsoperations-website", + "authority_role": "rendering-only public status", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": null, + "self_referential": false, + "revision_scope": "authoritative_source_at_commit", + "source_path": "public/data/public-status.json", + "source_file_sha256": null, + "source_freshness_state": "MISSING_AUTHORITY_SOURCE", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": false, + "missing_source_state": true, + "dangling_reference_state": true, + "contradictions": [], + "drift": [], + "next_legal_action": "restore hawkinsoperations-website/public/data/public-status.json from its owning repository" + }, + { + "repository": "hoxline", + "authority_role": "case-growth and fixture-review product truth", + "resolved_ref": "feature/hoxline-case-growth-convergence-v1", + "source_commit_sha": "bed34b8704f476075d65ec50095bb37121cb635b", + "source_parent_sha": "0be75c48ea4269fd6f7726620bfd157c42c43e95", + "self_referential": true, + "revision_scope": "authoritative_sources_excluding_snapshot", + "source_path": "src/hoxline/case_growth/collector.py", + "source_file_sha256": null, + "source_freshness_state": "MISSING_AUTHORITY_SOURCE", + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": false, + "current_authority": false, + "missing_source_state": true, + "dangling_reference_state": true, + "contradictions": [], + "drift": [], + "next_legal_action": "restore hoxline/src/hoxline/case_growth/collector.py from its owning repository" + } + ], + "contradictions": [ + { + "code": "MISSING_AUTHORITY_SOURCE", + "source_owner": ".github", + "source_path": "scripts/verify-command-center-invariants.py", + "expected": "existing authoritative source", + "actual": "missing", + "classification": "ACTIONABLE_DRIFT", + "next_legal_action": "restore .github/scripts/verify-command-center-invariants.py from its owning repository" + }, + { + "code": "MISSING_AUTHORITY_SOURCE", + "source_owner": "hawkinsoperations-platform", + "source_path": "contracts/public-status-source-contract-v1.json", + "expected": "existing authoritative source", + "actual": "missing", + "classification": "ACTIONABLE_DRIFT", + "next_legal_action": "restore hawkinsoperations-platform/contracts/public-status-source-contract-v1.json from its owning repository" + }, + { + "code": "MISSING_AUTHORITY_SOURCE", + "source_owner": "hawkinsoperations-website", + "source_path": "public/data/public-status.json", + "expected": "existing authoritative source", + "actual": "missing", + "classification": "ACTIONABLE_DRIFT", + "next_legal_action": "restore hawkinsoperations-website/public/data/public-status.json from its owning repository" + }, + { + "code": "MISSING_AUTHORITY_SOURCE", + "source_owner": "hoxline", + "source_path": "src/hoxline/case_growth/collector.py", + "expected": "existing authoritative source", + "actual": "missing", + "classification": "ACTIONABLE_DRIFT", + "next_legal_action": "restore hoxline/src/hoxline/case_growth/collector.py from its owning repository" + } + ], + "drift": [], + "next_legal_action": "restore .github/scripts/verify-command-center-invariants.py from its owning repository", "repos_scanned": [ { "repo": ".github", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\.github", + "path": ".github", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "org metadata and reviewer routing only; not proof authority", "files_scanned": 1 }, { "repo": "hawkinsoperations-detections", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\hawkinsoperations-detections", + "path": "hawkinsoperations-detections", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "source package and source status authority only", "files_scanned": 5 }, { "repo": "hawkinsoperations-validation", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\hawkinsoperations-validation", + "path": "hawkinsoperations-validation", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "controlled validation authority only", "files_scanned": 7 }, { "repo": "hawkinsoperations-platform", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\hawkinsoperations-platform", + "path": "hawkinsoperations-platform", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "platform runtime-candidate, collector, receipt, and ledger contract authority only", "files_scanned": 4 }, { "repo": "hawkinsoperations-proof", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\hawkinsoperations-proof", + "path": "hawkinsoperations-proof", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "proof ceiling, proof record, ProofCard, public-safe, blocked-claim, and next-gate authority", "files_scanned": 4 }, { "repo": "hawkinsoperations-website", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\hawkinsoperations-website", + "path": "hawkinsoperations-website", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "route/rendering surface only; not proof authority", "files_scanned": 1 }, { "repo": "hoxline", - "path": "C:\\Raylee\\Repo\\HawkinsOperations\\hoxline\\tests\\fixtures\\case_growth\\org\\hoxline", + "path": "hoxline", "exists": true, - "branch": "feature/hoxline-case-growth-index-v0", + "branch": "feature/hoxline-case-growth-convergence-v1", "dirty": true, "authority_boundary": "product metrics and Hoxline Gauntlet artifact authority only", "files_scanned": 2 @@ -175,7 +365,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-27T04:32:03-05:00", + "last_updated": "2026-06-27T05:31:19-05:00", "next_gate": "human review before public wording", "evidence_confidence": "HIGH", "notes": [ @@ -216,7 +406,7 @@ "case_state": "BLOCKED_WAITING_NEXT_GATE", "metrics_available": false, "metrics_refs": [], - "last_updated": "2026-06-27T04:32:03-05:00", + "last_updated": "2026-06-27T05:31:19-05:00", "next_gate": "create controlled validation package", "evidence_confidence": "MEDIUM", "notes": [ @@ -286,7 +476,7 @@ "metrics_refs": [ "hoxline/examples/gauntlet/sample-work-impact-metrics.json" ], - "last_updated": "2026-06-27T04:32:03-05:00", + "last_updated": "2026-06-27T05:31:19-05:00", "next_gate": "human review before runtime, signal, customer, production, public wording, or final human gate promotion", "evidence_confidence": "MEDIUM", "notes": [ @@ -309,5 +499,6 @@ "final_authorization_claimed": false, "website_rendering_treated_as_proof": false, "green_ci_treated_as_approval": false - } + }, + "reproducibility_sha256": "a70e4f765aa56a3fe8a5ac43eaac3032c7becf76004ab3b901f63150b5b329d1" } diff --git a/examples/demo/ho-det-010-safe-fixture.json b/examples/demo/ho-det-010-safe-fixture.json index a79fa4c..ccb022d 100644 --- a/examples/demo/ho-det-010-safe-fixture.json +++ b/examples/demo/ho-det-010-safe-fixture.json @@ -2,14 +2,14 @@ "schema_version": "hoxline-demo-fixture-v0", "fixture_id": "ho-det-010-safe-positive-fixture-v0", "artifact_id": "HO-DET-010", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "safe_fixture": true, "expected_detection": true, "endpoint_mutation": false, "runtime_required": false, "network_required": false, "host": "demo-host-010", - "description": "Synthetic local Administrators membership change fixture for the one-command reviewer demo.", + "description": "Controlled-test local Administrators membership change fixture for the one-command reviewer demo.", "events": [ { "event_id": 4732, diff --git a/examples/demo/ho-det-010-safe-negative-fixture.json b/examples/demo/ho-det-010-safe-negative-fixture.json index bed9fae..566961d 100644 --- a/examples/demo/ho-det-010-safe-negative-fixture.json +++ b/examples/demo/ho-det-010-safe-negative-fixture.json @@ -2,14 +2,14 @@ "schema_version": "hoxline-demo-fixture-v0", "fixture_id": "ho-det-010-safe-negative-fixture-v0", "artifact_id": "HO-DET-010", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "safe_fixture": true, "expected_detection": false, "endpoint_mutation": false, "runtime_required": false, "network_required": false, "host": "demo-host-010", - "description": "Synthetic non-admin group membership fixture that must not fire the demo detection.", + "description": "Controlled-test non-admin group membership fixture that must not fire the demo detection.", "events": [ { "event_id": 4732, diff --git a/examples/gauntlet/controlled-test-events.json b/examples/gauntlet/controlled-test-events.json new file mode 100644 index 0000000..e866385 --- /dev/null +++ b/examples/gauntlet/controlled-test-events.json @@ -0,0 +1,155 @@ +{ + "schema_version": "controlled-test-events-v0", + "artifact_id": "HOX-GAUNTLET-001", + "dataset_scope": "controlled controlled-test fixture only", + "contains_malware_code": false, + "contains_exploit_instructions": false, + "contains_customer_data": false, + "contains_private_runtime_evidence": false, + "events": [ + { + "event_id": "controlled-test-event-001", + "event_time": "2026-06-27T00:00:01Z", + "host": "controlled-test-host-01", + "user": "controlled-test-user-a", + "parent_process_name": "controlled_test_browser.exe", + "process_name": "powershell.exe", + "process_command_line": "powershell.exe -NoProfile -File C:\\Controlled-test\\Review\\cache-followup.ps1", + "file_path": "C:\\Users\\controlled-test-user-a\\AppData\\Local\\Browser\\Cache\\cache-item-001.tmp", + "expected_detection_match": true, + "rationale": "Browser-like parent, cache-like path, and script interpreter child match the controlled review rule." + }, + { + "event_id": "controlled-test-event-002", + "event_time": "2026-06-27T00:00:02Z", + "host": "controlled-test-host-01", + "user": "controlled-test-user-b", + "parent_process_name": "chrome.exe", + "process_name": "cmd.exe", + "process_command_line": "cmd.exe /c C:\\Controlled-test\\Review\\cache-followup.cmd", + "file_path": "C:\\Users\\controlled-test-user-b\\AppData\\Local\\Chrome\\User Data\\Default\\Cache\\cache-item-002.bin", + "expected_detection_match": true, + "rationale": "Chrome parent, browser cache path, and command interpreter child match the controlled review rule." + }, + { + "event_id": "controlled-test-event-003", + "event_time": "2026-06-27T00:00:03Z", + "host": "controlled-test-host-02", + "user": "controlled-test-user-c", + "parent_process_name": "msedge.exe", + "process_name": "wscript.exe", + "process_command_line": "wscript.exe C:\\Controlled-test\\Review\\cache-followup.vbs", + "file_path": "C:\\Users\\controlled-test-user-c\\AppData\\Local\\Edge\\User Data\\Default\\Cache\\cache-item-003.dat", + "expected_detection_match": true, + "rationale": "Edge parent, cache path, and Windows script host child match the controlled review rule." + }, + { + "event_id": "controlled-test-event-004", + "event_time": "2026-06-27T00:00:04Z", + "host": "controlled-test-host-02", + "user": "controlled-test-user-d", + "parent_process_name": "firefox.exe", + "process_name": "pwsh.exe", + "process_command_line": "pwsh.exe -File C:\\Controlled-test\\Review\\cache-followup.ps1", + "file_path": "C:\\Users\\controlled-test-user-d\\AppData\\Local\\Firefox\\Profiles\\controlled-test\\cache2\\cache-item-004.bin", + "expected_detection_match": true, + "rationale": "Firefox parent, cache path, and PowerShell child match the controlled review rule." + }, + { + "event_id": "controlled-test-event-005", + "event_time": "2026-06-27T00:00:05Z", + "host": "controlled-test-host-03", + "user": "controlled-test-user-e", + "parent_process_name": "controlled_test_browser.exe", + "process_name": "notepad.exe", + "process_command_line": "notepad.exe C:\\Controlled-test\\Review\\cache-note.txt", + "file_path": "C:\\Users\\controlled-test-user-e\\AppData\\Local\\Browser\\Cache\\cache-note.txt", + "expected_detection_match": false, + "rationale": "Benign browser-cache event uses a non-script child process and must not match." + }, + { + "event_id": "controlled-test-event-006", + "event_time": "2026-06-27T00:00:06Z", + "host": "controlled-test-host-03", + "user": "controlled-test-user-f", + "parent_process_name": "explorer.exe", + "process_name": "powershell.exe", + "process_command_line": "powershell.exe -NoProfile -File C:\\Controlled-test\\Review\\admin-note.ps1", + "file_path": "C:\\Controlled-test\\Review\\admin-note.ps1", + "expected_detection_match": false, + "rationale": "Script interpreter child exists, but the parent is not browser-like and the file path is not cache-like." + }, + { + "event_id": "controlled-test-event-007", + "event_time": "2026-06-27T00:00:07Z", + "host": "controlled-test-host-04", + "user": "controlled-test-user-g", + "parent_process_name": "chrome.exe", + "process_name": "notepad.exe", + "process_command_line": "notepad.exe C:\\Controlled-test\\Review\\cache-text.txt", + "file_path": "C:\\Users\\controlled-test-user-g\\AppData\\Local\\Chrome\\User Data\\Default\\Cache\\cache-text.txt", + "expected_detection_match": false, + "rationale": "Browser parent and cache path exist, but the child process is not a script interpreter." + }, + { + "event_id": "controlled-test-event-008", + "event_time": "2026-06-27T00:00:08Z", + "host": "controlled-test-host-04", + "user": "controlled-test-user-h", + "parent_process_name": "msedge.exe", + "process_name": "powershell.exe", + "process_command_line": "powershell.exe -NoProfile -File C:\\Controlled-test\\Review\\download-followup.ps1", + "file_path": "C:\\Users\\controlled-test-user-h\\Downloads\\download-followup.ps1", + "expected_detection_match": false, + "rationale": "Browser parent and script interpreter child exist, but the file path is not cache-like." + }, + { + "event_id": "controlled-test-event-009", + "event_time": "2026-06-27T00:00:09Z", + "host": "controlled-test-host-05", + "user": "controlled-test-user-i", + "parent_process_name": "controlled_test_browser.exe", + "process_name": "rundll32.exe", + "process_command_line": "rundll32.exe C:\\Controlled-test\\Review\\sample.dll,ReviewEntry", + "file_path": "C:\\Users\\controlled-test-user-i\\AppData\\Local\\Browser\\Cache\\cache-item-009.bin", + "expected_detection_match": false, + "rationale": "Browser parent and cache path exist, but the child process is outside the controlled script-interpreter list." + }, + { + "event_id": "controlled-test-event-010", + "event_time": "2026-06-27T00:00:10Z", + "host": "controlled-test-host-05", + "user": "controlled-test-user-j", + "parent_process_name": "outlook.exe", + "process_name": "wscript.exe", + "process_command_line": "wscript.exe C:\\Controlled-test\\Review\\mail-followup.vbs", + "file_path": "C:\\Controlled-test\\Review\\mail-followup.vbs", + "expected_detection_match": false, + "rationale": "Script host child exists, but neither browser parent nor cache path is present." + }, + { + "event_id": "controlled-test-event-011", + "event_time": "2026-06-27T00:00:11Z", + "host": "controlled-test-host-06", + "user": "controlled-test-user-k", + "parent_process_name": "firefox.exe", + "process_name": "calc.exe", + "process_command_line": "calc.exe", + "file_path": "C:\\Users\\controlled-test-user-k\\AppData\\Local\\Firefox\\Profiles\\controlled-test\\cache2\\cache-item-011.bin", + "expected_detection_match": false, + "rationale": "Browser parent and cache path exist, but the child process is a benign calculator process." + }, + { + "event_id": "controlled-test-event-012", + "event_time": "2026-06-27T00:00:12Z", + "host": "controlled-test-host-06", + "user": "controlled-test-user-l", + "parent_process_name": "services.exe", + "process_name": "cmd.exe", + "process_command_line": "cmd.exe /c C:\\Controlled-test\\Review\\service-check.cmd", + "file_path": "C:\\Controlled-test\\Review\\service-check.cmd", + "expected_detection_match": false, + "rationale": "Command interpreter child exists, but the parent and path do not match the browser-cache scenario." + } + ] +} diff --git a/examples/gauntlet/expected-detection-results.json b/examples/gauntlet/expected-detection-results.json index d235f79..eaebc5e 100644 --- a/examples/gauntlet/expected-detection-results.json +++ b/examples/gauntlet/expected-detection-results.json @@ -1,8 +1,8 @@ { "schema_version": "expected-detection-results-v0", "artifact_id": "HOX-GAUNTLET-001", - "fixture_scope": "controlled synthetic fixture only", - "detector_note": "Controlled fixture perfect means the deterministic review rule matches this synthetic dataset exactly; it does not claim production perfection.", + "fixture_scope": "controlled controlled-test fixture only", + "detector_note": "Controlled fixture perfect means the deterministic review rule matches this controlled-test dataset exactly; it does not claim production perfection.", "events_total": 12, "expected_positive": 4, "expected_negative": 8, diff --git a/examples/gauntlet/sample-artifact.json b/examples/gauntlet/sample-artifact.json index cd56b70..852c8ad 100644 --- a/examples/gauntlet/sample-artifact.json +++ b/examples/gauntlet/sample-artifact.json @@ -1,11 +1,11 @@ { "schema_version": "sample-artifact-v0", "artifact_id": "HOX-GAUNTLET-001", - "title": "Synthetic Splunk SOC detection review for browser-cache payload extraction", + "title": "Controlled-test Splunk SOC detection review for browser-cache payload extraction", "ai_assisted": true, "scenario": "An AI assistant drafts a Splunk/SOC-style detection idea and release note for browser-cache / ClickFix-style payload extraction review.", "public_safety": { - "synthetic_only": true, + "controlled_test_only": true, "contains_malware_code": false, "contains_exploit_instructions": false, "contains_customer_data": false, @@ -17,13 +17,13 @@ "status": "SOURCE_CONTROLLED_SAMPLE" }, "detection_artifact": { - "type": "synthetic_splunk_detection_review", + "type": "controlled_test_detection_review", "platform": "Splunk", "soc_queue": "detection-engineering-review", "name": "Browser cache extraction followed by script interpreter staging", - "description": "Synthetic review object for suspicious browser cache file access followed by script interpreter staging. The object is a detection-review fixture, not deployable detection logic.", + "description": "Controlled-test review object for suspicious browser cache file access followed by script interpreter staging. The object is a detection-review fixture, not deployable detection logic.", "risk_hypothesis": "A user interaction lure may lead to cached payload extraction and local interpreter staging.", - "splunk_search_pseudocode": "index=synthetic_endpoint sourcetype=synthetic_process (parent_process_name IN (browser.exe, synthetic_browser.exe)) AND (process_name IN (powershell.exe, cmd.exe, wscript.exe)) AND file_path=\"*\\\\Browser\\\\Cache\\\\*\"", + "splunk_search_pseudocode": "index=controlled_test_endpoint sourcetype=controlled_test_process (parent_process_name IN (browser.exe, controlled_test_browser.exe)) AND (process_name IN (powershell.exe, cmd.exe, wscript.exe)) AND file_path=\"*\\\\Browser\\\\Cache\\\\*\"", "required_fields": [ "event_time", "host", @@ -43,8 +43,8 @@ }, "telemetry_contract": { "contract_id": "HOX-GAUNTLET-001-TELEMETRY-CONTRACT", - "status": "PASSED_SYNTHETIC_CONTRACT", - "fixture_scope": "synthetic Splunk-style events only", + "status": "CONTROLLED_TEST_VALIDATED", + "fixture_scope": "controlled-test Splunk-style events only", "required_fields_present": true, "runtime_source_required_for_stronger_claims": true }, diff --git a/examples/gauntlet/sample-evidence-graph.json b/examples/gauntlet/sample-evidence-graph.json index cf19ede..38f697a 100644 --- a/examples/gauntlet/sample-evidence-graph.json +++ b/examples/gauntlet/sample-evidence-graph.json @@ -19,7 +19,7 @@ "id": "HOX-GAUNTLET-001-TELEMETRY-CONTRACT", "type": "telemetry_contract_check", "label": "Telemetry Contract Check", - "status": "PASSED_SYNTHETIC_CONTRACT" + "status": "CONTROLLED_TEST_VALIDATED" }, { "id": "HOX-GAUNTLET-001-CONTROLLED-VALIDATION", @@ -107,7 +107,7 @@ ], "telemetry_contract": { "contract_id": "HOX-GAUNTLET-001-TELEMETRY-CONTRACT", - "status": "PASSED_SYNTHETIC_CONTRACT", + "status": "CONTROLLED_TEST_VALIDATED", "required_fields": [ "event_time", "host", diff --git a/examples/gauntlet/sample-proofcard.json b/examples/gauntlet/sample-proofcard.json index 6a28a04..d3a6942 100644 --- a/examples/gauntlet/sample-proofcard.json +++ b/examples/gauntlet/sample-proofcard.json @@ -5,13 +5,13 @@ "title": "Hoxline Gauntlet v0 Splunk ProofOps lab ProofCard", "proof_ceiling": "CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY", "what_exists": [ - "Synthetic Splunk/SOC-style detection-review artifact in source control.", + "Controlled-test Splunk/SOC-style detection-review artifact in source control.", "Bad AI-assisted release note with unsupported claims.", "Safe release note constrained to controlled validation and claim-boundary workflow.", "Evidence graph, promotion state, telemetry contract, controlled validation result, and Claim Authority output." ], "what_was_tested": [ - "Required synthetic telemetry fields are declared.", + "Required controlled-test telemetry fields are declared.", "Positive fixture expectation equals four matches.", "Negative fixture expectation equals eight non-matches.", "Bad release note is blocked by Claim Authority policy.", @@ -20,7 +20,7 @@ "validation_result": { "status": "PASSED_CONTROLLED_FIXTURES", "deterministic": true, - "scope": "local synthetic fixture validation only", + "scope": "local controlled-test fixture validation only", "positive_fixture_expected_matches": 4, "positive_fixture_actual_matches": 4, "negative_fixture_expected_matches": 8, diff --git a/examples/gauntlet/synthetic-events.json b/examples/gauntlet/synthetic-events.json deleted file mode 100644 index 0016753..0000000 --- a/examples/gauntlet/synthetic-events.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "schema_version": "synthetic-events-v0", - "artifact_id": "HOX-GAUNTLET-001", - "dataset_scope": "controlled synthetic fixture only", - "contains_malware_code": false, - "contains_exploit_instructions": false, - "contains_customer_data": false, - "contains_private_runtime_evidence": false, - "events": [ - { - "event_id": "synthetic-event-001", - "event_time": "2026-06-27T00:00:01Z", - "host": "synthetic-host-01", - "user": "synthetic-user-a", - "parent_process_name": "synthetic_browser.exe", - "process_name": "powershell.exe", - "process_command_line": "powershell.exe -NoProfile -File C:\\Synthetic\\Review\\cache-followup.ps1", - "file_path": "C:\\Users\\synthetic-user-a\\AppData\\Local\\Browser\\Cache\\cache-item-001.tmp", - "expected_detection_match": true, - "rationale": "Browser-like parent, cache-like path, and script interpreter child match the controlled review rule." - }, - { - "event_id": "synthetic-event-002", - "event_time": "2026-06-27T00:00:02Z", - "host": "synthetic-host-01", - "user": "synthetic-user-b", - "parent_process_name": "chrome.exe", - "process_name": "cmd.exe", - "process_command_line": "cmd.exe /c C:\\Synthetic\\Review\\cache-followup.cmd", - "file_path": "C:\\Users\\synthetic-user-b\\AppData\\Local\\Chrome\\User Data\\Default\\Cache\\cache-item-002.bin", - "expected_detection_match": true, - "rationale": "Chrome parent, browser cache path, and command interpreter child match the controlled review rule." - }, - { - "event_id": "synthetic-event-003", - "event_time": "2026-06-27T00:00:03Z", - "host": "synthetic-host-02", - "user": "synthetic-user-c", - "parent_process_name": "msedge.exe", - "process_name": "wscript.exe", - "process_command_line": "wscript.exe C:\\Synthetic\\Review\\cache-followup.vbs", - "file_path": "C:\\Users\\synthetic-user-c\\AppData\\Local\\Edge\\User Data\\Default\\Cache\\cache-item-003.dat", - "expected_detection_match": true, - "rationale": "Edge parent, cache path, and Windows script host child match the controlled review rule." - }, - { - "event_id": "synthetic-event-004", - "event_time": "2026-06-27T00:00:04Z", - "host": "synthetic-host-02", - "user": "synthetic-user-d", - "parent_process_name": "firefox.exe", - "process_name": "pwsh.exe", - "process_command_line": "pwsh.exe -File C:\\Synthetic\\Review\\cache-followup.ps1", - "file_path": "C:\\Users\\synthetic-user-d\\AppData\\Local\\Firefox\\Profiles\\synthetic\\cache2\\cache-item-004.bin", - "expected_detection_match": true, - "rationale": "Firefox parent, cache path, and PowerShell child match the controlled review rule." - }, - { - "event_id": "synthetic-event-005", - "event_time": "2026-06-27T00:00:05Z", - "host": "synthetic-host-03", - "user": "synthetic-user-e", - "parent_process_name": "synthetic_browser.exe", - "process_name": "notepad.exe", - "process_command_line": "notepad.exe C:\\Synthetic\\Review\\cache-note.txt", - "file_path": "C:\\Users\\synthetic-user-e\\AppData\\Local\\Browser\\Cache\\cache-note.txt", - "expected_detection_match": false, - "rationale": "Benign browser-cache event uses a non-script child process and must not match." - }, - { - "event_id": "synthetic-event-006", - "event_time": "2026-06-27T00:00:06Z", - "host": "synthetic-host-03", - "user": "synthetic-user-f", - "parent_process_name": "explorer.exe", - "process_name": "powershell.exe", - "process_command_line": "powershell.exe -NoProfile -File C:\\Synthetic\\Review\\admin-note.ps1", - "file_path": "C:\\Synthetic\\Review\\admin-note.ps1", - "expected_detection_match": false, - "rationale": "Script interpreter child exists, but the parent is not browser-like and the file path is not cache-like." - }, - { - "event_id": "synthetic-event-007", - "event_time": "2026-06-27T00:00:07Z", - "host": "synthetic-host-04", - "user": "synthetic-user-g", - "parent_process_name": "chrome.exe", - "process_name": "notepad.exe", - "process_command_line": "notepad.exe C:\\Synthetic\\Review\\cache-text.txt", - "file_path": "C:\\Users\\synthetic-user-g\\AppData\\Local\\Chrome\\User Data\\Default\\Cache\\cache-text.txt", - "expected_detection_match": false, - "rationale": "Browser parent and cache path exist, but the child process is not a script interpreter." - }, - { - "event_id": "synthetic-event-008", - "event_time": "2026-06-27T00:00:08Z", - "host": "synthetic-host-04", - "user": "synthetic-user-h", - "parent_process_name": "msedge.exe", - "process_name": "powershell.exe", - "process_command_line": "powershell.exe -NoProfile -File C:\\Synthetic\\Review\\download-followup.ps1", - "file_path": "C:\\Users\\synthetic-user-h\\Downloads\\download-followup.ps1", - "expected_detection_match": false, - "rationale": "Browser parent and script interpreter child exist, but the file path is not cache-like." - }, - { - "event_id": "synthetic-event-009", - "event_time": "2026-06-27T00:00:09Z", - "host": "synthetic-host-05", - "user": "synthetic-user-i", - "parent_process_name": "synthetic_browser.exe", - "process_name": "rundll32.exe", - "process_command_line": "rundll32.exe C:\\Synthetic\\Review\\sample.dll,ReviewEntry", - "file_path": "C:\\Users\\synthetic-user-i\\AppData\\Local\\Browser\\Cache\\cache-item-009.bin", - "expected_detection_match": false, - "rationale": "Browser parent and cache path exist, but the child process is outside the controlled script-interpreter list." - }, - { - "event_id": "synthetic-event-010", - "event_time": "2026-06-27T00:00:10Z", - "host": "synthetic-host-05", - "user": "synthetic-user-j", - "parent_process_name": "outlook.exe", - "process_name": "wscript.exe", - "process_command_line": "wscript.exe C:\\Synthetic\\Review\\mail-followup.vbs", - "file_path": "C:\\Synthetic\\Review\\mail-followup.vbs", - "expected_detection_match": false, - "rationale": "Script host child exists, but neither browser parent nor cache path is present." - }, - { - "event_id": "synthetic-event-011", - "event_time": "2026-06-27T00:00:11Z", - "host": "synthetic-host-06", - "user": "synthetic-user-k", - "parent_process_name": "firefox.exe", - "process_name": "calc.exe", - "process_command_line": "calc.exe", - "file_path": "C:\\Users\\synthetic-user-k\\AppData\\Local\\Firefox\\Profiles\\synthetic\\cache2\\cache-item-011.bin", - "expected_detection_match": false, - "rationale": "Browser parent and cache path exist, but the child process is a benign calculator process." - }, - { - "event_id": "synthetic-event-012", - "event_time": "2026-06-27T00:00:12Z", - "host": "synthetic-host-06", - "user": "synthetic-user-l", - "parent_process_name": "services.exe", - "process_name": "cmd.exe", - "process_command_line": "cmd.exe /c C:\\Synthetic\\Review\\service-check.cmd", - "file_path": "C:\\Synthetic\\Review\\service-check.cmd", - "expected_detection_match": false, - "rationale": "Command interpreter child exists, but the parent and path do not match the browser-cache scenario." - } - ] -} diff --git a/examples/review/aws-det-001-artifact-manifest-v1.json b/examples/review/aws-det-001-artifact-manifest-v1.json new file mode 100644 index 0000000..b75d29b --- /dev/null +++ b/examples/review/aws-det-001-artifact-manifest-v1.json @@ -0,0 +1,78 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "controlled local fixture review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "AWS-DET-001", + "artifact_name": "AWS IAM denied action fixture review", + "artifact_type": "detection-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "confidence": "bounded-fixture-only", + "detection_family": "aws-det-001", + "endpoint_mutation": false, + "expected_event_ids": [], + "expected_event_keys": [ + "CreateUser" + ], + "expected_rule_ids": [], + "field_mapping": { + "event_key": "fixture selector", + "channel": "fixture source label", + "action": "bounded fixture action", + "actor": "controlled-test actor label" + }, + "fixture_paths": { + "negative": "examples/review/fixtures/aws-det-001-safe-negative-fixture.json", + "positive": "examples/review/fixtures/aws-det-001-safe-fixture.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "human_review_gate", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "AWS-DET-001 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." + ], + "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", + "runtime_proof": false, + "severity": "medium", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [], + "event_keys": [ + "CreateUser" + ], + "event_key_field": "event_key", + "required_fields": [ + "event_key", + "channel", + "action", + "actor" + ], + "scope": "fixture-only metadata contract", + "source": "AWS CloudTrail-style controlled fixture metadata", + "source_control_note": "Source and controlled-validation metadata remain owned by their repositories; this manifest carries sanitized fixture metadata only.", + "wazuh_rule_ids": [] + }, + "triage_what_happened": "A controlled-test fixture represented the AWS-DET-001 controlled review pattern.", + "triage_why_it_matters": "The bounded pattern is useful for deterministic reviewer reproduction when kept separate from runtime and proof claims.", + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/fixtures/aws-det-001-safe-fixture.json b/examples/review/fixtures/aws-det-001-safe-fixture.json new file mode 100644 index 0000000..ccd6f6b --- /dev/null +++ b/examples/review/fixtures/aws-det-001-safe-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "AWS-DET-001", + "endpoint_mutation": false, + "events": [ + { + "event_key": "CreateUser", + "channel": "AWS CloudTrail fixture", + "action": "iam_action", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": true, + "fixture_id": "aws-det-001-positive-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/aws-det-001-safe-negative-fixture.json b/examples/review/fixtures/aws-det-001-safe-negative-fixture.json new file mode 100644 index 0000000..23f2e0d --- /dev/null +++ b/examples/review/fixtures/aws-det-001-safe-negative-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "AWS-DET-001", + "endpoint_mutation": false, + "events": [ + { + "event_key": "ListUsers", + "channel": "AWS CloudTrail fixture", + "action": "iam_action", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": false, + "fixture_id": "aws-det-001-negative-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/ho-det-009-safe-fixture.json b/examples/review/fixtures/ho-det-009-safe-fixture.json index 438500a..648f06b 100644 --- a/examples/review/fixtures/ho-det-009-safe-fixture.json +++ b/examples/review/fixtures/ho-det-009-safe-fixture.json @@ -1,6 +1,6 @@ { "artifact_id": "HO-DET-009", - "description": "Synthetic fixture for HO-DET-009; local review only and not runtime evidence.", + "description": "Controlled-test fixture for HO-DET-009; local review only and not runtime evidence.", "endpoint_mutation": false, "events": [ { @@ -13,7 +13,7 @@ ], "expected_detection": true, "fixture_id": "ho-det-009-safe-positive-fixture-v1", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "host": "demo-host-009", "network_required": false, "runtime_required": false, diff --git a/examples/review/fixtures/ho-det-009-safe-negative-fixture.json b/examples/review/fixtures/ho-det-009-safe-negative-fixture.json index 250fa6c..aebe4fa 100644 --- a/examples/review/fixtures/ho-det-009-safe-negative-fixture.json +++ b/examples/review/fixtures/ho-det-009-safe-negative-fixture.json @@ -1,6 +1,6 @@ { "artifact_id": "HO-DET-009", - "description": "Synthetic negative fixture for HO-DET-009; local review only and not runtime evidence.", + "description": "Controlled-test negative fixture for HO-DET-009; local review only and not runtime evidence.", "endpoint_mutation": false, "events": [ { @@ -13,7 +13,7 @@ ], "expected_detection": false, "fixture_id": "ho-det-009-safe-negative-fixture-v1", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "host": "demo-host-009", "network_required": false, "runtime_required": false, diff --git a/examples/review/fixtures/ho-det-011-safe-fixture.json b/examples/review/fixtures/ho-det-011-safe-fixture.json index d8c25ea..8148538 100644 --- a/examples/review/fixtures/ho-det-011-safe-fixture.json +++ b/examples/review/fixtures/ho-det-011-safe-fixture.json @@ -1,6 +1,6 @@ { "artifact_id": "HO-DET-011", - "description": "Synthetic fixture for HO-DET-011; local review only and not runtime evidence.", + "description": "Controlled-test fixture for HO-DET-011; local review only and not runtime evidence.", "endpoint_mutation": false, "events": [ { @@ -14,7 +14,7 @@ ], "expected_detection": true, "fixture_id": "ho-det-011-safe-positive-fixture-v1", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "host": "demo-host-011", "network_required": false, "runtime_required": false, diff --git a/examples/review/fixtures/ho-det-011-safe-negative-fixture.json b/examples/review/fixtures/ho-det-011-safe-negative-fixture.json index 3ae3f76..5e1f299 100644 --- a/examples/review/fixtures/ho-det-011-safe-negative-fixture.json +++ b/examples/review/fixtures/ho-det-011-safe-negative-fixture.json @@ -1,6 +1,6 @@ { "artifact_id": "HO-DET-011", - "description": "Synthetic negative fixture for HO-DET-011; local review only and not runtime evidence.", + "description": "Controlled-test negative fixture for HO-DET-011; local review only and not runtime evidence.", "endpoint_mutation": false, "events": [ { @@ -14,7 +14,7 @@ ], "expected_detection": false, "fixture_id": "ho-det-011-safe-negative-fixture-v1", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "host": "demo-host-011", "network_required": false, "runtime_required": false, diff --git a/examples/review/fixtures/ho-det-012-safe-fixture.json b/examples/review/fixtures/ho-det-012-safe-fixture.json index 0b5b176..2d70846 100644 --- a/examples/review/fixtures/ho-det-012-safe-fixture.json +++ b/examples/review/fixtures/ho-det-012-safe-fixture.json @@ -1,6 +1,6 @@ { "artifact_id": "HO-DET-012", - "description": "Synthetic fixture for HO-DET-012; local review only and not runtime evidence.", + "description": "Controlled-test fixture for HO-DET-012; local review only and not runtime evidence.", "endpoint_mutation": false, "events": [ { @@ -14,7 +14,7 @@ ], "expected_detection": true, "fixture_id": "ho-det-012-safe-positive-fixture-v1", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "host": "demo-host-012", "network_required": false, "runtime_required": false, diff --git a/examples/review/fixtures/ho-det-012-safe-negative-fixture.json b/examples/review/fixtures/ho-det-012-safe-negative-fixture.json index 4e2a353..680b25c 100644 --- a/examples/review/fixtures/ho-det-012-safe-negative-fixture.json +++ b/examples/review/fixtures/ho-det-012-safe-negative-fixture.json @@ -1,6 +1,6 @@ { "artifact_id": "HO-DET-012", - "description": "Synthetic negative fixture for HO-DET-012; local review only and not runtime evidence.", + "description": "Controlled-test negative fixture for HO-DET-012; local review only and not runtime evidence.", "endpoint_mutation": false, "events": [ { @@ -14,7 +14,7 @@ ], "expected_detection": false, "fixture_id": "ho-det-012-safe-negative-fixture-v1", - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "host": "demo-host-012", "network_required": false, "runtime_required": false, diff --git a/examples/review/fixtures/ho-det-013-safe-fixture.json b/examples/review/fixtures/ho-det-013-safe-fixture.json new file mode 100644 index 0000000..a6dc3bc --- /dev/null +++ b/examples/review/fixtures/ho-det-013-safe-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "HO-DET-013", + "endpoint_mutation": false, + "events": [ + { + "event_id": 1102, + "channel": "Windows Security", + "action": "security_control_tamper", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": true, + "fixture_id": "ho-det-013-positive-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/ho-det-013-safe-negative-fixture.json b/examples/review/fixtures/ho-det-013-safe-negative-fixture.json new file mode 100644 index 0000000..6e163bd --- /dev/null +++ b/examples/review/fixtures/ho-det-013-safe-negative-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "HO-DET-013", + "endpoint_mutation": false, + "events": [ + { + "event_id": 7036, + "channel": "Windows Security", + "action": "security_control_tamper", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": false, + "fixture_id": "ho-det-013-negative-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-001-safe-fixture.json b/examples/review/fixtures/id-det-001-safe-fixture.json new file mode 100644 index 0000000..e8a822e --- /dev/null +++ b/examples/review/fixtures/id-det-001-safe-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-001", + "endpoint_mutation": false, + "events": [ + { + "event_key": "impossible_travel", + "channel": "Identity fixture", + "action": "identity_session_context", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": true, + "fixture_id": "id-det-001-positive-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-001-safe-negative-fixture.json b/examples/review/fixtures/id-det-001-safe-negative-fixture.json new file mode 100644 index 0000000..6ef9c88 --- /dev/null +++ b/examples/review/fixtures/id-det-001-safe-negative-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-001", + "endpoint_mutation": false, + "events": [ + { + "event_key": "known_device_login", + "channel": "Identity fixture", + "action": "identity_session_context", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": false, + "fixture_id": "id-det-001-negative-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-002-safe-fixture.json b/examples/review/fixtures/id-det-002-safe-fixture.json new file mode 100644 index 0000000..9d4783d --- /dev/null +++ b/examples/review/fixtures/id-det-002-safe-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-002", + "endpoint_mutation": false, + "events": [ + { + "event_key": "mfa_push_fatigue_volume", + "channel": "Identity fixture", + "action": "authentication_control_change", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": true, + "fixture_id": "id-det-002-positive-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-002-safe-negative-fixture.json b/examples/review/fixtures/id-det-002-safe-negative-fixture.json new file mode 100644 index 0000000..8d453fc --- /dev/null +++ b/examples/review/fixtures/id-det-002-safe-negative-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-002", + "endpoint_mutation": false, + "events": [ + { + "event_key": "approved_authentication_change", + "channel": "Identity fixture", + "action": "authentication_control_change", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": false, + "fixture_id": "id-det-002-negative-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-003-safe-fixture.json b/examples/review/fixtures/id-det-003-safe-fixture.json new file mode 100644 index 0000000..2e3f667 --- /dev/null +++ b/examples/review/fixtures/id-det-003-safe-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-003", + "endpoint_mutation": false, + "events": [ + { + "event_key": "privileged_role_assignment", + "channel": "Identity fixture", + "action": "privilege_change", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": true, + "fixture_id": "id-det-003-positive-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-003-safe-negative-fixture.json b/examples/review/fixtures/id-det-003-safe-negative-fixture.json new file mode 100644 index 0000000..b3dcb2e --- /dev/null +++ b/examples/review/fixtures/id-det-003-safe-negative-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-003", + "endpoint_mutation": false, + "events": [ + { + "event_key": "approved_role_assignment", + "channel": "Identity fixture", + "action": "privilege_change", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": false, + "fixture_id": "id-det-003-negative-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-004-safe-fixture.json b/examples/review/fixtures/id-det-004-safe-fixture.json new file mode 100644 index 0000000..649c671 --- /dev/null +++ b/examples/review/fixtures/id-det-004-safe-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-004", + "endpoint_mutation": false, + "events": [ + { + "event_key": "impossible_travel", + "channel": "Identity fixture", + "action": "travel_session_context", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": true, + "fixture_id": "id-det-004-positive-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/fixtures/id-det-004-safe-negative-fixture.json b/examples/review/fixtures/id-det-004-safe-negative-fixture.json new file mode 100644 index 0000000..6789f67 --- /dev/null +++ b/examples/review/fixtures/id-det-004-safe-negative-fixture.json @@ -0,0 +1,20 @@ +{ + "artifact_id": "ID-DET-004", + "endpoint_mutation": false, + "events": [ + { + "event_key": "approved_travel", + "channel": "Identity fixture", + "action": "travel_session_context", + "actor": "controlled-test-review-actor" + } + ], + "expected_detection": false, + "fixture_id": "id-det-004-negative-fixture-v1", + "fixture_kind": "controlled-test-demo-only", + "host": "controlled-test-fixture-host", + "network_required": false, + "runtime_required": false, + "safe_fixture": true, + "schema_version": "hoxline-demo-fixture-v0" +} diff --git a/examples/review/ho-det-009-artifact-manifest-v1.json b/examples/review/ho-det-009-artifact-manifest-v1.json index acebc4a..760a3f0 100644 --- a/examples/review/ho-det-009-artifact-manifest-v1.json +++ b/examples/review/ho-det-009-artifact-manifest-v1.json @@ -1,7 +1,7 @@ { "ai_disposition_authority": false, "allowed_claim_class": "controlled local fixture review only", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "artifact_id": "HO-DET-009", "artifact_name": "Windows local user account creation detection", "artifact_type": "detection-review-artifact", @@ -39,7 +39,7 @@ ], "field_mapping": { "action": "account lifecycle action", - "actor": "synthetic actor label", + "actor": "controlled-test actor label", "event_id": "event identifier", "target_account": "local account under review" }, @@ -58,12 +58,12 @@ "public_proof_promoted": false, "public_safe_status": "NOT_PUBLIC_SAFE", "requested_claims": [ - "HO-DET-009 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled synthetic fixtures only." + "HO-DET-009 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." ], "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", "runtime_proof": false, "severity": "medium", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "source_owner": "hawkinsoperations-detections", "telemetry_contract": { "event_ids": [ @@ -79,14 +79,14 @@ ], "scope": "fixture-only metadata contract", "source": "Windows Security EventChannel", - "source_control_note": "Source-controlled detection and validation metadata exists outside Hoxline; this manifest carries only synthetic fixture metadata.", + "source_control_note": "Source-controlled detection and validation metadata exists outside Hoxline; this manifest carries only controlled-test fixture metadata.", "wazuh_rule_ids": [ 910091, 910092, 910093 ] }, - "triage_what_happened": "A synthetic fixture represented a local user account creation pattern.", + "triage_what_happened": "A controlled-test fixture represented a local user account creation pattern.", "triage_why_it_matters": "Unexpected local account creation can indicate account lifecycle abuse when backed by governed evidence.", "validation_owner": "hawkinsoperations-validation", "wazuh_mutation": false diff --git a/examples/review/ho-det-010-artifact-manifest-v1.json b/examples/review/ho-det-010-artifact-manifest-v1.json index 9e938d0..adc3a3a 100644 --- a/examples/review/ho-det-010-artifact-manifest-v1.json +++ b/examples/review/ho-det-010-artifact-manifest-v1.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -12,12 +12,8 @@ "telemetry_contract": { "source": "Windows Security EventChannel", "event_ids": [ - 4720, - 4725, - 4726, 4732, - 4733, - 4738 + 4733 ], "wazuh_rule_ids": [ 910101, @@ -39,12 +35,8 @@ "negative": "examples/demo/ho-det-010-safe-negative-fixture.json" }, "expected_event_ids": [ - 4720, - 4725, - 4726, 4732, - 4733, - 4738 + 4733 ], "expected_rule_ids": [ 910101, @@ -53,7 +45,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -78,6 +70,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/ho-det-011-artifact-manifest-v1.json b/examples/review/ho-det-011-artifact-manifest-v1.json index 37418ac..69898ad 100644 --- a/examples/review/ho-det-011-artifact-manifest-v1.json +++ b/examples/review/ho-det-011-artifact-manifest-v1.json @@ -4,7 +4,7 @@ ], "ai_disposition_authority": false, "allowed_claim_class": "controlled local fixture review only", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "artifact_id": "HO-DET-011", "artifact_name": "Windows service creation or service binary change detection", "artifact_type": "detection-review-artifact", @@ -42,7 +42,7 @@ ], "field_mapping": { "action": "service creation action", - "actor": "synthetic actor label", + "actor": "controlled-test actor label", "event_id": "event identifier", "service_image_path": "service binary path label", "service_name": "service under review" @@ -62,12 +62,12 @@ "public_proof_promoted": false, "public_safe_status": "NOT_PUBLIC_SAFE", "requested_claims": [ - "HO-DET-011 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled synthetic fixtures only." + "HO-DET-011 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." ], "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", "runtime_proof": false, "severity": "medium", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "source_owner": "hawkinsoperations-detections", "telemetry_contract": { "event_ids": [ @@ -83,7 +83,7 @@ ], "scope": "fixture-only metadata contract", "source": "Windows Security EventChannel", - "source_control_note": "Source-controlled detection and validation metadata exists outside Hoxline; this manifest carries only synthetic fixture metadata.", + "source_control_note": "Source-controlled detection and validation metadata exists outside Hoxline; this manifest carries only controlled-test fixture metadata.", "wazuh_rule_ids": [ 910011, 910012, @@ -91,7 +91,7 @@ 910014 ] }, - "triage_what_happened": "A synthetic fixture represented a Windows service creation pattern.", + "triage_what_happened": "A controlled-test fixture represented a Windows service creation pattern.", "triage_why_it_matters": "Unexpected service creation can indicate persistence when backed by governed evidence.", "validation_owner": "hawkinsoperations-validation", "wazuh_mutation": false diff --git a/examples/review/ho-det-012-artifact-manifest-v1.json b/examples/review/ho-det-012-artifact-manifest-v1.json index 50009b7..df01cc0 100644 --- a/examples/review/ho-det-012-artifact-manifest-v1.json +++ b/examples/review/ho-det-012-artifact-manifest-v1.json @@ -4,7 +4,7 @@ ], "ai_disposition_authority": false, "allowed_claim_class": "controlled local fixture review only", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "artifact_id": "HO-DET-012", "artifact_name": "Suspicious scheduled task creation or update detection", "artifact_type": "detection-review-artifact", @@ -42,7 +42,7 @@ ], "field_mapping": { "action": "task lifecycle action", - "actor": "synthetic actor label", + "actor": "controlled-test actor label", "event_id": "event identifier", "task_action": "task action label", "task_name": "scheduled task under review" @@ -62,12 +62,12 @@ "public_proof_promoted": false, "public_safe_status": "NOT_PUBLIC_SAFE", "requested_claims": [ - "HO-DET-012 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled synthetic fixtures only." + "HO-DET-012 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." ], "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", "runtime_proof": false, "severity": "medium", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "source_owner": "hawkinsoperations-detections", "telemetry_contract": { "event_ids": [ @@ -84,14 +84,14 @@ ], "scope": "fixture-only metadata contract", "source": "Windows Security EventChannel", - "source_control_note": "Source-controlled detection and validation metadata exists outside Hoxline; this manifest carries only synthetic fixture metadata.", + "source_control_note": "Source-controlled detection and validation metadata exists outside Hoxline; this manifest carries only controlled-test fixture metadata.", "wazuh_rule_ids": [ 910021, 910022, 910023 ] }, - "triage_what_happened": "A synthetic fixture represented a scheduled task creation pattern.", + "triage_what_happened": "A controlled-test fixture represented a scheduled task creation pattern.", "triage_why_it_matters": "Unexpected scheduled tasks can indicate persistence when backed by governed evidence.", "validation_owner": "hawkinsoperations-validation", "wazuh_mutation": false diff --git a/examples/review/ho-det-013-artifact-manifest-v1.json b/examples/review/ho-det-013-artifact-manifest-v1.json new file mode 100644 index 0000000..3e42d8b --- /dev/null +++ b/examples/review/ho-det-013-artifact-manifest-v1.json @@ -0,0 +1,78 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "controlled local fixture review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "HO-DET-013", + "artifact_name": "Security control tamper fixture review", + "artifact_type": "detection-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "confidence": "bounded-fixture-only", + "detection_family": "ho-det-013", + "endpoint_mutation": false, + "expected_event_ids": [ + 1102 + ], + "expected_event_keys": [], + "expected_rule_ids": [], + "field_mapping": { + "event_id": "fixture selector", + "channel": "fixture source label", + "action": "bounded fixture action", + "actor": "controlled-test actor label" + }, + "fixture_paths": { + "negative": "examples/review/fixtures/ho-det-013-safe-negative-fixture.json", + "positive": "examples/review/fixtures/ho-det-013-safe-fixture.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "human_review_gate", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "HO-DET-013 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." + ], + "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", + "runtime_proof": false, + "severity": "medium", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [ + 1102 + ], + "event_keys": [], + "event_key_field": "event_id", + "required_fields": [ + "event_id", + "channel", + "action", + "actor" + ], + "scope": "fixture-only metadata contract", + "source": "Windows controlled fixture metadata", + "source_control_note": "Source and controlled-validation metadata remain owned by their repositories; this manifest carries sanitized fixture metadata only.", + "wazuh_rule_ids": [] + }, + "triage_what_happened": "A controlled-test fixture represented the HO-DET-013 controlled review pattern.", + "triage_why_it_matters": "The bounded pattern is useful for deterministic reviewer reproduction when kept separate from runtime and proof claims.", + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/ho-ndr-001-artifact-manifest-v1.json b/examples/review/ho-ndr-001-artifact-manifest-v1.json new file mode 100644 index 0000000..4eaffd7 --- /dev/null +++ b/examples/review/ho-ndr-001-artifact-manifest-v1.json @@ -0,0 +1,64 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "boundary contract review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "HO-NDR-001", + "artifact_name": "Security Onion NDR boundary contract", + "artifact_type": "boundary-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "endpoint_mutation": false, + "expected_block_reason": "HO-NDR-001 is boundary-contract scoped and has no owned positive and negative fixture contract; runtime, telemetry, and live Security Onion claims remain blocked.", + "expected_event_ids": [], + "expected_event_keys": [], + "expected_review_outcome": "BLOCKED", + "expected_rule_ids": [], + "fixture_paths": { + "negative": "examples/review/fixtures/ho-ndr-001-boundary-negative-unavailable.json", + "positive": "examples/review/fixtures/ho-ndr-001-boundary-positive-unavailable.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "separate owned fixture contract and human review", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Boundary contract only; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "HO-NDR-001 remains blocked in local fixture review until an owned fixture contract exists." + ], + "runtime_boundary": "No runtime execution, Security Onion access, endpoint mutation, SIEM action, or live signal action.", + "runtime_proof": false, + "signal_boundary": "No controlled-test or live signal is claimed.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [], + "event_keys": [], + "event_key_field": "event_key", + "required_fields": [ + "event_key", + "channel", + "action", + "actor" + ], + "scope": "boundary contract only", + "source": "Security Onion boundary metadata only", + "wazuh_rule_ids": [] + }, + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/hostile-batch/duplicate-artifact-id-index.json b/examples/review/hostile-batch/duplicate-artifact-id-index.json index 23c0172..7fe7ac5 100644 --- a/examples/review/hostile-batch/duplicate-artifact-id-index.json +++ b/examples/review/hostile-batch/duplicate-artifact-id-index.json @@ -32,5 +32,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile-batch/missing-manifest-index.json b/examples/review/hostile-batch/missing-manifest-index.json index 3743d86..efbf9f0 100644 --- a/examples/review/hostile-batch/missing-manifest-index.json +++ b/examples/review/hostile-batch/missing-manifest-index.json @@ -40,5 +40,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile-batch/private-evidence-batch-index.json b/examples/review/hostile-batch/private-evidence-batch-index.json index 4693706..51d09c5 100644 --- a/examples/review/hostile-batch/private-evidence-batch-index.json +++ b/examples/review/hostile-batch/private-evidence-batch-index.json @@ -37,9 +37,9 @@ "index_id": "hoxline-multi-artifact-review-index-v1", "index_version": "multi-artifact-review-index-v1", "next_gate": "human_review_gate", - "private_evidence_attempt": "blocked synthetic hostile field marker", + "private_evidence_attempt": "blocked controlled-test hostile field marker", "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile-batch/production-claim-batch-index.json b/examples/review/hostile-batch/production-claim-batch-index.json index 2bf5b6c..6923ea2 100644 --- a/examples/review/hostile-batch/production-claim-batch-index.json +++ b/examples/review/hostile-batch/production-claim-batch-index.json @@ -40,5 +40,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile-batch/unexpected-blocked-pass-index.json b/examples/review/hostile-batch/unexpected-blocked-pass-index.json index 1cef231..68923b6 100644 --- a/examples/review/hostile-batch/unexpected-blocked-pass-index.json +++ b/examples/review/hostile-batch/unexpected-blocked-pass-index.json @@ -41,5 +41,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile-batch/unexpected-pass-index.json b/examples/review/hostile-batch/unexpected-pass-index.json index 95e6729..e40a7f5 100644 --- a/examples/review/hostile-batch/unexpected-pass-index.json +++ b/examples/review/hostile-batch/unexpected-pass-index.json @@ -41,5 +41,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile-batch/unsafe-batch-public-safe-claim-index.json b/examples/review/hostile-batch/unsafe-batch-public-safe-claim-index.json index abf3e13..fe1bc4e 100644 --- a/examples/review/hostile-batch/unsafe-batch-public-safe-claim-index.json +++ b/examples/review/hostile-batch/unsafe-batch-public-safe-claim-index.json @@ -40,5 +40,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/examples/review/hostile/missing-fixture-path.json b/examples/review/hostile/missing-fixture-path.json index 97dea18..c5813c6 100644 --- a/examples/review/hostile/missing-fixture-path.json +++ b/examples/review/hostile/missing-fixture-path.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -53,7 +53,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/missing-telemetry-contract.json b/examples/review/hostile/missing-telemetry-contract.json index f3be51d..c56b1f2 100644 --- a/examples/review/hostile/missing-telemetry-contract.json +++ b/examples/review/hostile/missing-telemetry-contract.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -28,7 +28,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -53,6 +53,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/private-evidence-field-attempt.json b/examples/review/hostile/private-evidence-field-attempt.json index 41d69fd..50f16be 100644 --- a/examples/review/hostile/private-evidence-field-attempt.json +++ b/examples/review/hostile/private-evidence-field-attempt.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -53,7 +53,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -78,7 +78,7 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate", - "private_evidence": "synthetic hostile private evidence field attempt" + "private_evidence": "controlled-test hostile private evidence field attempt" } diff --git a/examples/review/hostile/private-execution-id-field-attempt.json b/examples/review/hostile/private-execution-id-field-attempt.json index 02407af..7ed39de 100644 --- a/examples/review/hostile/private-execution-id-field-attempt.json +++ b/examples/review/hostile/private-execution-id-field-attempt.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -53,7 +53,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -78,7 +78,7 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate", - "private_execution_id": "synthetic-hostile-private-execution-id" + "private_execution_id": "controlled-test-hostile-private-execution-id" } diff --git a/examples/review/hostile/raw-alert-like-field-attempt.json b/examples/review/hostile/raw-alert-like-field-attempt.json index 246b4d2..8ffdfa1 100644 --- a/examples/review/hostile/raw-alert-like-field-attempt.json +++ b/examples/review/hostile/raw-alert-like-field-attempt.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -53,7 +53,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -78,7 +78,7 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate", - "raw_alert": "synthetic hostile raw alert-like field attempt" + "raw_alert": "controlled-test hostile raw alert-like field attempt" } diff --git a/examples/review/hostile/requested-ai-approved-claim.json b/examples/review/hostile/requested-ai-approved-claim.json index b314199..df8f223 100644 --- a/examples/review/hostile/requested-ai-approved-claim.json +++ b/examples/review/hostile/requested-ai-approved-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-analyst-approved-claim.json b/examples/review/hostile/requested-analyst-approved-claim.json index c278d62..f9ec1e1 100644 --- a/examples/review/hostile/requested-analyst-approved-claim.json +++ b/examples/review/hostile/requested-analyst-approved-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-autonomous-soc-claim.json b/examples/review/hostile/requested-autonomous-soc-claim.json index 9f2698f..f8ef378 100644 --- a/examples/review/hostile/requested-autonomous-soc-claim.json +++ b/examples/review/hostile/requested-autonomous-soc-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-case-closure-claim.json b/examples/review/hostile/requested-case-closure-claim.json index 9f393c7..d89d929 100644 --- a/examples/review/hostile/requested-case-closure-claim.json +++ b/examples/review/hostile/requested-case-closure-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-customer-claim.json b/examples/review/hostile/requested-customer-claim.json index b37f5fd..e4fa99d 100644 --- a/examples/review/hostile/requested-customer-claim.json +++ b/examples/review/hostile/requested-customer-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-final-authorization-claim.json b/examples/review/hostile/requested-final-authorization-claim.json index ce00316..84a72ec 100644 --- a/examples/review/hostile/requested-final-authorization-claim.json +++ b/examples/review/hostile/requested-final-authorization-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-production-claim.json b/examples/review/hostile/requested-production-claim.json index d8cc69a..ade4495 100644 --- a/examples/review/hostile/requested-production-claim.json +++ b/examples/review/hostile/requested-production-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/requested-socaas-claim.json b/examples/review/hostile/requested-socaas-claim.json index 9aa0bc9..ac3ca5b 100644 --- a/examples/review/hostile/requested-socaas-claim.json +++ b/examples/review/hostile/requested-socaas-claim.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/hostile/unsafe-public-safe-status.json b/examples/review/hostile/unsafe-public-safe-status.json index be57c15..fe3417e 100644 --- a/examples/review/hostile/unsafe-public-safe-status.json +++ b/examples/review/hostile/unsafe-public-safe-status.json @@ -3,7 +3,7 @@ "artifact_id": "HO-DET-010", "artifact_name": "Windows local Administrators group membership change detection", "artifact_type": "detection-review-artifact", - "artifact_family": "synthetic-review-only", + "artifact_family": "controlled-test-review-only", "source_owner": "hawkinsoperations-detections", "validation_owner": "hawkinsoperations-validation", "platform_owner": "hawkinsoperations-platform", @@ -53,7 +53,7 @@ ], "allowed_claim_class": "controlled local fixture review only", "requested_claims": [ - "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe synthetic fixtures only." + "HO-DET-010 can be reviewed locally through deterministic Hoxline Review Engine v1 using public-safe controlled-test fixtures only." ], "blocked_claim_classes": [ "production ready", @@ -78,6 +78,6 @@ "lifetime_ledger_changed": false, "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signal only; not public signal observation.", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", "next_gate": "human_review_gate" } diff --git a/examples/review/id-det-001-artifact-manifest-v1.json b/examples/review/id-det-001-artifact-manifest-v1.json new file mode 100644 index 0000000..429b784 --- /dev/null +++ b/examples/review/id-det-001-artifact-manifest-v1.json @@ -0,0 +1,78 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "controlled local fixture review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "ID-DET-001", + "artifact_name": "Identity session context fixture review", + "artifact_type": "detection-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "confidence": "bounded-fixture-only", + "detection_family": "id-det-001", + "endpoint_mutation": false, + "expected_event_ids": [], + "expected_event_keys": [ + "impossible_travel" + ], + "expected_rule_ids": [], + "field_mapping": { + "event_key": "fixture selector", + "channel": "fixture source label", + "action": "bounded fixture action", + "actor": "controlled-test actor label" + }, + "fixture_paths": { + "negative": "examples/review/fixtures/id-det-001-safe-negative-fixture.json", + "positive": "examples/review/fixtures/id-det-001-safe-fixture.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "human_review_gate", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "ID-DET-001 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." + ], + "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", + "runtime_proof": false, + "severity": "medium", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [], + "event_keys": [ + "impossible_travel" + ], + "event_key_field": "event_key", + "required_fields": [ + "event_key", + "channel", + "action", + "actor" + ], + "scope": "fixture-only metadata contract", + "source": "Identity controlled fixture metadata", + "source_control_note": "Source and controlled-validation metadata remain owned by their repositories; this manifest carries sanitized fixture metadata only.", + "wazuh_rule_ids": [] + }, + "triage_what_happened": "A controlled-test fixture represented the ID-DET-001 controlled review pattern.", + "triage_why_it_matters": "The bounded pattern is useful for deterministic reviewer reproduction when kept separate from runtime and proof claims.", + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/id-det-002-artifact-manifest-v1.json b/examples/review/id-det-002-artifact-manifest-v1.json new file mode 100644 index 0000000..da2d8b2 --- /dev/null +++ b/examples/review/id-det-002-artifact-manifest-v1.json @@ -0,0 +1,78 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "controlled local fixture review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "ID-DET-002", + "artifact_name": "Identity authentication control fixture review", + "artifact_type": "detection-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "confidence": "bounded-fixture-only", + "detection_family": "id-det-002", + "endpoint_mutation": false, + "expected_event_ids": [], + "expected_event_keys": [ + "mfa_push_fatigue_volume" + ], + "expected_rule_ids": [], + "field_mapping": { + "event_key": "fixture selector", + "channel": "fixture source label", + "action": "bounded fixture action", + "actor": "controlled-test actor label" + }, + "fixture_paths": { + "negative": "examples/review/fixtures/id-det-002-safe-negative-fixture.json", + "positive": "examples/review/fixtures/id-det-002-safe-fixture.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "human_review_gate", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "ID-DET-002 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." + ], + "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", + "runtime_proof": false, + "severity": "medium", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [], + "event_keys": [ + "mfa_push_fatigue_volume" + ], + "event_key_field": "event_key", + "required_fields": [ + "event_key", + "channel", + "action", + "actor" + ], + "scope": "fixture-only metadata contract", + "source": "Identity controlled fixture metadata", + "source_control_note": "Source and controlled-validation metadata remain owned by their repositories; this manifest carries sanitized fixture metadata only.", + "wazuh_rule_ids": [] + }, + "triage_what_happened": "A controlled-test fixture represented the ID-DET-002 controlled review pattern.", + "triage_why_it_matters": "The bounded pattern is useful for deterministic reviewer reproduction when kept separate from runtime and proof claims.", + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/id-det-003-artifact-manifest-v1.json b/examples/review/id-det-003-artifact-manifest-v1.json new file mode 100644 index 0000000..2473385 --- /dev/null +++ b/examples/review/id-det-003-artifact-manifest-v1.json @@ -0,0 +1,78 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "controlled local fixture review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "ID-DET-003", + "artifact_name": "Identity privilege change fixture review", + "artifact_type": "detection-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "confidence": "bounded-fixture-only", + "detection_family": "id-det-003", + "endpoint_mutation": false, + "expected_event_ids": [], + "expected_event_keys": [ + "privileged_role_assignment" + ], + "expected_rule_ids": [], + "field_mapping": { + "event_key": "fixture selector", + "channel": "fixture source label", + "action": "bounded fixture action", + "actor": "controlled-test actor label" + }, + "fixture_paths": { + "negative": "examples/review/fixtures/id-det-003-safe-negative-fixture.json", + "positive": "examples/review/fixtures/id-det-003-safe-fixture.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "human_review_gate", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "ID-DET-003 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." + ], + "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", + "runtime_proof": false, + "severity": "medium", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [], + "event_keys": [ + "privileged_role_assignment" + ], + "event_key_field": "event_key", + "required_fields": [ + "event_key", + "channel", + "action", + "actor" + ], + "scope": "fixture-only metadata contract", + "source": "Identity controlled fixture metadata", + "source_control_note": "Source and controlled-validation metadata remain owned by their repositories; this manifest carries sanitized fixture metadata only.", + "wazuh_rule_ids": [] + }, + "triage_what_happened": "A controlled-test fixture represented the ID-DET-003 controlled review pattern.", + "triage_why_it_matters": "The bounded pattern is useful for deterministic reviewer reproduction when kept separate from runtime and proof claims.", + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/id-det-004-artifact-manifest-v1.json b/examples/review/id-det-004-artifact-manifest-v1.json new file mode 100644 index 0000000..d890ef0 --- /dev/null +++ b/examples/review/id-det-004-artifact-manifest-v1.json @@ -0,0 +1,78 @@ +{ + "ai_disposition_authority": false, + "allowed_claim_class": "controlled local fixture review only", + "artifact_family": "controlled-test-review-only", + "artifact_id": "ID-DET-004", + "artifact_name": "Identity travel context fixture review", + "artifact_type": "detection-review-artifact", + "blocked_claim_classes": [ + "production ready", + "public-safe runtime proof", + "SOCaaS deployed", + "customer deployed", + "autonomous SOC", + "AI-approved disposition", + "analyst-approved disposition", + "final authorization", + "case closure", + "website rendering as proof", + "green CI as approval" + ], + "confidence": "bounded-fixture-only", + "detection_family": "id-det-004", + "endpoint_mutation": false, + "expected_event_ids": [], + "expected_event_keys": [ + "impossible_travel" + ], + "expected_rule_ids": [], + "field_mapping": { + "event_key": "fixture selector", + "channel": "fixture source label", + "action": "bounded fixture action", + "actor": "controlled-test actor label" + }, + "fixture_paths": { + "negative": "examples/review/fixtures/id-det-004-safe-negative-fixture.json", + "positive": "examples/review/fixtures/id-det-004-safe-fixture.json" + }, + "human_review_required": true, + "lifetime_ledger_changed": false, + "manifest_version": "artifact-manifest-v1", + "next_gate": "human_review_gate", + "platform_owner": "hawkinsoperations-platform", + "product_owner": "hoxline", + "proof_boundary": "Fixture-only local review output; not public proof and not proof authority.", + "proof_owner": "hawkinsoperations-proof", + "public_proof_promoted": false, + "public_safe_status": "NOT_PUBLIC_SAFE", + "requested_claims": [ + "ID-DET-004 can be reviewed locally through deterministic Hoxline Review Engine v1 using bundled controlled-test fixtures only." + ], + "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", + "runtime_proof": false, + "severity": "medium", + "signal_boundary": "Controlled-test fixture signal only; not public signal observation.", + "source_owner": "hawkinsoperations-detections", + "telemetry_contract": { + "event_ids": [], + "event_keys": [ + "impossible_travel" + ], + "event_key_field": "event_key", + "required_fields": [ + "event_key", + "channel", + "action", + "actor" + ], + "scope": "fixture-only metadata contract", + "source": "Identity controlled fixture metadata", + "source_control_note": "Source and controlled-validation metadata remain owned by their repositories; this manifest carries sanitized fixture metadata only.", + "wazuh_rule_ids": [] + }, + "triage_what_happened": "A controlled-test fixture represented the ID-DET-004 controlled review pattern.", + "triage_why_it_matters": "The bounded pattern is useful for deterministic reviewer reproduction when kept separate from runtime and proof claims.", + "validation_owner": "hawkinsoperations-validation", + "wazuh_mutation": false +} diff --git a/examples/review/multi-artifact-review-index-v1.json b/examples/review/multi-artifact-review-index-v1.json index 9d66cc9..00b007d 100644 --- a/examples/review/multi-artifact-review-index-v1.json +++ b/examples/review/multi-artifact-review-index-v1.json @@ -16,16 +16,52 @@ { "artifact_id": "HO-DET-012", "manifest_path": "examples/review/ho-det-012-artifact-manifest-v1.json" + }, + { + "artifact_id": "HO-DET-013", + "manifest_path": "examples/review/ho-det-013-artifact-manifest-v1.json" + }, + { + "artifact_id": "AWS-DET-001", + "manifest_path": "examples/review/aws-det-001-artifact-manifest-v1.json" + }, + { + "artifact_id": "ID-DET-001", + "manifest_path": "examples/review/id-det-001-artifact-manifest-v1.json" + }, + { + "artifact_id": "ID-DET-002", + "manifest_path": "examples/review/id-det-002-artifact-manifest-v1.json" + }, + { + "artifact_id": "ID-DET-003", + "manifest_path": "examples/review/id-det-003-artifact-manifest-v1.json" + }, + { + "artifact_id": "ID-DET-004", + "manifest_path": "examples/review/id-det-004-artifact-manifest-v1.json" + }, + { + "artifact_id": "HO-NDR-001", + "manifest_path": "examples/review/ho-ndr-001-artifact-manifest-v1.json" } ], "batch_claim_boundary": "Local fixture review set only; not runtime proof and not proof authority.", - "description": "Governed local fixture review set for HO-DET-009, HO-DET-010, HO-DET-011, and HO-DET-012.", - "expected_blocked_artifacts": [], + "description": "Governed local cross-domain fixture review set with an explicit boundary-contract BLOCKED outcome for HO-NDR-001.", + "expected_blocked_artifacts": [ + "HO-NDR-001" + ], "expected_pass_artifacts": [ "HO-DET-009", "HO-DET-010", "HO-DET-011", - "HO-DET-012" + "HO-DET-012", + "HO-DET-013", + "AWS-DET-001", + "ID-DET-001", + "ID-DET-002", + "ID-DET-003", + "ID-DET-004" ], "generated_outputs": [ "batch-machine-state.json", @@ -40,5 +76,5 @@ "proof_boundary": "Batch output is local reviewer evidence of engine behavior only; it is not public proof.", "public_safe_status": "NOT_PUBLIC_SAFE", "runtime_boundary": "No runtime execution, endpoint mutation, VM, Wazuh, Splunk, Cribl, private infrastructure, or live signal action.", - "signal_boundary": "Synthetic fixture signals only; not public signal observation." + "signal_boundary": "Controlled-test fixture signals only; not public signal observation." } diff --git a/schemas/artifact-manifest-v1.schema.json b/schemas/artifact-manifest-v1.schema.json index 7c0ca40..15eddc2 100644 --- a/schemas/artifact-manifest-v1.schema.json +++ b/schemas/artifact-manifest-v1.schema.json @@ -3,33 +3,141 @@ "$id": "https://hawkinsoperations.example/schemas/artifact-manifest-v1.schema.json", "title": "Hoxline Artifact Manifest v1", "type": "object", - "required": ["manifest_version", "artifact_id", "artifact_name", "artifact_type", "artifact_family", "source_owner", "validation_owner", "platform_owner", "proof_owner", "product_owner", "telemetry_contract", "fixture_paths", "expected_event_ids", "expected_rule_ids", "allowed_claim_class", "requested_claims", "blocked_claim_classes", "public_safe_status", "human_review_required", "ai_disposition_authority", "proof_boundary", "runtime_boundary", "signal_boundary", "next_gate"], + "additionalProperties": false, + "required": [ + "manifest_version", + "artifact_id", + "artifact_name", + "artifact_type", + "artifact_family", + "source_owner", + "validation_owner", + "platform_owner", + "proof_owner", + "product_owner", + "telemetry_contract", + "fixture_paths", + "expected_event_ids", + "expected_rule_ids", + "allowed_claim_class", + "requested_claims", + "blocked_claim_classes", + "public_safe_status", + "human_review_required", + "ai_disposition_authority", + "proof_boundary", + "runtime_boundary", + "signal_boundary", + "next_gate" + ], "properties": { "manifest_version": { "const": "artifact-manifest-v1" }, - "artifact_id": { "type": "string" }, - "artifact_name": { "type": "string" }, - "artifact_type": { "type": "string" }, - "artifact_family": { "type": "string" }, + "artifact_id": { + "type": "string", + "pattern": "^(?:HO-DET|HO-NDR|ID-DET|AWS-DET)-[0-9]{3}$" + }, + "artifact_name": { "type": "string", "minLength": 1 }, + "artifact_type": { "const": "detection-review-artifact" }, + "artifact_family": { "const": "controlled-test-review-only" }, + "source_owner": { "const": "hawkinsoperations-detections" }, + "validation_owner": { "const": "hawkinsoperations-validation" }, + "platform_owner": { "const": "hawkinsoperations-platform" }, + "proof_owner": { "const": "hawkinsoperations-proof" }, + "product_owner": { "const": "hoxline" }, "telemetry_contract": { "type": "object", - "required": ["source", "event_ids", "wazuh_rule_ids"], + "additionalProperties": false, + "required": ["source", "event_ids", "wazuh_rule_ids", "required_fields", "scope"], "properties": { - "source": { "type": "string" }, - "event_ids": { "type": "array", "items": { "type": "integer" } }, - "wazuh_rule_ids": { "type": "array", "items": { "type": "integer" } } + "source": { "type": "string", "minLength": 1 }, + "event_ids": { + "type": "array", + "uniqueItems": true, + "items": { "type": "integer", "minimum": 0 } + }, + "event_keys": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "event_key_field": { "type": "string", "minLength": 1 }, + "wazuh_rule_ids": { + "type": "array", + "uniqueItems": true, + "items": { "type": "integer", "minimum": 0 } + }, + "required_fields": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "scope": { "type": "string", "minLength": 1 }, + "source_control_note": { "type": "string", "minLength": 1 } } }, "fixture_paths": { "type": "object", + "additionalProperties": false, "required": ["positive", "negative"], "properties": { - "positive": { "type": "string" }, - "negative": { "type": "string" } + "positive": { "type": "string", "pattern": "^examples/" }, + "negative": { "type": "string", "pattern": "^examples/" } } }, + "expected_event_ids": { + "type": "array", + "uniqueItems": true, + "items": { "type": "integer", "minimum": 0 } + }, + "expected_event_keys": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "expected_rule_ids": { + "type": "array", + "uniqueItems": true, + "items": { "type": "integer", "minimum": 0 } + }, + "allowed_claim_class": { "type": "string", "minLength": 1 }, + "requested_claims": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "blocked_claim_classes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, "public_safe_status": { "const": "NOT_PUBLIC_SAFE" }, "human_review_required": { "const": true }, - "ai_disposition_authority": { "const": false } - }, - "additionalProperties": true + "ai_disposition_authority": { "const": false }, + "endpoint_mutation": { "const": false }, + "wazuh_mutation": { "const": false }, + "runtime_proof": { "const": false }, + "public_proof_promoted": { "const": false }, + "lifetime_ledger_changed": { "const": false }, + "proof_boundary": { "type": "string", "minLength": 1 }, + "runtime_boundary": { "type": "string", "minLength": 1 }, + "signal_boundary": { "type": "string", "minLength": 1 }, + "next_gate": { "type": "string", "minLength": 1 }, + "expected_review_outcome": { "const": "BLOCKED" }, + "expected_block_reason": { "type": "string", "minLength": 1 }, + "additional_telemetry_sources": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "attack_mapping": { "type": "object" }, + "confidence": { "type": "string" }, + "detection_family": { "type": "string" }, + "field_mapping": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "severity": { "type": "string" }, + "triage_what_happened": { "type": "string" }, + "triage_why_it_matters": { "type": "string" } + } } diff --git a/schemas/case-growth-index-v0.schema.json b/schemas/case-growth-index-v0.schema.json index ca9154e..5548d76 100644 --- a/schemas/case-growth-index-v0.schema.json +++ b/schemas/case-growth-index-v0.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://hawkinsoperations.example/schemas/case-growth-index-v0.schema.json", - "title": "Hoxline Case Growth Index v0", + "title": "Hoxline Case Growth Index v1", "type": "object", "additionalProperties": false, "required": [ @@ -9,6 +9,15 @@ "generated_at", "repo_root", "proof_ceiling", + "historical_snapshot", + "current_authority", + "snapshot_state", + "source_revisions", + "contradictions", + "drift", + "next_legal_action", + "source_manifest_digest", + "reproducibility_sha256", "repos_scanned", "repo_slot_accuracy", "source_files_scanned_count", @@ -20,10 +29,66 @@ "boundary" ], "properties": { - "schema_version": { "const": "case-growth-index-v0" }, + "schema_version": { "const": "case-growth-index-v1" }, "generated_at": { "type": "string", "minLength": 1 }, "repo_root": { "type": "string", "minLength": 1 }, "proof_ceiling": { "const": "CASE_GROWTH_INDEX_CONTROLLED_REPO_AGGREGATION_ONLY" }, + "historical_snapshot": { "type": "boolean" }, + "current_authority": { "type": "boolean" }, + "snapshot_state": { "type": "object" }, + "source_revisions": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "items": { + "type": "object", + "required": [ + "repository", + "authority_role", + "resolved_ref", + "source_commit_sha", + "source_observed_head_sha", + "current_observed_head_sha", + "source_observation_kind", + "source_path", + "authoritative_path", + "authoritative_git_blob_sha", + "authoritative_content_fingerprint", + "source_file_sha256", + "source_freshness_state", + "snapshot_freshness_state", + "historical_snapshot", + "current_authority", + "missing_source_state", + "dangling_reference_state", + "contradictions", + "drift", + "next_legal_action" + ], + "properties": { + "repository": { "type": "string" }, + "source_commit_sha": { "type": "string" }, + "source_observed_head_sha": { "type": "string" }, + "current_observed_head_sha": { "type": "string" }, + "source_observation_kind": { "const": "reviewed_immutable_commit" }, + "source_path": { "type": "string" }, + "authoritative_path": { "type": "string" }, + "authoritative_git_blob_sha": { "type": ["string", "null"] }, + "authoritative_content_fingerprint": { "type": ["string", "null"] }, + "source_file_sha256": { "type": ["string", "null"] }, + "historical_snapshot": { "type": "boolean" }, + "current_authority": { "type": "boolean" }, + "missing_source_state": { "type": "boolean" }, + "dangling_reference_state": { "type": "boolean" } + }, + "additionalProperties": true + } + }, + "contradictions": { "type": "array", "items": { "type": "object" } }, + "drift": { "type": "array", "items": { "type": "object" } }, + "next_legal_action": { "type": "string", "minLength": 1 }, + "source_manifest_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "reproducibility_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "repos_scanned": { "type": "array", "items": { "type": "object" } @@ -195,6 +260,17 @@ } } }, + "allOf": [ + { + "not": { + "required": ["historical_snapshot", "current_authority"], + "properties": { + "historical_snapshot": { "const": true }, + "current_authority": { "const": true } + } + } + } + ], "$defs": { "string_array": { "type": "array", diff --git a/schemas/multi-artifact-review-index-v1.schema.json b/schemas/multi-artifact-review-index-v1.schema.json index df5ceb4..79ede0c 100644 --- a/schemas/multi-artifact-review-index-v1.schema.json +++ b/schemas/multi-artifact-review-index-v1.schema.json @@ -1,54 +1,9 @@ { "$id": "https://hawkinsoperations.example/schemas/multi-artifact-review-index-v1.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "additionalProperties": true, - "properties": { - "ai_disposition_authority": { - "const": false - }, - "artifacts": { - "items": { - "properties": { - "artifact_id": { - "type": "string" - }, - "manifest_path": { - "type": "string" - } - }, - "required": [ - "artifact_id", - "manifest_path" - ], - "type": "object" - }, - "type": "array" - }, - "expected_blocked_artifacts": { - "items": { - "type": "string" - }, - "type": "array" - }, - "expected_pass_artifacts": { - "items": { - "type": "string" - }, - "type": "array" - }, - "human_review_required": { - "const": true - }, - "index_id": { - "type": "string" - }, - "index_version": { - "const": "multi-artifact-review-index-v1" - }, - "public_safe_status": { - "const": "NOT_PUBLIC_SAFE" - } - }, + "title": "Hoxline Multi-Artifact Review Index v1", + "type": "object", + "additionalProperties": false, "required": [ "index_version", "index_id", @@ -66,6 +21,54 @@ "generated_outputs", "next_gate" ], - "title": "Hoxline Multi-Artifact Review Index v1", - "type": "object" + "properties": { + "index_version": { "const": "multi-artifact-review-index-v1" }, + "index_id": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["artifact_id", "manifest_path"], + "properties": { + "artifact_id": { + "type": "string", + "pattern": "^(?:HO-DET|HO-NDR|ID-DET|AWS-DET)-[0-9]{3}$" + }, + "manifest_path": { + "type": "string", + "pattern": "^examples/review/[a-z0-9-]+-artifact-manifest-v1[.]json$" + } + } + } + }, + "expected_pass_artifacts": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string" } + }, + "expected_blocked_artifacts": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string" } + }, + "batch_claim_boundary": { "type": "string", "minLength": 1 }, + "public_safe_status": { "const": "NOT_PUBLIC_SAFE" }, + "human_review_required": { "const": true }, + "ai_disposition_authority": { "const": false }, + "runtime_boundary": { "type": "string", "minLength": 1 }, + "signal_boundary": { "type": "string", "minLength": 1 }, + "proof_boundary": { "type": "string", "minLength": 1 }, + "generated_outputs": { + "const": [ + "batch-machine-state.json", + "batch-summary.md", + "batch-reviewer-pack.md", + "batch-run-summary.json" + ] + }, + "next_gate": { "type": "string", "minLength": 1 } + } } diff --git a/schemas/review-machine-state-v1.schema.json b/schemas/review-machine-state-v1.schema.json index e59a848..4c82f35 100644 --- a/schemas/review-machine-state-v1.schema.json +++ b/schemas/review-machine-state-v1.schema.json @@ -3,18 +3,88 @@ "$id": "https://hawkinsoperations.example/schemas/review-machine-state-v1.schema.json", "title": "Hoxline Review Machine State v1", "type": "object", - "required": ["schema_version", "engine_version", "run_id", "artifact_id", "manifest_path", "stages", "outputs", "final_status", "requested_claims", "blocked_claims", "proof_boundary", "runtime_boundary", "signal_boundary", "public_safe_status", "human_review_required", "ai_disposition_authority", "endpoint_mutation", "wazuh_mutation", "private_evidence_committed", "public_proof_promoted", "lifetime_ledger_changed", "next_gate", "created_at"], + "additionalProperties": false, + "required": [ + "schema_version", + "engine_version", + "run_id", + "artifact_id", + "manifest_path", + "stages", + "outputs", + "output_digests", + "state_integrity_digest", + "final_status", + "block_reason", + "allowed_claim", + "requested_claims", + "blocked_claims", + "proof_boundary", + "runtime_boundary", + "signal_boundary", + "public_safe_status", + "human_review_required", + "ai_disposition_authority", + "endpoint_mutation", + "wazuh_mutation", + "runtime_proof", + "private_evidence_committed", + "public_proof_promoted", + "lifetime_ledger_changed", + "next_gate", + "created_at", + "product" + ], "properties": { "schema_version": { "const": "review-machine-state-v1" }, "engine_version": { "const": "hoxline-review-engine-v1" }, + "run_id": { "type": "string" }, + "artifact_id": { "type": "string" }, + "manifest_path": { "type": "string" }, + "stages": { "type": "array", "minItems": 11, "maxItems": 11 }, + "outputs": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "output_digests": { + "type": "object", + "additionalProperties": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + }, + "state_integrity_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "input_digests": { + "type": "object", + "additionalProperties": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + }, + "source_manifest_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "authority_binding": { "type": "object" }, "final_status": { "enum": ["PASS", "BLOCKED"] }, + "block_reason": { "type": ["string", "null"] }, + "allowed_claim": { "type": ["string", "null"] }, + "requested_claims": { "type": "array" }, + "blocked_claims": { "type": "array" }, + "proof_boundary": { "type": "string" }, + "runtime_boundary": { "type": "string" }, + "signal_boundary": { "type": "string" }, "public_safe_status": { "const": "NOT_PUBLIC_SAFE" }, "human_review_required": { "const": true }, "ai_disposition_authority": { "const": false }, "endpoint_mutation": { "const": false }, "wazuh_mutation": { "const": false }, + "runtime_proof": { "const": false }, + "private_evidence_committed": { "const": false }, "public_proof_promoted": { "const": false }, - "lifetime_ledger_changed": { "const": false } + "lifetime_ledger_changed": { "const": false }, + "next_gate": { "type": "string" }, + "created_at": { "type": "string" }, + "product": { "type": "string" } }, - "additionalProperties": true + "allOf": [ + { + "if": { "properties": { "final_status": { "const": "PASS" } } }, + "then": { + "required": ["input_digests", "source_manifest_digest", "authority_binding"], + "properties": { "block_reason": { "type": "null" } } + } + } + ] } diff --git a/src/claimfirewall/claim_authority/evaluator.py b/src/claimfirewall/claim_authority/evaluator.py index 590b71d..ca16b63 100644 --- a/src/claimfirewall/claim_authority/evaluator.py +++ b/src/claimfirewall/claim_authority/evaluator.py @@ -167,13 +167,22 @@ def _iter_scan_files(paths: Iterable[str | Path], exclude_patterns: Iterable[str for raw_path in paths: path = Path(raw_path) if path.is_file(): - if _is_supported(path) and not _is_excluded(path, excludes): + if _is_supported(path) and not _is_excluded( + path, + excludes, + path.parent, + ): yield path continue if path.is_dir(): + scan_root = path.resolve() for child in sorted(path.rglob("*")): - if child.is_file() and _is_supported(child) and not _is_excluded(child, excludes): + if child.is_file() and _is_supported(child) and not _is_excluded( + child, + excludes, + scan_root, + ): yield child continue @@ -184,10 +193,14 @@ def _is_supported(path: Path) -> bool: return path.suffix.lower() in SUPPORTED_EXTENSIONS -def _is_excluded(path: Path, exclude_patterns: Iterable[str]) -> bool: +def _is_excluded( + path: Path, + exclude_patterns: Iterable[str], + scan_root: Path, +) -> bool: normalized = path.as_posix() try: - relative = path.resolve().relative_to(Path.cwd().resolve()).as_posix() + relative = path.resolve().relative_to(scan_root.resolve()).as_posix() except ValueError: relative = normalized name = path.name diff --git a/src/claimfirewall/scanner.py b/src/claimfirewall/scanner.py index c8ffceb..ab3a9ce 100644 --- a/src/claimfirewall/scanner.py +++ b/src/claimfirewall/scanner.py @@ -76,13 +76,22 @@ def _iter_scan_files(paths: Iterable[str | Path], exclude_patterns: Iterable[str for raw_path in paths: path = Path(raw_path) if path.is_file(): - if _is_supported(path) and not _is_excluded(path, excludes): + if _is_supported(path) and not _is_excluded( + path, + excludes, + path.parent, + ): yield path continue if path.is_dir(): + scan_root = path.resolve() for child in sorted(path.rglob("*")): - if child.is_file() and _is_supported(child) and not _is_excluded(child, excludes): + if child.is_file() and _is_supported(child) and not _is_excluded( + child, + excludes, + scan_root, + ): yield child continue @@ -93,10 +102,14 @@ def _is_supported(path: Path) -> bool: return path.suffix.lower() in SUPPORTED_EXTENSIONS -def _is_excluded(path: Path, exclude_patterns: Iterable[str]) -> bool: +def _is_excluded( + path: Path, + exclude_patterns: Iterable[str], + scan_root: Path, +) -> bool: normalized = path.as_posix() try: - relative = path.resolve().relative_to(Path.cwd().resolve()).as_posix() + relative = path.resolve().relative_to(scan_root.resolve()).as_posix() except ValueError: relative = normalized name = path.name diff --git a/src/hoxline/case_growth/__init__.py b/src/hoxline/case_growth/__init__.py index 037d5c4..ba80ebb 100644 --- a/src/hoxline/case_growth/__init__.py +++ b/src/hoxline/case_growth/__init__.py @@ -1,6 +1,11 @@ from __future__ import annotations -from .collector import build_case_growth_index +from .collector import build_case_growth_index, diff_case_growth_snapshot, verify_case_growth_snapshot from .render import render_case_growth_markdown -__all__ = ["build_case_growth_index", "render_case_growth_markdown"] +__all__ = [ + "build_case_growth_index", + "diff_case_growth_snapshot", + "render_case_growth_markdown", + "verify_case_growth_snapshot", +] diff --git a/src/hoxline/case_growth/collector.py b/src/hoxline/case_growth/collector.py index 9b39a5e..90c809e 100644 --- a/src/hoxline/case_growth/collector.py +++ b/src/hoxline/case_growth/collector.py @@ -2,25 +2,55 @@ from copy import deepcopy from datetime import datetime, timezone +import hashlib +import json +import os from pathlib import Path import re +import subprocess from typing import Any from .discovery import ( REPO_NAMES, case_growth_files, discover_case_ids, + file_sha256, + git_blob_identity, + git_commit_exists, last_git_update, load_structured, repo_branch, repo_dirty, + repo_dirty_paths, + repo_head_sha, + repo_origin, repo_relative, resolve_repo_paths, + sanitized_git_env, + semantic_fingerprint, ) PROOF_CEILING = "CASE_GROWTH_INDEX_CONTROLLED_REPO_AGGREGATION_ONLY" +AUTHORITY_SOURCES = { + ".github": ("org command-center routing", "governance/COMMAND_CENTER_INVARIANTS.json"), + "hawkinsoperations-detections": ("detection source truth", "detections/DETECTION_PROMOTION_MATRIX.yml"), + "hawkinsoperations-validation": ("controlled validation truth", "validation/VALIDATION_REGISTRY.yml"), + "hawkinsoperations-platform": ("platform contract truth", "contracts/public-status-source-contract-v1.json"), + "hawkinsoperations-proof": ("proof and claim-boundary truth", "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml"), + "hawkinsoperations-website": ("rendering-only public status contract", "schemas/public-status-v0.schema.json"), + "hoxline": ("case-growth and fixture-review product truth", "src/hoxline/case_growth/collector.py"), +} + +CANONICAL_ORIGINS = { + repository: f"github.com/HawkinsOperations/{repository}".casefold() + for repository in REPO_NAMES +} + +CONVERGENCE_SOURCE_MANIFEST = Path(".github/governance/CONVERGENCE_SOURCE_MANIFEST.json") +CONVERGENCE_SOURCE_MANIFEST_SCHEMA = "hawkinsoperations-convergence-source-manifest-v1" + BOUNDARY = { "runtime_public_proof_claimed": False, "signal_public_proof_claimed": False, @@ -85,7 +115,7 @@ def build_case_growth_index(repo_root: Path, generated_at: str | None = None) -> repos_scanned.append( { "repo": name, - "path": str(path) if path is not None else "NOT_FOUND", + "path": name if path is not None else "NOT_FOUND", "exists": path is not None, "branch": repo_branch(path) if path is not None else "NOT_FOUND", "dirty": repo_dirty(path) if path is not None else None, @@ -116,11 +146,60 @@ def build_case_growth_index(repo_root: Path, generated_at: str | None = None) -> case_growth_health = _build_case_growth_health(summary) _add_cross_repo_quality_notes(ordered_rows, data_quality_notes) - return { - "schema_version": "case-growth-index-v0", + source_selections: dict[str, dict[str, Any]] | None = None + selection_manifest = ( + repo_root / ".github" / "governance" / "CONVERGENCE_SOURCE_MANIFEST.json" + ) + if selection_manifest.is_file(): + source_selections, selection_errors = _load_convergence_source_selections( + repo_root + ) + if selection_errors: + raise ValueError( + "invalid seven-source selection manifest: " + + "; ".join(selection_errors) + ) + checkout_errors: list[str] = [] + for repository in REPO_NAMES: + checkout_errors.extend( + _verify_selected_source_checkout( + repo_root, + repository, + source_selections, + ) + ) + if checkout_errors: + raise ValueError( + "selected source checkout verification failed: " + + "; ".join(checkout_errors) + ) + + source_revisions = _build_source_revisions(repo_paths, source_selections) + contradictions, drift = _source_convergence_findings(repo_paths, source_revisions, summary, ordered_rows) + global_current_authority = ( + not contradictions + and not drift + and all(item.get("current_authority") is True for item in source_revisions) + ) + result = { + "schema_version": "case-growth-index-v1", "generated_at": generated, - "repo_root": str(repo_root), + "repo_root": "HawkinsOperations", "proof_ceiling": PROOF_CEILING, + "historical_snapshot": False, + "current_authority": global_current_authority, + "snapshot_state": { + "freshness": "CURRENT" if global_current_authority else "BLOCKED", + "historical_snapshot": False, + "current_authority": global_current_authority, + "identity_model": "repo_path_git_blob_and_semantic_fingerprint_with_separate_head_observation", + "generated_consumers_are_authority": False, + }, + "source_revisions": source_revisions, + "source_manifest_digest": _source_manifest_digest(source_revisions), + "contradictions": contradictions, + "drift": drift, + "next_legal_action": _next_legal_action(source_revisions, contradictions, drift), "repos_scanned": repos_scanned, "repo_slot_accuracy": repo_slot_accuracy, "source_files_scanned_count": scanned_count, @@ -131,6 +210,8 @@ def build_case_growth_index(repo_root: Path, generated_at: str | None = None) -> "data_quality_notes": data_quality_notes, "boundary": deepcopy(BOUNDARY), } + result["reproducibility_sha256"] = _reproducibility_hash(result) + return result def _repo_boundary(repo_name: str) -> str: @@ -145,6 +226,611 @@ def _repo_boundary(repo_name: str) -> str: }[repo_name] +def _normalized_origin(value: str) -> str: + origin = value.strip().replace("\\", "/") + origin = re.sub(r"^git@", "", origin) + if origin.startswith("github.com:"): + origin = origin.replace(":", "/", 1) + origin = re.sub(r"^(?:https?|ssh)://", "", origin, flags=re.IGNORECASE) + return origin.removesuffix(".git").rstrip("/").casefold() + + +def _git_output(repo: Path, *args: str) -> str | None: + try: + result = subprocess.run( + ["git", "-C", str(repo), *args], + check=False, + capture_output=True, + text=True, + env=sanitized_git_env(), + ) + except OSError: + return None + return result.stdout.strip() if result.returncode == 0 else None + + +def _is_ancestor(repo: Path, ancestor: str, descendant: str) -> bool: + try: + result = subprocess.run( + ["git", "-C", str(repo), "merge-base", "--is-ancestor", ancestor, descendant], + check=False, + capture_output=True, + text=True, + env=sanitized_git_env(), + ) + except OSError: + return False + return result.returncode == 0 + + +def _load_convergence_source_selections(repo_root: Path) -> tuple[dict[str, dict[str, Any]], list[str]]: + root = Path(repo_root).resolve() + manifest_path = root / CONVERGENCE_SOURCE_MANIFEST + if not manifest_path.is_file(): + return {}, [f"missing explicit seven-source selection manifest: {CONVERGENCE_SOURCE_MANIFEST.as_posix()}"] + command_center = root / ".github" + manifest_relative = "governance/CONVERGENCE_SOURCE_MANIFEST.json" + if _normalized_origin(repo_origin(command_center)) != CANONICAL_ORIGINS[".github"]: + return {}, ["seven-source selection manifest owner origin is not canonical"] + tracked_path = _git_output(command_center, "ls-files", "--error-unmatch", "--", manifest_relative) + committed_blob = _git_output(command_center, "rev-parse", f"HEAD:{manifest_relative}") + worktree_blob = _git_output(command_center, "hash-object", "--", manifest_relative) + if tracked_path != manifest_relative or committed_blob is None or worktree_blob != committed_blob: + return {}, ["seven-source selection manifest must be tracked and clean at the checked command-center head"] + try: + manifest = load_structured(manifest_path) + except (OSError, ValueError, json.JSONDecodeError) as exc: + return {}, [f"seven-source selection manifest is not strict valid JSON: {exc}"] + if not isinstance(manifest, dict): + return {}, ["seven-source selection manifest must be an object"] + errors: list[str] = [] + unknown_top_level = sorted(set(manifest) - {"schema", "manifest_id", "repositories", "constraints"}) + if unknown_top_level: + errors.append(f"seven-source selection manifest contains unsupported fields: {unknown_top_level}") + if manifest.get("schema") != CONVERGENCE_SOURCE_MANIFEST_SCHEMA: + errors.append(f"seven-source selection manifest schema must be {CONVERGENCE_SOURCE_MANIFEST_SCHEMA}") + entries = manifest.get("repositories") + if not isinstance(entries, list): + return {}, [*errors, "seven-source selection manifest repositories must be a list"] + selections: dict[str, dict[str, Any]] = {} + for entry in entries: + if not isinstance(entry, dict): + errors.append("seven-source selection manifest entries must be objects") + continue + repository = entry.get("repository") + if not isinstance(repository, str) or repository not in REPO_NAMES: + errors.append(f"seven-source selection manifest has unknown repository {repository!r}") + continue + if repository in selections: + errors.append(f"seven-source selection manifest duplicates repository {repository}") + continue + expected_canonical = f"HawkinsOperations/{repository}" + if entry.get("canonical_repository") != expected_canonical: + errors.append(f"{repository}: selected canonical repository must be {expected_canonical}") + if repository == ".github": + unknown = sorted( + set(entry) + - { + "repository", + "canonical_repository", + "revision_source", + "authority_content_revision", + "tree_source", + } + ) + if unknown: + errors.append(f".github: selection contains unsupported fields: {unknown}") + if entry.get("revision_source") != "github_event_sha" or entry.get("tree_source") != "github_event_tree": + errors.append(".github: dynamic command-center selection must use event SHA and event tree") + content_revision = entry.get("authority_content_revision") + if ( + not isinstance(content_revision, str) + or re.fullmatch(r"[0-9a-f]{40}", content_revision) is None + ): + errors.append( + ".github: authority content revision must be an immutable 40-character SHA" + ) + else: + unknown = sorted( + set(entry) + - { + "repository", + "canonical_repository", + "revision", + "authority_content_revision", + "reviewed_tree_sha", + } + ) + if unknown: + errors.append(f"{repository}: selection contains unsupported fields: {unknown}") + revision = entry.get("revision") + content_revision = entry.get("authority_content_revision") + tree = entry.get("reviewed_tree_sha") + if not isinstance(revision, str) or re.fullmatch(r"[0-9a-f]{40}", revision) is None: + errors.append(f"{repository}: selected revision must be an immutable 40-character SHA") + if ( + not isinstance(content_revision, str) + or re.fullmatch(r"[0-9a-f]{40}", content_revision) is None + ): + errors.append( + f"{repository}: authority content revision must be an immutable 40-character SHA" + ) + if not isinstance(tree, str) or re.fullmatch(r"[0-9a-f]{40}", tree) is None: + errors.append(f"{repository}: reviewed tree must be a 40-character Git tree SHA") + selections[repository] = entry + missing = sorted(set(REPO_NAMES) - set(selections)) + if missing or len(selections) != len(REPO_NAMES): + errors.append(f"seven-source selection manifest must name exactly seven repositories; missing={missing}") + constraints = manifest.get("constraints") + if not isinstance(constraints, dict): + errors.append("seven-source selection manifest constraints must be an object") + else: + expected_constraint_keys = { + "exact_repository_count", + "read_only", + "default_branch_fallback", + "require_detached_exact_revision", + "record_checked_revisions", + "consumer_outputs_are_not_authority", + "proof_ceiling", + } + unknown = sorted(set(constraints) - expected_constraint_keys) + missing_constraints = sorted(expected_constraint_keys - set(constraints)) + if unknown or missing_constraints: + errors.append( + "seven-source selection manifest constraints must have exact fields; " + f"missing={missing_constraints}, unsupported={unknown}" + ) + for key, expected in { + "exact_repository_count": 7, + "read_only": True, + "default_branch_fallback": False, + "require_detached_exact_revision": True, + "record_checked_revisions": True, + "consumer_outputs_are_not_authority": True, + }.items(): + if constraints.get(key) != expected: + errors.append(f"seven-source selection manifest constraint {key} must be {expected!r}") + if constraints.get("proof_ceiling") != "CONTROLLED_REPO_CONVERGENCE_AND_LOCAL_FIXTURE_REVIEW_ONLY": + errors.append("seven-source selection manifest proof ceiling is unsupported") + return selections, errors + + +def _verify_selected_source_checkout( + root: Path, + repository: str, + selections: dict[str, dict[str, Any]], +) -> list[str]: + errors: list[str] = [] + if repository not in REPO_NAMES: + return [f"unknown source repository {repository!r}"] + repo = root / repository + if not repo.is_dir(): + return [f"{repository}: selected source repository is missing"] + head = repo_head_sha(repo) + tree = _git_output(repo, "rev-parse", "HEAD^{tree}") + if head == "UNKNOWN" or tree is None: + return [f"{repository}: checked source head/tree is unavailable"] + entry = selections[repository] + authority_path = AUTHORITY_SOURCES[repository][1] + content_revision = str(entry["authority_content_revision"]) + if not git_commit_exists(repo, content_revision): + errors.append( + f"{repository}: authority content revision is unreachable in the checked repository" + ) + return errors + current_blob = git_blob_identity(repo, head, authority_path) + content_blob = git_blob_identity(repo, content_revision, authority_path) + if current_blob is None or content_blob is None or current_blob[0] != content_blob[0]: + errors.append( + f"{repository}: authority content revision does not carry the checked authority blob" + ) + elif ( + semantic_fingerprint(authority_path, current_blob[1]) + != semantic_fingerprint(authority_path, content_blob[1]) + ): + errors.append( + f"{repository}: authority content revision semantic fingerprint disagrees with current" + ) + head_is_content_ancestor = ( + head != content_revision and _is_ancestor(repo, head, content_revision) + ) + content_is_head_ancestor = ( + head != content_revision and _is_ancestor(repo, content_revision, head) + ) + content_tree = _git_output(repo, "rev-parse", f"{content_revision}^{{tree}}") + rewritten_reviewed_projection = False + if ( + repository != ".github" + and head != content_revision + and not head_is_content_ancestor + and not content_is_head_ancestor + ): + selected = str(entry["revision"]) + reviewed_tree = str(entry["reviewed_tree_sha"]) + selected_tree = _git_output(repo, "rev-parse", f"{selected}^{{tree}}") + content_is_selected_ancestor = ( + content_revision == selected + or _is_ancestor(repo, content_revision, selected) + ) + rewritten_reviewed_projection = ( + selected_tree is not None + and selected_tree == reviewed_tree + and tree == reviewed_tree + and content_is_selected_ancestor + ) + if head_is_content_ancestor: + errors.append( + f"{repository}: checked head is behind the authority content revision" + ) + elif ( + head != content_revision + and not content_is_head_ancestor + and content_tree != tree + and repository != ".github" + and not rewritten_reviewed_projection + ): + errors.append( + f"{repository}: authority content revision is outside the reviewed current lineage" + ) + if repository == ".github": + observed = os.environ.get( + "HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA", + "", + ).strip() + detached = repo_branch(repo).startswith("UNKNOWN_WITH_REASON:") + if detached and re.fullmatch(r"[0-9a-f]{40}", observed) is None: + errors.append( + ".github: detached command-center checkout requires " + "HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA" + ) + elif observed and observed != head: + errors.append( + ".github: checked command-center head differs from the immutable " + "workflow observation" + ) + return errors + selected = str(entry["revision"]) + reviewed_tree = str(entry["reviewed_tree_sha"]) + selected_tree = _git_output(repo, "rev-parse", f"{selected}^{{tree}}") + if selected_tree is not None and selected_tree != reviewed_tree: + errors.append(f"{repository}: manifest reviewed tree disagrees with its selected revision") + selected_exists = git_commit_exists(repo, selected) + checked_head_is_behind = head != selected and selected_exists and _is_ancestor(repo, head, selected) + selected_is_ancestor_of_head = head != selected and selected_exists and _is_ancestor(repo, selected, head) + if checked_head_is_behind: + errors.append( + f"{repository}: checked head is behind the explicit selected revision; " + "an arbitrary same-blob ancestor is not current authority" + ) + rewritten_content_identity = ( + head != selected + and not checked_head_is_behind + and not selected_is_ancestor_of_head + ) + if rewritten_content_identity and tree != reviewed_tree: + errors.append( + f"{repository}: checked tree does not match the explicit reviewed tree; " + "refresh the immutable source selection after content changes" + ) + return errors + + +def verify_selected_source_checkout(repo_root: Path, repository: str) -> list[str]: + """Prove a checked source is the manifest-selected content, not an arbitrary same-blob ancestor.""" + root = Path(repo_root).resolve() + selections, errors = _load_convergence_source_selections(root) + if errors: + return errors + return _verify_selected_source_checkout(root, repository, selections) + + +def verify_all_selected_source_checkouts(repo_root: Path) -> list[str]: + root = Path(repo_root).resolve() + selections, errors = _load_convergence_source_selections(root) + if errors: + return errors + for repository in REPO_NAMES: + errors.extend(_verify_selected_source_checkout(root, repository, selections)) + return errors + + +def _build_source_revisions( + repo_paths: dict[str, Path | None], + selections: dict[str, dict[str, Any]] | None = None, +) -> list[dict[str, Any]]: + revisions: list[dict[str, Any]] = [] + for repository in REPO_NAMES: + repo = repo_paths.get(repository) + authority_role, relative_path = AUTHORITY_SOURCES[repository] + source = repo / relative_path if repo is not None else None + source_exists = source is not None and source.is_file() + current_head = repo_head_sha(repo) if repo is not None else "UNKNOWN" + selected_content_sha = ( + str(selections[repository]["authority_content_revision"]) + if selections is not None and repository in selections + else current_head + ) + branch = repo_branch(repo) if repo is not None else "NOT_FOUND" + resolved_ref = ( + current_head + if branch.startswith("UNKNOWN_WITH_REASON:") + else branch + ) + dirty = repo_dirty(repo) if repo is not None else False + dirty_paths = repo_dirty_paths(repo) if repo is not None else [] + authority_dirty = relative_path.replace("\\", "/").casefold() in { + path.replace("\\", "/").casefold() for path in dirty_paths + } + origin = repo_origin(repo) if repo is not None else "UNKNOWN" + canonical_origin = _normalized_origin(origin) == CANONICAL_ORIGINS[repository] + if repo is None: + freshness = "MISSING_REPOSITORY" + elif not source_exists: + freshness = "MISSING_AUTHORITY_SOURCE" + elif current_head == "UNKNOWN": + freshness = "UNVERSIONED_SOURCE" + elif authority_dirty: + freshness = "WORKTREE_MODIFIED" + elif not canonical_origin: + freshness = "REPOSITORY_IDENTITY_INVALID" + else: + freshness = "CURRENT" + blob_identity = ( + git_blob_identity(repo, selected_content_sha, relative_path) + if repo is not None + else None + ) + committed_fingerprint = hashlib.sha256(blob_identity[1]).hexdigest() if blob_identity is not None else None + semantic = semantic_fingerprint(relative_path, blob_identity[1]) if blob_identity is not None else None + revisions.append( + { + "repository": repository, + "authority_role": authority_role, + "resolved_ref": resolved_ref, + "source_commit_sha": selected_content_sha, + "source_observed_head_sha": selected_content_sha, + "current_observed_head_sha": current_head, + "source_observation_kind": "reviewed_immutable_commit", + "source_parent_sha": None, + "self_referential": False, + "revision_scope": "content_addressed_authority", + "source_path": relative_path, + "authoritative_path": relative_path, + "authoritative_git_blob_sha": blob_identity[0] if blob_identity is not None else None, + "source_git_blob_sha": blob_identity[0] if blob_identity is not None else None, + "source_file_sha256": ( + committed_fingerprint + if committed_fingerprint is not None + else file_sha256(source) if source_exists and source is not None else None + ), + "authoritative_content_fingerprint": semantic, + "source_semantic_fingerprint_sha256": semantic, + "canonical_origin": CANONICAL_ORIGINS[repository], + "observed_origin": _normalized_origin(origin), + "repository_dirty_observed": dirty, + "authority_source_dirty": authority_dirty, + "source_freshness_state": freshness, + "snapshot_freshness_state": "CURRENT", + "historical_snapshot": False, + "current_authority": source_exists and current_head != "UNKNOWN" and canonical_origin and not authority_dirty, + "missing_source_state": not source_exists, + "dangling_reference_state": repo is not None and not source_exists, + "contradictions": [], + "drift": [], + "next_legal_action": ( + "none; preserve source ownership" + if freshness == "CURRENT" + else "review and commit scoped authority-source changes before regenerating" + if freshness == "WORKTREE_MODIFIED" + else f"restore {repository}/{relative_path} from its owning repository" + ), + } + ) + return revisions + + +def _finding( + code: str, + owner: str, + path: str, + expected: Any, + actual: Any, + remediation: str, + classification: str = "ACTIONABLE_DRIFT", +) -> dict[str, Any]: + return { + "code": code, + "source_owner": owner, + "source_path": path, + "expected": expected, + "actual": actual, + "classification": classification, + "next_legal_action": remediation, + } + + +def _duplicates(values: list[str]) -> list[str]: + return sorted({value for value in values if values.count(value) > 1}) + + +def _source_convergence_findings( + repo_paths: dict[str, Path | None], + source_revisions: list[dict[str, Any]], + summary: dict[str, int], + rows: list[dict[str, Any]], +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + contradictions: list[dict[str, Any]] = [] + drift: list[dict[str, Any]] = [] + for revision in source_revisions: + if revision["missing_source_state"]: + contradictions.append( + _finding( + "MISSING_AUTHORITY_SOURCE", + revision["repository"], + revision["source_path"], + "existing authoritative source", + "missing", + revision["next_legal_action"], + ) + ) + + structured_sources = ( + ("hawkinsoperations-detections", "detections/DETECTION_PROMOTION_MATRIX.yml", "entries"), + ("hawkinsoperations-validation", "validation/VALIDATION_REGISTRY.yml", "packages"), + ("hawkinsoperations-proof", "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", "entries"), + ) + for owner, relative_path, collection in structured_sources: + repo = repo_paths.get(owner) + if repo is None or not (repo / relative_path).is_file(): + continue + data = load_structured(repo / relative_path) or {} + ids = [str(item.get("detection_id")) for item in data.get(collection, []) if isinstance(item, dict) and item.get("detection_id")] + for duplicate in _duplicates(ids): + contradictions.append( + _finding( + "DUPLICATE_CASE_ID", + owner, + relative_path, + "one entry per case ID", + duplicate, + f"remove or reconcile the duplicate {duplicate} entry in the owning source", + ) + ) + + proof_repo = repo_paths.get("hawkinsoperations-proof") + if proof_repo is not None: + proof_path = proof_repo / AUTHORITY_SOURCES["hawkinsoperations-proof"][1] + if proof_path.is_file(): + proof_data = load_structured(proof_path) or {} + for entry in proof_data.get("entries", []): + if not isinstance(entry, dict): + continue + case_id = str(entry.get("detection_id") or "UNKNOWN") + for field in ("proof_record_path", "proof_card_path"): + ref = entry.get(field) + if ref and not (proof_repo / str(ref)).is_file(): + contradictions.append( + _finding( + "DANGLING_PROOF_PATH", + "hawkinsoperations-proof", + str(ref), + "existing source-controlled file", + f"missing reference for {case_id}", + f"repair or explicitly clear {field} for {case_id} in the proof index", + ) + ) + + detection_repo = repo_paths.get("hawkinsoperations-detections") + if detection_repo is not None and proof_repo is not None: + matrix_path = detection_repo / AUTHORITY_SOURCES["hawkinsoperations-detections"][1] + proof_path = proof_repo / AUTHORITY_SOURCES["hawkinsoperations-proof"][1] + if matrix_path.is_file() and proof_path.is_file(): + matrix_entries = { + str(item.get("detection_id")): item + for item in (load_structured(matrix_path) or {}).get("entries", []) + if isinstance(item, dict) and item.get("detection_id") + } + proof_entries = { + str(item.get("detection_id")): item + for item in (load_structured(proof_path) or {}).get("entries", []) + if isinstance(item, dict) and item.get("detection_id") + } + for case_id, proof_entry in proof_entries.items(): + matrix_entry = matrix_entries.get(case_id, {}) + notes = str(matrix_entry.get("notes") or "") + if proof_entry.get("proof_record_path") and re.search(r"(?i)\bno\b.*\bproof record\b", notes): + contradictions.append( + _finding( + "DETECTION_PROOF_RECORD_CONTRADICTION", + "hawkinsoperations-detections", + "detections/DETECTION_PROMOTION_MATRIX.yml", + f"proof record exists at {proof_entry['proof_record_path']}", + notes, + f"update the detection matrix note for {case_id} from the proof-owned current index", + ) + ) + + website_repo = repo_paths.get("hawkinsoperations-website") + if website_repo is not None: + website_path = website_repo / "public" / "data" / "public-status.json" + if website_path.is_file(): + website = load_structured(website_path) or {} + rendered = ((website.get("metrics") or {}).get("proof_records") or {}).get("value") + current = summary["proof_records_count"] + if rendered is not None and rendered != current: + drift.append( + _finding( + "WEBSITE_PROOF_COUNT_DRIFT", + "hawkinsoperations-proof", + "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", + current, + rendered, + "regenerate website public status from the proof-owned current index; website remains rendering-only", + ) + ) + + case_ids = [str(row.get("case_id")) for row in rows] + for duplicate in _duplicates(case_ids): + contradictions.append( + _finding( + "DUPLICATE_GENERATED_CASE_ID", + "hoxline", + "generated cases", + "unique case IDs", + duplicate, + "reconcile duplicate source entries before regenerating the snapshot", + ) + ) + return contradictions, drift + + +def _next_legal_action( + source_revisions: list[dict[str, Any]], contradictions: list[dict[str, Any]], drift: list[dict[str, Any]] +) -> str: + if contradictions: + return str(contradictions[0]["next_legal_action"]) + if drift: + return str(drift[0]["next_legal_action"]) + if any(item["source_freshness_state"] == "WORKTREE_MODIFIED" for item in source_revisions): + return "commit only the validated scoped changes, then regenerate the current snapshot from clean source revisions" + return "none; current source-controlled inputs converge" + + +def _reproducibility_hash(index: dict[str, Any]) -> str: + stable = deepcopy(index) + stable.pop("generated_at", None) + stable.pop("reproducibility_sha256", None) + encoded = json.dumps(stable, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def _content_normalized_cases(value: Any) -> Any: + """Remove only commit-clock observations after source currentness is verified separately.""" + if not isinstance(value, list): + return value + normalized = deepcopy(value) + for row in normalized: + if isinstance(row, dict): + row.pop("last_updated", None) + return normalized + + +def _source_manifest_digest(source_revisions: list[dict[str, Any]]) -> str: + manifest = [ + { + "repository": item.get("repository"), + "authority_role": item.get("authority_role"), + "authoritative_path": item.get("authoritative_path") or item.get("source_path"), + "authoritative_git_blob_sha": item.get("authoritative_git_blob_sha"), + "authoritative_content_fingerprint": item.get("authoritative_content_fingerprint"), + } + for item in source_revisions + ] + encoded = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + def _base_row(case_id: str) -> dict[str, Any]: detection_id = None if case_id.startswith("HOX-GAUNTLET-") else case_id return { @@ -675,3 +1361,338 @@ def _add_cross_repo_quality_notes(rows: list[dict[str, Any]], notes: list[str]) notes.append(f"{row['case_id']} has controlled validation but no proof record") if row["proof_record_status"] == "PROOF_RECORD_EXISTS" and row["proofcard_status"] != "PROOFCARD_EXISTS": notes.append(f"{row['case_id']} has proof record but no ProofCard") + + +def verify_case_growth_snapshot(repo_root: Path, snapshot: dict[str, Any]) -> tuple[list[str], dict[str, Any]]: + errors: list[str] = [] + current = build_case_growth_index(repo_root, generated_at=str(snapshot.get("generated_at") or "1970-01-01T00:00:00Z")) + historical = snapshot.get("historical_snapshot") is True + current_authority = snapshot.get("current_authority") is True + if historical and current_authority: + errors.append("snapshot cannot be both historical_snapshot=true and current_authority=true") + if not historical and not current_authority: + errors.append("non-historical snapshot must declare current_authority=true") + if snapshot.get("schema_version") != "case-growth-index-v1": + errors.append("snapshot schema_version must be case-growth-index-v1") + + if _contains_absolute_local_path(snapshot): + errors.append("snapshot contains an absolute local path") + + cases = snapshot.get("cases") if isinstance(snapshot.get("cases"), list) else [] + case_ids = [str(item.get("case_id")) for item in cases if isinstance(item, dict) and item.get("case_id")] + for duplicate in _duplicates(case_ids): + errors.append(f"duplicate case ID in snapshot: {duplicate}") + + boundary = snapshot.get("boundary") if isinstance(snapshot.get("boundary"), dict) else {} + for key, value in boundary.items(): + if value is not False: + errors.append(f"unauthorized boundary promotion: {key}={value!r}") + for row in cases: + if not isinstance(row, dict): + continue + case_id = row.get("case_id", "UNKNOWN") + if row.get("public_safe_status") not in {None, "NOT_PUBLIC_SAFE"}: + errors.append(f"{case_id}: unauthorized public-safe status {row.get('public_safe_status')!r}") + if row.get("case_state") == "CLOSED": + errors.append(f"{case_id}: unauthorized case closure") + if row.get("signal_status") not in {None, "NOT_PROVEN"}: + errors.append(f"{case_id}: signal status exceeds checked-source authority") + errors.extend(f"{case_id}: {error}" for error in _case_claim_violations(row)) + + stated_revisions = snapshot.get("source_revisions") if isinstance(snapshot.get("source_revisions"), list) else [] + if len(stated_revisions) != len(REPO_NAMES): + errors.append(f"source_revisions must contain exactly {len(REPO_NAMES)} repositories") + stated_names = [str(item.get("repository") or "") for item in stated_revisions if isinstance(item, dict)] + if len(stated_names) != len(set(stated_names)): + errors.append("source_revisions repository names must be unique") + if set(stated_names) != set(REPO_NAMES): + missing = sorted(set(REPO_NAMES) - set(stated_names)) + extra = sorted(set(stated_names) - set(REPO_NAMES)) + errors.append(f"source_revisions must match exact seven-repository set; missing={missing}, extra={extra}") + current_by_repo = {item["repository"]: item for item in current["source_revisions"]} + repo_paths = resolve_repo_paths(Path(repo_root)) + selection_errors: list[str] = [] + if current_authority: + selection_errors = verify_all_selected_source_checkouts(Path(repo_root)) + errors.extend(selection_errors) + for stated in stated_revisions: + if not isinstance(stated, dict): + errors.append("source_revisions entries must be objects") + continue + repository = str(stated.get("repository") or "") + if repository not in current_by_repo: + errors.append(f"unknown source repository in snapshot: {repository or 'MISSING'}") + continue + current_revision = current_by_repo[repository] + stated_sha = stated.get("source_commit_sha") + observed_head = stated.get("source_observed_head_sha") + current_observed = stated.get("current_observed_head_sha") + if stated.get("source_observation_kind") != "reviewed_immutable_commit": + errors.append(f"{repository}: source_observation_kind must be reviewed_immutable_commit") + if observed_head != stated_sha: + errors.append( + f"{repository}: source_observed_head_sha must identify the content-addressed source commit" + ) + if not re.fullmatch(r"[0-9a-f]{40}", str(current_observed or "")): + errors.append( + f"{repository}: current_observed_head_sha must be a 40-character Git SHA" + ) + resolved_ref = stated.get("resolved_ref") + if not isinstance(resolved_ref, str) or not re.fullmatch(r"[A-Za-z0-9._/-]+", resolved_ref): + errors.append(f"{repository}: resolved_ref is malformed") + source_path = str(stated.get("authoritative_path") or stated.get("source_path") or "") + if source_path != current_revision["source_path"]: + errors.append(f"{repository}: authoritative source path disagrees with current owner path") + stated_blob = stated.get("authoritative_git_blob_sha") or stated.get("source_git_blob_sha") + current_blob = current_revision.get("authoritative_git_blob_sha") + if stated_blob != current_blob: + errors.append( + f"{repository}: authoritative Git blob disagrees with the file at the checked current tree" + ) + stated_semantic = ( + stated.get("authoritative_content_fingerprint") + or stated.get("source_semantic_fingerprint_sha256") + ) + current_semantic = current_revision.get("authoritative_content_fingerprint") + if stated_semantic != current_semantic: + errors.append( + f"{repository}: authoritative semantic fingerprint disagrees with checked current content" + ) + if stated.get("revision_scope") != "content_addressed_authority": + errors.append(f"{repository}: revision_scope must be content_addressed_authority") + if stated.get("self_referential") is not False: + errors.append(f"{repository}: generated consumers must not be self-referential authority") + if stated.get("canonical_origin") != current_revision.get("canonical_origin"): + errors.append(f"{repository}: canonical repository origin disagrees with owner") + if stated.get("observed_origin") != current_revision.get("observed_origin"): + errors.append(f"{repository}: observed repository origin is not canonical") + if not re.fullmatch(r"[0-9a-f]{40}", str(stated_sha or "")): + errors.append(f"{repository}: source_commit_sha must be a 40-character Git SHA") + elif repo_paths.get(repository) is None: + errors.append(f"{repository}: source repository is missing") + elif git_commit_exists(repo_paths[repository], str(stated_sha)): + observed_identity = git_blob_identity(repo_paths[repository], str(stated_sha), source_path) + if observed_identity is None or observed_identity[0] != current_blob: + errors.append( + f"{repository}: observed commit does not carry the checked current authoritative blob" + ) + else: + # The observed branch tip is freshness metadata, not the authority + # identity. A squash/rebase may make that commit unavailable while + # the checked current path/blob/semantic identity remains exact. + # `source_observation_kind` and the three equal observation fields + # above keep this explicitly bounded rather than silently treating + # an arbitrary ancestor as current authority. + pass + if ( + repo_paths.get(repository) is not None + and re.fullmatch(r"[0-9a-f]{40}", str(current_observed or "")) + and git_commit_exists(repo_paths[repository], str(current_observed)) + ): + current_observed_identity = git_blob_identity( + repo_paths[repository], + str(current_observed), + source_path, + ) + if ( + current_observed_identity is None + or current_observed_identity[0] != current_blob + ): + errors.append( + f"{repository}: generation-time head observation does not carry " + "the checked current authoritative blob" + ) + if stated.get("source_file_sha256") != current_revision.get("source_file_sha256") and not historical: + errors.append( + f"{repository}: authoritative source fingerprint drifted; regenerate from {current_revision['source_path']}" + ) + if stated.get("missing_source_state") is True or stated.get("dangling_reference_state") is True: + errors.append(f"{repository}: snapshot records missing or dangling authority source") + allowed_freshness = {"CURRENT"} + if current_authority and stated.get("source_freshness_state") not in allowed_freshness: + errors.append( + f"{repository}: current snapshot source_freshness_state must be one of {sorted(allowed_freshness)}, " + f"got {stated.get('source_freshness_state')!r}" + ) + if current_authority and current_revision.get("source_freshness_state") not in allowed_freshness: + errors.append( + f"{repository}: current repository source is not clean/current: " + f"{current_revision.get('source_freshness_state')!r}" + ) + + stated_hash = snapshot.get("reproducibility_sha256") + if stated_hash != _reproducibility_hash(snapshot): + errors.append("snapshot reproducibility_sha256 does not reproduce from its normalized content") + stated_source_manifest = snapshot.get("source_manifest_digest") + if stated_source_manifest != _source_manifest_digest(stated_revisions): + errors.append("snapshot source_manifest_digest does not reproduce from its authority identities") + if current_authority and stated_source_manifest != current.get("source_manifest_digest"): + errors.append("current snapshot source_manifest_digest disagrees with checked authority content") + snapshot_state = snapshot.get("snapshot_state") if isinstance(snapshot.get("snapshot_state"), dict) else {} + if snapshot_state.get("identity_model") != ( + "repo_path_git_blob_and_semantic_fingerprint_with_separate_head_observation" + ): + errors.append("snapshot identity model is missing or unsupported") + if snapshot_state.get("generated_consumers_are_authority") is not False: + errors.append("generated consumers must not be classified as authority") + if snapshot_state.get("historical_snapshot") is not historical: + errors.append("snapshot_state historical classification disagrees with snapshot") + if snapshot_state.get("current_authority") is not current_authority: + errors.append("snapshot_state current authority classification disagrees with snapshot") + + if current_authority: + if snapshot.get("summary") != current.get("summary"): + errors.append("current snapshot summary counts disagree with current authoritative repository state") + if snapshot.get("case_ids_discovered_count") != current.get("case_ids_discovered_count"): + errors.append("current snapshot case count disagrees with current authoritative repository state") + for field in ("cases", "case_growth_health", "repo_slot_accuracy", "boundary"): + before = snapshot.get(field) + after = current.get(field) + if field == "cases" and not selection_errors: + before = _content_normalized_cases(before) + after = _content_normalized_cases(after) + if before != after: + errors.append(f"current snapshot {field} disagrees with normalized current authoritative content") + for finding in current.get("contradictions", []): + errors.append(f"current contradiction {finding['code']}: {finding['actual']} ({finding['next_legal_action']})") + for finding in current.get("drift", []): + errors.append(f"current drift {finding['code']}: expected {finding['expected']}, actual {finding['actual']} ({finding['next_legal_action']})") + return errors, current + + +ABSOLUTE_LOCAL_PATH = re.compile( + r"(?i)(?:[A-Z]:[\\/]|(?]+)" +) + + +def _contains_absolute_local_path(value: Any) -> bool: + if isinstance(value, dict): + return any(_contains_absolute_local_path(key) or _contains_absolute_local_path(item) for key, item in value.items()) + if isinstance(value, list): + return any(_contains_absolute_local_path(item) for item in value) + if isinstance(value, str): + return ABSOLUTE_LOCAL_PATH.search(value) is not None + return False + + +def _case_claim_violations(row: dict[str, Any]) -> list[str]: + violations: list[str] = [] + runtime = str(row.get("runtime_candidate_status") or "") + authority = str(row.get("claim_authority_status") or "") + if re.search(r"(?i)RUNTIME[_ -]?ACTIVE|PRODUCTION[_ -]?READY", runtime): + violations.append(f"unauthorized runtime_candidate_status {runtime!r}") + if re.search(r"(?i)(?:AI|ANALYST)[_ -]?APPROVED|FINAL[_ -]?AUTHORIZATION|CASE[_ -]?CLOSED", authority): + violations.append(f"unauthorized claim_authority_status {authority!r}") + safe_row = {key: value for key, value in row.items() if key != "blocked_claims"} + text = json.dumps(safe_row, sort_keys=True) + text = re.sub( + r"(?i)\b(?:missing|blocked|not)(?:_[a-z0-9]+)*_(?:final_authorization|case_closure|ai_approved_disposition|analyst_approved_disposition)\b", + "", + text, + ) + for label, pattern in ( + ("AI-approved disposition", r"(?i)AI[-_ ]approved disposition"), + ("analyst-approved disposition", r"(?i)analyst[-_ ]approved disposition"), + ("final authorization", r"(?i)final[-_ ]authorization"), + ("case closure", r"(?i)case[-_ ](?:closure|closed)"), + ): + if re.search(pattern, text): + violations.append(f"unauthorized {label} wording outside blocked_claims") + return violations + + +def diff_case_growth_snapshot(repo_root: Path, snapshot: dict[str, Any]) -> dict[str, Any]: + current = build_case_growth_index(repo_root, generated_at=str(snapshot.get("generated_at") or "1970-01-01T00:00:00Z")) + historical = snapshot.get("historical_snapshot") is True + before_revisions = { + item.get("repository"): item + for item in snapshot.get("source_revisions", []) + if isinstance(item, dict) and item.get("repository") + } + changes: list[dict[str, Any]] = [] + for current_revision in current["source_revisions"]: + repository = current_revision["repository"] + before = before_revisions.get(repository, {}) + for field in ( + "source_commit_sha", + "source_observed_head_sha", + "current_observed_head_sha", + "source_file_sha256", + "authoritative_git_blob_sha", + "authoritative_content_fingerprint", + "source_path", + ): + if before.get(field) != current_revision[field]: + observation_only_content_current = ( + field in {"source_commit_sha", "source_observed_head_sha", "current_observed_head_sha"} + and before.get("authoritative_git_blob_sha") + == current_revision.get("authoritative_git_blob_sha") + and before.get("authoritative_content_fingerprint") + == current_revision.get("authoritative_content_fingerprint") + ) + changes.append( + { + "field": field, + "source_owner": repository, + "source_path": current_revision["source_path"], + "before": before.get(field), + "after": current_revision[field], + "old_source_revision": before.get("source_commit_sha"), + "current_source_revision": current_revision["source_commit_sha"], + "classification": ( + "EXPECTED_HISTORICAL_CONTEXT" + if historical + else "OBSERVATION_ONLY_CONTENT_CURRENT" + if observation_only_content_current + else "ACTIONABLE_DRIFT" + ), + "next_remediation": ( + "retain as historical context" + if historical + else "refresh observed-head metadata when producing the next reviewer snapshot; no authority-content regeneration required" + if observation_only_content_current + else "regenerate the current snapshot from the owning source" + ), + } + ) + before_summary = snapshot.get("summary") if isinstance(snapshot.get("summary"), dict) else {} + for field, after in current["summary"].items(): + before = before_summary.get(field) + if before != after: + changes.append( + { + "field": f"summary.{field}", + "source_owner": "hoxline", + "source_path": "derived from seven source-owned inputs", + "before": before, + "after": after, + "old_source_revision": None, + "current_source_revision": current_by_repo_sha(current, "hoxline"), + "classification": "EXPECTED_HISTORICAL_CONTEXT" if historical else "ACTIONABLE_DRIFT", + "next_remediation": "retain as historical context" if historical else "regenerate the current snapshot", + } + ) + actionable_changes = [item for item in changes if item["classification"] == "ACTIONABLE_DRIFT"] + return { + "schema_version": "case-growth-diff-v1", + "historical_snapshot": historical, + "current_authority": snapshot.get("current_authority") is True, + "changes": changes, + "contradictions": current["contradictions"], + "drift": current["drift"], + "next_legal_action": ( + current["next_legal_action"] + if actionable_changes or current["drift"] + else "none; authority content converges and only observed-head metadata changed" + if changes + else "none; snapshot converges" + ), + } + + +def current_by_repo_sha(index: dict[str, Any], repository: str) -> str | None: + for item in index.get("source_revisions", []): + if item.get("repository") == repository: + return item.get("source_commit_sha") + return None diff --git a/src/hoxline/case_growth/discovery.py b/src/hoxline/case_growth/discovery.py index 081c1a3..92c47f5 100644 --- a/src/hoxline/case_growth/discovery.py +++ b/src/hoxline/case_growth/discovery.py @@ -1,6 +1,8 @@ from __future__ import annotations import json +import hashlib +import os import re import subprocess from pathlib import Path @@ -23,16 +25,63 @@ "hoxline", ) +CASE_AUTHORITY_COLLECTIONS = ( + ("hawkinsoperations-detections", "detections/DETECTION_PROMOTION_MATRIX.yml", "entries"), + ("hawkinsoperations-validation", "validation/VALIDATION_REGISTRY.yml", "packages"), + ("hawkinsoperations-proof", "proof/indexes/DETECTION_PROOF_STATUS_INDEX.yml", "entries"), +) + + +class _UniqueKeyLoader(yaml.SafeLoader): + pass + + +def sanitized_git_env() -> dict[str, str]: + env = { + key: value + for key, value in os.environ.items() + if not key.casefold().startswith("git_") + } + env["GIT_NO_REPLACE_OBJECTS"] = "1" + env["GIT_TERMINAL_PROMPT"] = "0" + return env + + +def _construct_unique_mapping(loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False) -> dict[Any, Any]: + pairs = loader.construct_pairs(node, deep=deep) + result: dict[Any, Any] = {} + seen: set[str] = set() + for key, value in pairs: + normalized = str(key).casefold() + if normalized in seen: + raise ValueError("structured authority source contains a duplicate key") + seen.add(normalized) + result[key] = value + return result + + +_UniqueKeyLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + _construct_unique_mapping, +) + + +def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + seen: set[str] = set() + for key, value in pairs: + normalized = key.casefold() + if normalized in seen: + raise ValueError("structured authority source contains a duplicate key") + seen.add(normalized) + result[key] = value + return result + def resolve_repo_paths(repo_root: Path) -> dict[str, Path | None]: root = repo_root.resolve() paths: dict[str, Path | None] = {} - github_candidates = ( - root / ".github", - root / "HawkinsOperations.github", - root.parent / "HawkinsOperations.github", - ) - paths[".github"] = next((path for path in github_candidates if path.exists()), None) + paths[".github"] = root / ".github" if (root / ".github").is_dir() else None for name in REPO_NAMES: if name == ".github": continue @@ -50,6 +99,7 @@ def git_lines(repo_path: Path, args: list[str]) -> list[str]: text=True, encoding="utf-8", errors="replace", + env=sanitized_git_env(), ) except (OSError, subprocess.CalledProcessError): return [] @@ -61,8 +111,148 @@ def repo_branch(repo_path: Path) -> str: return lines[0] if lines else "UNKNOWN_WITH_REASON: no git branch available" +def _is_volatile_generated_path(path: str) -> bool: + normalized = path.replace("\\", "/").lstrip("./") + return ( + "/__pycache__/" in f"/{normalized}" + or normalized.endswith(".pyc") + or normalized.startswith(".hoxline/") + ) + + def repo_dirty(repo_path: Path) -> bool: - return bool(git_lines(repo_path, ["status", "--short"])) + return bool(repo_dirty_paths(repo_path)) + + +def repo_dirty_paths(repo_path: Path) -> list[str]: + paths = [line[3:].strip().replace("\\", "/") for line in git_lines(repo_path, ["status", "--short"]) if len(line) > 3] + return [path for path in paths if not _is_volatile_generated_path(path)] + + +def repo_head_sha(repo_path: Path) -> str: + lines = git_lines(repo_path, ["rev-parse", "HEAD"]) + return lines[0] if lines and re.fullmatch(r"[0-9a-f]{40}", lines[0]) else "UNKNOWN" + + +def repo_parent_sha(repo_path: Path) -> str: + lines = git_lines(repo_path, ["rev-parse", "HEAD^"]) + return lines[0] if lines and re.fullmatch(r"[0-9a-f]{40}", lines[0]) else "UNKNOWN" + + +def git_commit_exists(repo_path: Path, sha: str) -> bool: + if not re.fullmatch(r"[0-9a-f]{40}", sha): + return False + try: + result = subprocess.run( + ["git", "-C", str(repo_path), "cat-file", "-e", f"{sha}^{{commit}}"], + check=False, + capture_output=True, + text=True, + env=sanitized_git_env(), + ) + except OSError: + return False + return result.returncode == 0 + + +def git_blob_sha256(repo_path: Path, sha: str, repo_relative_path: str) -> str | None: + if not re.fullmatch(r"[0-9a-f]{40}", sha): + return None + normalized_path = repo_relative_path.replace("\\", "/").lstrip("/") + if not normalized_path or ".." in Path(normalized_path).parts: + return None + try: + result = subprocess.run( + ["git", "-C", str(repo_path), "show", f"{sha}:{normalized_path}"], + check=False, + capture_output=True, + env=sanitized_git_env(), + ) + except OSError: + return None + if result.returncode != 0: + return None + return hashlib.sha256(result.stdout).hexdigest() + + +def git_blob_identity(repo_path: Path, sha: str, repo_relative_path: str) -> tuple[str, bytes] | None: + if not re.fullmatch(r"[0-9a-f]{40}", sha): + return None + normalized_path = repo_relative_path.replace("\\", "/").lstrip("/") + if ( + not normalized_path + or normalized_path.startswith("/") + or "\\" in normalized_path + or any(part in {"", ".", ".."} for part in normalized_path.split("/")) + ): + return None + blob = git_lines(repo_path, ["rev-parse", f"{sha}:{normalized_path}"]) + if len(blob) != 1 or not re.fullmatch(r"[0-9a-f]{40}", blob[0]): + return None + try: + result = subprocess.run( + ["git", "-C", str(repo_path), "cat-file", "blob", blob[0]], + check=False, + capture_output=True, + env=sanitized_git_env(), + ) + except OSError: + return None + if result.returncode != 0: + return None + return blob[0], result.stdout + + +def semantic_fingerprint(repo_relative_path: str, raw: bytes) -> str: + suffix = Path(repo_relative_path).suffix.casefold() + if suffix == ".json": + value = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_json_object) + canonical = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + elif suffix in {".yml", ".yaml"}: + value = yaml.load(raw.decode("utf-8"), Loader=_UniqueKeyLoader) + canonical = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + else: + canonical = raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + return hashlib.sha256(canonical).hexdigest() + + +def repo_origin(repo_path: Path) -> str: + # Inspect only the value physically stored in this repository. `git remote + # get-url` applies ambient url.*.insteadOf rewriting and can conceal a + # non-canonical stored owner. + try: + result = subprocess.run( + [ + "git", + "-C", + str(repo_path), + "config", + "--local", + "--null", + "--get-all", + "remote.origin.url", + ], + check=True, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + env=sanitized_git_env(), + ) + except (OSError, subprocess.CalledProcessError): + return "UNKNOWN" + values = result.stdout.split("\0") + if values and values[-1] == "": + values.pop() + stripped = [value.strip() for value in values] + return stripped[0] if len(stripped) == 1 and stripped[0] else "UNKNOWN" + + +def file_sha256(path: Path) -> str | None: + try: + return hashlib.sha256(path.read_bytes()).hexdigest() + except OSError: + return None def tracked_files(repo_path: Path) -> list[Path]: @@ -108,27 +298,32 @@ def repo_relative(repo_name: str, repo_path: Path, path: Path) -> str: def load_structured(path: Path) -> Any: text = path.read_text(encoding="utf-8") if path.suffix.lower() == ".json": - return json.loads(text) + return json.loads(text, object_pairs_hook=_unique_json_object) if path.suffix.lower() in {".yml", ".yaml"}: - return yaml.safe_load(text) + return yaml.load(text, Loader=_UniqueKeyLoader) raise ValueError(f"unsupported structured file: {path}") def discover_case_ids(repo_paths: dict[str, Path | None]) -> tuple[set[str], int]: ids: set[str] = set() scanned = 0 - for repo_name, repo_path in repo_paths.items(): + for repo_name, relative_path, collection_name in CASE_AUTHORITY_COLLECTIONS: + repo_path = repo_paths.get(repo_name) if repo_path is None: continue - for path in case_growth_files(repo_name, repo_path): - scanned += 1 - if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".avif", ".zip", ".sqlite"}: - continue - try: - text = path.read_text(encoding="utf-8", errors="ignore") - except OSError: + path = repo_path / relative_path + if not path.is_file(): + continue + scanned += 1 + data = load_structured(path) + if not isinstance(data, dict) or not isinstance(data.get(collection_name), list): + continue + for entry in data[collection_name]: + if not isinstance(entry, dict): continue - ids.update(CASE_ID_PATTERN.findall(text)) + case_id = entry.get("detection_id") + if isinstance(case_id, str) and CASE_ID_PATTERN.fullmatch(case_id): + ids.add(case_id) return ids, scanned diff --git a/src/hoxline/case_growth/render.py b/src/hoxline/case_growth/render.py index 7a6a02b..56cb3f4 100644 --- a/src/hoxline/case_growth/render.py +++ b/src/hoxline/case_growth/render.py @@ -8,11 +8,15 @@ def render_case_growth_markdown(index: dict[str, Any]) -> str: health = index["case_growth_health"] repo_slots = index.get("repo_slot_accuracy", {}) lines = [ - "# Hoxline Case Growth Index v0", + "# Hoxline Case Growth Index v1", "", f"Generated: `{index['generated_at']}`", f"Proof ceiling: `{index['proof_ceiling']}`", f"Repo-slot accuracy: `{repo_slots.get('wording', 'UNKNOWN_WITH_REASON')}`", + f"Historical snapshot: `{str(index.get('historical_snapshot')).lower()}`", + f"Current authority: `{str(index.get('current_authority')).lower()}`", + f"Source manifest digest: `{index.get('source_manifest_digest')}`", + f"Reproducibility SHA-256: `{index.get('reproducibility_sha256')}`", "", "## Summary", "", @@ -41,6 +45,35 @@ def render_case_growth_markdown(index: dict[str, Any]) -> str: ): lines.append(f"| `{key}` | {summary[key]} |") + lines.extend( + [ + "", + "## Source Revisions", + "", + "| Repository | Authority role | Authority path | Observed head | Git blob | Semantic fingerprint | Source freshness |", + "| --- | --- | --- | --- | --- | --- | --- |", + ] + ) + for source in index.get("source_revisions", []): + lines.append( + f"| {_cell(source['repository'])} | {_cell(source['authority_role'])} | " + f"{_cell(source.get('authoritative_path') or source['source_path'])} | " + f"`{source['source_commit_sha']}` | `{source.get('authoritative_git_blob_sha')}` | " + f"`{source.get('authoritative_content_fingerprint')}` | `{source['source_freshness_state']}` |" + ) + + lines.extend(["", "## Convergence Findings", ""]) + findings = list(index.get("contradictions", [])) + list(index.get("drift", [])) + if findings: + for finding in findings: + lines.append( + f"- `{finding['code']}` owner `{finding['source_owner']}` path `{finding['source_path']}`: " + f"expected `{finding['expected']}`, actual `{finding['actual']}`; next: {finding['next_legal_action']}" + ) + else: + lines.append("- No missing, dangling, contradictory, or stale source-owned state detected.") + lines.extend(["", f"Next legal action: {index.get('next_legal_action')}"]) + lines.extend( [ "", diff --git a/src/hoxline/case_growth/report.py b/src/hoxline/case_growth/report.py index 12cf597..7b584f9 100644 --- a/src/hoxline/case_growth/report.py +++ b/src/hoxline/case_growth/report.py @@ -1,6 +1,12 @@ from __future__ import annotations -from .collector import ROW_FIELDS, build_case_growth_index +from .collector import ROW_FIELDS, build_case_growth_index, diff_case_growth_snapshot, verify_case_growth_snapshot from .render import render_case_growth_markdown -__all__ = ["ROW_FIELDS", "build_case_growth_index", "render_case_growth_markdown"] +__all__ = [ + "ROW_FIELDS", + "build_case_growth_index", + "diff_case_growth_snapshot", + "render_case_growth_markdown", + "verify_case_growth_snapshot", +] diff --git a/src/hoxline/cli.py b/src/hoxline/cli.py index da7337b..159661a 100644 --- a/src/hoxline/cli.py +++ b/src/hoxline/cli.py @@ -2,10 +2,17 @@ import argparse import json +import os from pathlib import Path import sys +import tempfile -from .case_growth import build_case_growth_index, render_case_growth_markdown +from .case_growth import ( + build_case_growth_index, + diff_case_growth_snapshot, + render_case_growth_markdown, + verify_case_growth_snapshot, +) from .gauntlet import GauntletError, build_full_loop_run, render_markdown, verify_full_loop_run_file from .gauntlet import decide_claim_authority_v1, render_proofcard_v1, summarize_gauntlet_run_v1 from .demo import DemoError, build_demo_run, default_output_dir, render_quickstart_console, verify_demo_run_dir, write_demo_run @@ -57,6 +64,10 @@ def main(argv: list[str] | None = None) -> int: return _verify_review_batch(args) if args.command == "case-growth" and args.case_growth_command == "index": return _run_case_growth_index(args) + if args.command == "case-growth" and args.case_growth_command == "verify": + return _run_case_growth_verify(args) + if args.command == "case-growth" and args.case_growth_command == "diff": + return _run_case_growth_diff(args) parser.print_help() return 2 @@ -75,7 +86,7 @@ def _build_parser() -> argparse.ArgumentParser: run_parser.add_argument("--output", help="optional output file path") metrics_parser = gauntlet_subparsers.add_parser("metrics", help="emit Hoxline Gauntlet work-impact metrics") - metrics_parser.add_argument("--events", required=True, help="synthetic events fixture path") + metrics_parser.add_argument("--events", required=True, help="controlled-test events fixture path") metrics_parser.add_argument("--artifact", required=True, help="sample artifact JSON path") metrics_parser.add_argument("--proofcard", required=True, help="sample ProofCard JSON path") metrics_parser.add_argument("--claim-output", required=True, help="sample Claim Authority output JSON path") @@ -149,6 +160,18 @@ def _build_parser() -> argparse.ArgumentParser: case_growth_index_parser.add_argument("--repo-root", required=True, help="HawkinsOperations local org repo root") case_growth_index_parser.add_argument("--format", choices=("json", "markdown"), default="json", help="output format") case_growth_index_parser.add_argument("--output", help="optional output path") + case_growth_index_parser.add_argument( + "--paired-output-base", + help="write a content-bound JSON/Markdown pair using this path without a suffix", + ) + case_growth_verify_parser = case_growth_subparsers.add_parser("verify", help="fail closed when a snapshot drifts from current authority") + case_growth_verify_parser.add_argument("--repo-root", required=True, help="HawkinsOperations local org repo root") + case_growth_verify_parser.add_argument("--snapshot", required=True, help="checked-in Case Growth snapshot JSON") + case_growth_verify_parser.add_argument("--format", choices=("json", "text"), default="json", help="output format") + case_growth_diff_parser = case_growth_subparsers.add_parser("diff", help="show source-owned snapshot drift") + case_growth_diff_parser.add_argument("--repo-root", required=True, help="HawkinsOperations local org repo root") + case_growth_diff_parser.add_argument("--snapshot", required=True, help="checked-in Case Growth snapshot JSON") + case_growth_diff_parser.add_argument("--format", choices=("json", "markdown"), default="json", help="output format") return parser def _add_demo_run_args(parser: argparse.ArgumentParser) -> None: @@ -225,10 +248,16 @@ def _run_case_growth_index(args: argparse.Namespace) -> int: print(f"Hoxline Case Growth Index: error: {exc}", file=sys.stderr) return 2 - if args.format == "json": - output = json.dumps(index, indent=2) + "\n" - else: - output = render_case_growth_markdown(index) + json_output = json.dumps(index, indent=2) + "\n" + markdown_output = render_case_growth_markdown(index) + output = json_output if args.format == "json" else markdown_output + + if args.paired_output_base: + try: + _write_case_growth_pair(Path(args.paired_output_base), json_output, markdown_output) + except OSError as exc: + print(f"Hoxline Case Growth Index: paired output error: {exc}", file=sys.stderr) + return 2 if args.output: output_path = Path(args.output) @@ -239,6 +268,83 @@ def _run_case_growth_index(args: argparse.Namespace) -> int: return 0 +def _write_case_growth_pair(base_path: Path, json_output: str, markdown_output: str) -> None: + if base_path.suffix: + raise OSError("paired output base must not include a suffix") + base_path.parent.mkdir(parents=True, exist_ok=True) + targets = ((base_path.with_suffix(".json"), json_output), (base_path.with_suffix(".md"), markdown_output)) + temporary: list[tuple[Path, Path]] = [] + try: + for target, content in targets: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + newline="\n", + dir=target.parent, + prefix=f".{target.name}.", + suffix=".tmp", + delete=False, + ) as handle: + handle.write(content) + temporary.append((Path(handle.name), target)) + for temp_path, target in temporary: + os.replace(temp_path, target) + finally: + for temp_path, _ in temporary: + if temp_path.exists(): + temp_path.unlink() + + +def _run_case_growth_verify(args: argparse.Namespace) -> int: + try: + snapshot_path = Path(args.snapshot) + snapshot = _load_json(snapshot_path) + errors, current = verify_case_growth_snapshot(Path(args.repo_root), snapshot) + markdown_path = snapshot_path.with_suffix(".md") + if not markdown_path.is_file(): + errors.append(f"paired Case Growth Markdown is missing: {markdown_path.name}") + elif markdown_path.read_text(encoding="utf-8") != render_case_growth_markdown(snapshot): + errors.append("paired Case Growth Markdown is not the exact render of the checked JSON") + except (OSError, ValueError) as exc: + print(f"Hoxline Case Growth verify: error: {exc}", file=sys.stderr) + return 2 + payload = { + "status": "FAIL" if errors else "PASS", + "error_count": len(errors), + "errors": errors, + "current_reproducibility_sha256": current["reproducibility_sha256"], + "next_legal_action": errors[0] if errors else "none; snapshot converges with current authority", + } + if args.format == "json": + print(json.dumps(payload, indent=2, sort_keys=True)) + else: + print(f"Hoxline Case Growth verify: {payload['status']}") + for error in errors: + print(f"- {error}") + return 1 if errors else 0 + + +def _run_case_growth_diff(args: argparse.Namespace) -> int: + try: + snapshot = _load_json(Path(args.snapshot)) + report = diff_case_growth_snapshot(Path(args.repo_root), snapshot) + except (OSError, ValueError) as exc: + print(f"Hoxline Case Growth diff: error: {exc}", file=sys.stderr) + return 2 + if args.format == "json": + print(json.dumps(report, indent=2, sort_keys=True)) + else: + lines = ["# Hoxline Case Growth Diff v1", "", f"Historical snapshot: `{str(report['historical_snapshot']).lower()}`", ""] + lines.extend( + f"- `{item['field']}`: `{item['before']}` -> `{item['after']}`; owner `{item['source_owner']}`; " + f"path `{item['source_path']}`; `{item['classification']}`; next: {item['next_remediation']}" + for item in report["changes"] + ) + lines.extend(["", f"Next legal action: {report['next_legal_action']}"]) + print("\n".join(lines)) + return 0 + + def _verify_gauntlet(args: argparse.Namespace) -> int: try: errors = verify_full_loop_run_file(Path(args.input), Path(args.schema)) @@ -428,10 +534,10 @@ def _build_gauntlet_v0_lab_report(artifact_path: Path) -> dict[str, object]: "scenario": artifact.get("scenario"), "proof_ceiling": proof_ceiling, "stages": [ - {"stage": "AI-assisted security work", "state": "SOURCE_CONTROLLED_SYNTHETIC_DRAFT"}, + {"stage": "AI-assisted security work", "state": "SOURCE_CONTROLLED_TEST_DRAFT"}, {"stage": "Artifact Intake", "state": "ACCEPTED"}, {"stage": "Evidence Graph", "state": "PRESENT"}, - {"stage": "Telemetry Contract Check", "state": "PASSED_SYNTHETIC_CONTRACT"}, + {"stage": "Telemetry Contract Check", "state": "CONTROLLED_TEST_VALIDATED"}, {"stage": "Controlled Validation", "state": "PASSED_CONTROLLED_FIXTURES"}, {"stage": "Runtime Candidate Ledger", "state": "NOT_PROMOTED"}, {"stage": "Signal Observation", "state": "NOT_OBSERVED"}, diff --git a/src/hoxline/demo.py b/src/hoxline/demo.py index f16a20a..ea030d8 100644 --- a/src/hoxline/demo.py +++ b/src/hoxline/demo.py @@ -13,7 +13,7 @@ SCHEMA_VERSION = "hoxline-demo-run-v0" DEMO_ID = "hoxline-one-command-reviewer-demo-v0" ARTIFACT_ID = "HO-DET-010" -ARTIFACT_TYPE = "synthetic-local-admin-membership-change-detection" +ARTIFACT_TYPE = "controlled-test-local-admin-membership-change-detection" PROOF_CEILING = "CONTROLLED_FIXTURE_VALIDATED" PUBLIC_SAFE_STATUS = "NOT_PUBLIC_SAFE" SAFE_ALLOWED_CLAIM = ( @@ -45,7 +45,7 @@ "evidence-graph.json", "telemetry-contract-check.json", "validation-result.json", - "synthetic-signal.json", + "controlled-test-signal.json", "enrichment.json", "triage-summary.md", "proofcard.json", @@ -102,7 +102,12 @@ def build_demo_run( fixture_path: Path | None = None, negative_fixture_path: Path | None = None, ) -> dict[str, Any]: - root = repo_root or Path(__file__).resolve().parents[2] + if repo_root is not None: + root = repo_root.resolve() + elif (Path.cwd() / "examples" / "demo").is_dir(): + root = Path.cwd().resolve() + else: + root = Path(__file__).resolve().parents[2] fixture = _load_json(fixture_path or root / "examples" / "demo" / "ho-det-010-safe-fixture.json") negative_fixture = _load_json( negative_fixture_path or root / "examples" / "demo" / "ho-det-010-safe-negative-fixture.json" @@ -113,7 +118,7 @@ def build_demo_run( intake = _artifact_intake() telemetry = _telemetry_contract_check(fixture) validation = _controlled_validation(fixture, negative_fixture, telemetry) - signal = _synthetic_signal(fixture, validation) + signal = _controlled_test_signal(fixture, validation) enrichment = _enrichment(fixture) triage = _triage(signal, enrichment, validation) proofcard = _proofcard(intake, telemetry, validation, signal, enrichment, triage) @@ -124,7 +129,7 @@ def build_demo_run( "evidence_graph": evidence_graph, "telemetry_contract_check": telemetry, "validation_result": validation, - "synthetic_signal": signal, + "controlled_test_signal": signal, "enrichment": enrichment, "triage_summary": _triage_markdown(triage), "proofcard": proofcard, @@ -148,7 +153,7 @@ def write_demo_run(output_dir: Path, run: dict[str, Any], force: bool = False) - "evidence-graph.json": run["evidence_graph"], "telemetry-contract-check.json": run["telemetry_contract_check"], "validation-result.json": run["validation_result"], - "synthetic-signal.json": run["synthetic_signal"], + "controlled-test-signal.json": run["controlled_test_signal"], "enrichment.json": run["enrichment"], "triage-summary.md": run["triage_summary"], "proofcard.json": run["proofcard"], @@ -180,7 +185,7 @@ def verify_demo_run_dir(input_path: Path) -> list[str]: claim_authority = _load_json(run_dir / "claim-authority.json") telemetry = _load_json(run_dir / "telemetry-contract-check.json") validation = _load_json(run_dir / "validation-result.json") - signal = _load_json(run_dir / "synthetic-signal.json") + signal = _load_json(run_dir / "controlled-test-signal.json") reviewer_pack = (run_dir / "reviewer-pack.md").read_text(encoding="utf-8") except (OSError, DemoError) as exc: return [str(exc)] @@ -212,7 +217,7 @@ def verify_demo_run_dir(input_path: Path) -> list[str]: if validation.get("result") != "pass" or validation.get("endpoint_mutation") is not False: errors.append("validation must pass without endpoint mutation") if signal.get("detection_fired") is not True or signal.get("source") != "safe bundled fixture": - errors.append("synthetic signal must fire from safe bundled fixture only") + errors.append("controlled-test signal must fire from safe bundled fixture only") if proofcard.get("public_safe_status") != PUBLIC_SAFE_STATUS: errors.append("ProofCard must keep public_safe_status NOT_PUBLIC_SAFE") if proofcard.get("human_review_required") is not True: @@ -243,7 +248,7 @@ def render_quickstart_console(output_dir: Path, run: dict[str, Any]) -> str: "2. Evidence graph linked.", "3. Telemetry contract checked.", "4. Controlled validation passed using bundled fixture.", - "5. Safe synthetic signal/detection event fired.", + "5. Safe controlled-test signal/detection event fired.", "6. Enrichment attached ATT&CK / source / field mapping.", "7. Triage summary generated.", "8. ProofCard rendered.", @@ -265,14 +270,14 @@ def _artifact_intake() -> dict[str, Any]: "artifact_id": ARTIFACT_ID, "artifact_type": ARTIFACT_TYPE, "source_owner": "hawkinsoperations-detections", - "source_label": "synthetic demo fixture for local Administrators membership change logic", + "source_label": "controlled-test demo fixture for local Administrators membership change logic", "ai_assisted": True, "initial_claim_ceiling": PROOF_CEILING, "public_safe_status": PUBLIC_SAFE_STATUS, "human_review_required": True, "ai_disposition_authority": False, "notes": [ - "Fixture is synthetic and local-only.", + "Fixture is controlled-test and local-only.", "No users, groups, endpoints, Wazuh systems, or private infrastructure are touched.", ], } @@ -313,15 +318,15 @@ def _controlled_validation(fixture: dict[str, Any], negative_fixture: dict[str, "endpoint_mutation": False, "runtime_rerun": False, "wazuh_mutation": False, - "explanation": "Validation evaluates bundled synthetic fixture records only.", + "explanation": "Validation evaluates bundled controlled-test fixture records only.", } -def _synthetic_signal(fixture: dict[str, Any], validation: dict[str, Any]) -> dict[str, Any]: +def _controlled_test_signal(fixture: dict[str, Any], validation: dict[str, Any]) -> dict[str, Any]: return { - "schema_version": "synthetic-signal-v0", + "schema_version": "controlled-test-signal-v0", "artifact_id": ARTIFACT_ID, - "signal_id": "synthetic-signal-ho-det-010-demo-v0", + "signal_id": "controlled-test-signal-ho-det-010-demo-v0", "source": "safe bundled fixture", "detection_fired": validation["result"] == "pass" and _fixture_matches_detection(fixture), "simulation_only": True, @@ -344,8 +349,8 @@ def _enrichment(fixture: dict[str, Any]) -> dict[str, Any]: "4733": "member removed from local group", "4738": "user account changed", }, - "source_mapping": {"channel": "Windows Security EventChannel", "fixture_host": fixture["host"], "fixture_scope": "synthetic demo host"}, - "field_mapping": {"event_id": "event identifier", "target_account": "account under review", "group_name": "local group name", "action": "membership or account action", "actor": "synthetic actor label"}, + "source_mapping": {"channel": "Windows Security EventChannel", "fixture_host": fixture["host"], "fixture_scope": "controlled-test demo host"}, + "field_mapping": {"event_id": "event identifier", "target_account": "account under review", "group_name": "local group name", "action": "membership or account action", "actor": "controlled-test actor label"}, "confidence": "bounded-demo-high", "severity": "medium", } @@ -355,9 +360,9 @@ def _triage(signal: dict[str, Any], enrichment: dict[str, Any], validation: dict return { "schema_version": "triage-summary-v0", "artifact_id": ARTIFACT_ID, - "what_happened": "A synthetic fixture represented a local Administrators membership change pattern.", + "what_happened": "A controlled-test fixture represented a local Administrators membership change pattern.", "why_it_matters": "Unexpected local administrator membership changes can indicate account or privilege manipulation.", - "evidence_exists": ["artifact intake record", "evidence graph", "telemetry contract check", "positive and negative synthetic fixtures", "fixture-derived synthetic signal", "enrichment mapping", "ProofCard", "Claim Authority decision"], + "evidence_exists": ["artifact intake record", "evidence graph", "telemetry contract check", "positive and negative controlled-test fixtures", "fixture-derived controlled-test signal", "enrichment mapping", "ProofCard", "Claim Authority decision"], "missing_evidence": ["public-safe runtime proof", "public signal proof", "human review gate completion", "final authorization record"], "next_gate": "human_review_gate", "detection_fired": signal["detection_fired"], @@ -377,10 +382,10 @@ def _proofcard(intake: dict[str, Any], telemetry: dict[str, Any], validation: di "proof_ceiling_meaning": "LOCAL_FIXTURE_DEMONSTRATION_ONLY", "review_lane": "ONE_COMMAND_REVIEWER_DEMO_V0", "review_version": "v0", - "owner_split": {"source_truth": "hawkinsoperations-detections", "behavior_truth": "bundled synthetic fixture", "platform_runtime_truth": "not asserted", "proof_authority": "not asserted by demo", "rendering": "local generated files only"}, + "owner_split": {"source_truth": "hawkinsoperations-detections", "behavior_truth": "bundled controlled-test fixture", "platform_runtime_truth": "not asserted", "proof_authority": "not asserted by demo", "rendering": "local generated files only"}, "telemetry_contract": telemetry, "controlled_validation": validation, - "synthetic_signal": signal, + "controlled_test_signal": signal, "enrichment": enrichment, "triage": triage, "allowed_claims": [SAFE_ALLOWED_CLAIM], @@ -415,7 +420,7 @@ def _evidence_graph(intake: dict[str, Any], telemetry: dict[str, Any], validatio _node("artifact-intake", "artifact_intake", intake["source_owner"], "PASS"), _node("telemetry-contract-check", "telemetry_contract_check", "hoxline-demo-fixture", telemetry["result"].upper()), _node("controlled-validation", "controlled_validation", "hoxline-demo-fixture", validation["result"].upper()), - _node("synthetic-signal", "synthetic_signal", "hoxline-demo-fixture", "PASS"), + _node("controlled-test-signal", "controlled_test_signal", "hoxline-demo-fixture", "PASS"), _node("proofcard", "proofcard", proofcard["proof_owner"], "PASS"), _node("claim-authority", "claim_authority", "hoxline", "PASS"), ] @@ -428,8 +433,8 @@ def _evidence_graph(intake: dict[str, Any], telemetry: dict[str, Any], validatio "edges": [ {"from": "artifact-intake", "to": "telemetry-contract-check", "relationship": "declares assumptions"}, {"from": "telemetry-contract-check", "to": "controlled-validation", "relationship": "bounds fixture validation"}, - {"from": "controlled-validation", "to": "synthetic-signal", "relationship": "creates fixture-only signal"}, - {"from": "synthetic-signal", "to": "proofcard", "relationship": "summarized by"}, + {"from": "controlled-validation", "to": "controlled-test-signal", "relationship": "creates fixture-only signal"}, + {"from": "controlled-test-signal", "to": "proofcard", "relationship": "summarized by"}, {"from": "proofcard", "to": "claim-authority", "relationship": "constrains claims"}, ], "missing_evidence": proofcard["missing_evidence"], @@ -456,14 +461,14 @@ def _run_summary(intake: dict[str, Any], evidence_graph: dict[str, Any], telemet "public_proof_promoted": False, "lifetime_ledger_changed": False, "website_rendering_is_proof": False, - "stage_results": {"intake": intake["intake_id"], "evidence_graph": evidence_graph["graph_id"], "telemetry_contract_check": telemetry["result"], "controlled_validation": validation["result"], "synthetic_signal": signal["detection_fired"], "enrichment": enrichment["confidence"], "triage": triage["next_gate"], "proofcard": proofcard["proofcard_id"], "claim_authority": claim_authority["decision_id"]}, + "stage_results": {"intake": intake["intake_id"], "evidence_graph": evidence_graph["graph_id"], "telemetry_contract_check": telemetry["result"], "controlled_validation": validation["result"], "controlled_test_signal": signal["detection_fired"], "enrichment": enrichment["confidence"], "triage": triage["next_gate"], "proofcard": proofcard["proofcard_id"], "claim_authority": claim_authority["decision_id"]}, } def _reviewer_pack(proofcard: dict[str, Any], claim_authority: dict[str, Any], triage: dict[str, Any]) -> str: lines = [ "# Hoxline One-Command Reviewer Demo v0", "", f"Product: {PRODUCT}", "", f"Doctrine: {DOCTRINE}", "", f"Artifact: `{ARTIFACT_ID}`", "", "## 30-Second Talk Track", "", TALK_TRACK, "", - "## What This Proves", "", "- A reviewer can run Hoxline locally against bundled synthetic fixtures.", "- Hoxline can produce intake, graph, telemetry, validation, signal simulation, enrichment, triage, ProofCard, Claim Authority, and reviewer-pack outputs.", "- Claim Authority allows only bounded demo wording and blocks stronger public claims.", "", + "## What This Proves", "", "- A reviewer can run Hoxline locally against bundled controlled-test fixtures.", "- Hoxline can produce intake, graph, telemetry, validation, signal simulation, enrichment, triage, ProofCard, Claim Authority, and reviewer-pack outputs.", "- Claim Authority allows only bounded demo wording and blocks stronger public claims.", "", "## What This Does Not Prove", "", "- It does not prove live runtime behavior.", "- It does not prove public signal observation.", "- It does not prove public-safe status, production readiness, deployment, approval, authorization, or case closure.", "- It does not touch endpoints, users, groups, Wazuh, Splunk, Cribl, private infrastructure, or ledgers.", "", "## Triage", "", f"- What happened: {triage['what_happened']}", f"- Why it matters: {triage['why_it_matters']}", f"- Next gate: `{triage['next_gate']}`", "", "## Allowed Claim", "", f"- {claim_authority['allowed_claims'][0]}", "", "## Blocked Claims", "", ] @@ -537,7 +542,7 @@ def _node(node_id: str, node_type: str, owner: str, status: str) -> dict[str, st def _validate_fixture(fixture: dict[str, Any], expected_detection: bool) -> None: - expected = {"schema_version": "hoxline-demo-fixture-v0", "artifact_id": ARTIFACT_ID, "fixture_kind": "synthetic-demo-only", "safe_fixture": True, "endpoint_mutation": False, "runtime_required": False, "network_required": False} + expected = {"schema_version": "hoxline-demo-fixture-v0", "artifact_id": ARTIFACT_ID, "fixture_kind": "controlled-test-demo-only", "safe_fixture": True, "endpoint_mutation": False, "runtime_required": False, "network_required": False} for field, value in expected.items(): if fixture.get(field) != value: raise DemoError(f"fixture field {field} must be {value!r}") diff --git a/src/hoxline/metrics/__init__.py b/src/hoxline/metrics/__init__.py index cf75a7d..047db9a 100644 --- a/src/hoxline/metrics/__init__.py +++ b/src/hoxline/metrics/__init__.py @@ -1,11 +1,11 @@ from __future__ import annotations -from .evaluator import DetectionMetrics, evaluate_detection_fixture, load_synthetic_events +from .evaluator import DetectionMetrics, evaluate_detection_fixture, load_controlled_test_events from .report import build_work_impact_report __all__ = [ "DetectionMetrics", "build_work_impact_report", "evaluate_detection_fixture", - "load_synthetic_events", + "load_controlled_test_events", ] diff --git a/src/hoxline/metrics/evaluator.py b/src/hoxline/metrics/evaluator.py index 90706f2..c146f5c 100644 --- a/src/hoxline/metrics/evaluator.py +++ b/src/hoxline/metrics/evaluator.py @@ -25,7 +25,7 @@ "edge.exe", "firefox.exe", "msedge.exe", - "synthetic_browser.exe", + "controlled_test_browser.exe", } SCRIPT_INTERPRETER_NAMES = { @@ -43,8 +43,8 @@ "/chrome/user data/default/cache/", "\\edge\\user data\\default\\cache\\", "/edge/user data/default/cache/", - "\\firefox\\profiles\\synthetic\\cache2\\", - "/firefox/profiles/synthetic/cache2/", + "\\firefox\\profiles\\controlled-test\\cache2\\", + "/firefox/profiles/controlled-test/cache2/", ) @@ -78,33 +78,33 @@ def as_dict(self) -> dict[str, int | float]: } -def load_synthetic_events(path: str | Path) -> dict[str, Any]: +def load_controlled_test_events(path: str | Path) -> dict[str, Any]: with Path(path).open("r", encoding="utf-8") as handle: data = json.load(handle) if not isinstance(data, dict): - raise ValueError("synthetic event fixture must be a JSON object") - if data.get("schema_version") != "synthetic-events-v0": - raise ValueError("synthetic event fixture schema_version must be synthetic-events-v0") + raise ValueError("controlled-test event fixture must be a JSON object") + if data.get("schema_version") != "controlled-test-events-v0": + raise ValueError("controlled-test event fixture schema_version must be controlled-test-events-v0") if data.get("artifact_id") != "HOX-GAUNTLET-001": - raise ValueError("synthetic event fixture artifact_id must be HOX-GAUNTLET-001") + raise ValueError("controlled-test event fixture artifact_id must be HOX-GAUNTLET-001") events = data.get("events") if not isinstance(events, list): - raise ValueError("synthetic event fixture must include an events list") + raise ValueError("controlled-test event fixture must include an events list") for event in events: _validate_event(event) return data def evaluate_detection_fixture(path: str | Path) -> DetectionMetrics: - fixture = load_synthetic_events(path) + fixture = load_controlled_test_events(path) events = fixture["events"] if not isinstance(events, list): - raise ValueError("synthetic event fixture must include an events list") + raise ValueError("controlled-test event fixture must include an events list") true_positive = true_negative = false_positive = false_negative = 0 for event in events: if not isinstance(event, dict): - raise ValueError("synthetic event must be an object") + raise ValueError("controlled-test event must be an object") expected = event["expected_detection_match"] is True observed = matches_browser_cache_script_interpreter(event) if expected and observed: @@ -169,10 +169,10 @@ def telemetry_coverage(events: list[dict[str, Any]], required_fields: list[str]) def _validate_event(event: object) -> None: if not isinstance(event, dict): - raise ValueError("synthetic event must be an object") + raise ValueError("controlled-test event must be an object") missing = [field for field in REQUIRED_EVENT_FIELDS if field not in event] if missing: - raise ValueError(f"synthetic event missing required fields: {', '.join(missing)}") + raise ValueError(f"controlled-test event missing required fields: {', '.join(missing)}") if not isinstance(event["expected_detection_match"], bool): raise ValueError("expected_detection_match must be boolean") diff --git a/src/hoxline/metrics/report.py b/src/hoxline/metrics/report.py index f0b8545..f650be2 100644 --- a/src/hoxline/metrics/report.py +++ b/src/hoxline/metrics/report.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any -from .evaluator import evaluate_detection_fixture, load_synthetic_events, telemetry_coverage +from .evaluator import evaluate_detection_fixture, load_controlled_test_events, telemetry_coverage PROOF_CEILING = "CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY" @@ -40,10 +40,10 @@ def build_work_impact_report( artifact = _load_json_object(artifact_path) proofcard = _load_json_object(proofcard_path) claim_output = _load_json_object(claim_output_path) - events_fixture = load_synthetic_events(events_path) + events_fixture = load_controlled_test_events(events_path) events = events_fixture["events"] if not isinstance(events, list): - raise ValueError("synthetic event fixture must include an events list") + raise ValueError("controlled-test event fixture must include an events list") _require(artifact, "artifact_id", "HOX-GAUNTLET-001") _require(artifact, "proof_ceiling", PROOF_CEILING) diff --git a/src/hoxline/review_engine.py b/src/hoxline/review_engine.py index 4ef2a27..66debef 100644 --- a/src/hoxline/review_engine.py +++ b/src/hoxline/review_engine.py @@ -2,12 +2,22 @@ from copy import deepcopy from datetime import datetime, timezone +import base64 +import binascii +import hashlib import json -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath import re import shutil +import subprocess from typing import Any +import unicodedata +from urllib.parse import unquote, urlsplit +import yaml + +from .case_growth.collector import verify_selected_source_checkout +from .case_growth.discovery import repo_origin, sanitized_git_env from .demo import ( BLOCKED_CLAIM_FAMILIES, PRODUCT, @@ -24,13 +34,14 @@ BATCH_INDEX_VERSION = "multi-artifact-review-index-v1" BATCH_MACHINE_STATE_VERSION = "batch-machine-state-v1" ARTIFACT_ID = "HO-DET-010" +ARTIFACT_ID_PATTERN = re.compile(r"^(?:HO-DET|HO-NDR|ID-DET|AWS-DET)-\d{3}$") EXPECTED_PASS_OUTPUTS = [ "artifact-manifest.json", "intake.json", "evidence-graph.json", "telemetry-contract-check.json", "validation-result.json", - "synthetic-signal.json", + "controlled-test-signal.json", "enrichment.json", "triage-summary.md", "proofcard.json", @@ -53,7 +64,7 @@ "evidence_graph", "telemetry_contract_check", "controlled_validation", - "synthetic_signal", + "controlled_test_signal", "enrichment", "triage", "proofcard", @@ -89,14 +100,35 @@ ] PROHIBITED_CLAIM_PATTERNS = { "public-safe runtime proof": re.compile(r"public[- ]safe runtime proof", re.IGNORECASE), + "public-safe promotion": re.compile( + r"\bpublic[-_ ]safe(?:[-_ ](?:is[-_ ])?(?:approved|confirmed|promotion|promoted|proof|status|true))\b", + re.IGNORECASE, + ), + "runtime promotion": re.compile( + r"\bruntime(?:[-_ ]active|\s+(?:is|was)\s+active)\b", + re.IGNORECASE, + ), + "signal promotion": re.compile( + r"\bsignal(?:[-_ ](?:was[-_ ]?)?observed|[-_ ]proof|\s+(?:is|was)\s+observed)\b", + re.IGNORECASE, + ), "production": re.compile(r"\bproduction(?:[- ]ready| readiness)?\b", re.IGNORECASE), "customer deployment": re.compile(r"\bcustomer(?:[- ]deployed| deployment)?\b", re.IGNORECASE), "SOCaaS deployment": re.compile(r"\bSOCaaS(?:[- ]ready| deployed| deployment)?\b", re.IGNORECASE), "autonomous SOC": re.compile(r"\bautonomous SOC\b", re.IGNORECASE), - "AI-approved disposition": re.compile(r"\bAI[- ]approved\b", re.IGNORECASE), - "analyst-approved disposition": re.compile(r"\banalyst[- ]approved\b", re.IGNORECASE), - "final authorization": re.compile(r"\bfinal authorization\b", re.IGNORECASE), - "case closure": re.compile(r"\bcase closure\b|\bcase[- ]closed\b", re.IGNORECASE), + "AI-approved disposition": re.compile( + r"\bAI(?:[-_ ]approved|[-_ ](?:authority|disposition)(?:[-_ ](?:enabled|approved|true))?)\b", + re.IGNORECASE, + ), + "analyst-approved disposition": re.compile( + r"\banalyst(?:[-_ ]approved|[-_ ]approval(?:[-_ ](?:granted|approved|true))?)\b", + re.IGNORECASE, + ), + "final authorization": re.compile(r"\bfinal[-_ ]authorization\b", re.IGNORECASE), + "case closure": re.compile(r"\bcase[-_ ]closure\b|\bcase[-_ ]closed\b", re.IGNORECASE), + "live cloud claim": re.compile(r"\blive (?:AWS|cloud)(?: runtime| proof| signal)?\b", re.IGNORECASE), + "live identity runtime claim": re.compile(r"\blive (?:IdP|identity)(?: runtime| proof| signal)?\b", re.IGNORECASE), + "live Security Onion proof": re.compile(r"\blive Security Onion(?: proof| signal| runtime)?\b", re.IGNORECASE), } PRIVATE_FIELD_PATTERNS = [ re.compile(pattern, re.IGNORECASE) @@ -126,6 +158,267 @@ r"\bprivate execution ID\b", ) ] +PRIVATE_KEY_TOKENS = { + "customeridentifier", + "endpointlog", + "generatedpassword", + "password", + "privateevidence", + "privateexecutionid", + "privatepacket", + "privatepayload", + "rawalert", + "rawwazuh", + "secret", + "token", +} +RETIRED_VOCABULARY = "syn" + "thetic" +TRACKED_TEXT_EXTENSIONS = { + ".cfg", ".css", ".csv", ".html", ".ini", ".js", ".json", ".md", ".mjs", + ".ps1", ".py", ".rst", ".sh", ".toml", ".ts", ".tsx", ".txt", ".yaml", ".yml", +} +TRACKED_BINARY_EXTENSIONS = {".gif", ".ico", ".jpeg", ".jpg", ".pdf", ".png", ".webp"} +TRACKED_TEXT_FILENAMES = { + ".editorconfig", ".gitattributes", ".gitignore", "LICENSE", "MANIFEST.in", "Makefile", +} +PASS_OUTPUT_ROLES = { + "artifact_manifest": "artifact-manifest.json", + "artifact_intake": "intake.json", + "evidence_graph": "evidence-graph.json", + "telemetry_contract_check": "telemetry-contract-check.json", + "controlled_validation": "validation-result.json", + "controlled_test_signal": "controlled-test-signal.json", + "enrichment": "enrichment.json", + "triage": "triage-summary.md", + "proofcard": "proofcard.json", + "proofcard_markdown": "proofcard.md", + "claim_authority": "claim-authority.json", + "reviewer_pack": "reviewer-pack.md", + "machine_state": "machine-state.json", + "run_summary": "run-summary.json", +} +BLOCKED_OUTPUT_ROLES = { + "artifact_manifest": "artifact-manifest.json", + "machine_state": "machine-state.json", + "blocked_review": "blocked-review.md", + "run_summary": "run-summary.json", +} +BATCH_OUTPUT_ROLES = { + "input_index": "input-index.json", + "machine_state": "batch-machine-state.json", + "summary": "batch-summary.md", + "reviewer_pack": "batch-reviewer-pack.md", + "run_summary": "batch-run-summary.json", +} +ABSOLUTE_LOCAL_PATH = re.compile( + r"(?i)(?:[A-Z]:[\\/]|(?]+)" +) +CANONICAL_ORIGINS = { + "hawkinsoperations-detections": "https://github.com/HawkinsOperations/hawkinsoperations-detections", + "hawkinsoperations-validation": "https://github.com/HawkinsOperations/hawkinsoperations-validation", +} +MANIFEST_ALLOWED_FIELDS = set(REQUIRED_MANIFEST_FIELDS) | { + "additional_telemetry_sources", + "attack_mapping", + "confidence", + "detection_family", + "endpoint_mutation", + "expected_block_reason", + "expected_event_keys", + "expected_review_outcome", + "field_mapping", + "lifetime_ledger_changed", + "public_proof_promoted", + "runtime_proof", + "severity", + "triage_what_happened", + "triage_why_it_matters", + "wazuh_mutation", +} +TELEMETRY_CONTRACT_ALLOWED_FIELDS = { + "event_ids", + "event_key_field", + "event_keys", + "required_fields", + "scope", + "source", + "source_control_note", + "wazuh_rule_ids", +} +BATCH_INDEX_ALLOWED_FIELDS = { + "index_version", + "index_id", + "description", + "artifacts", + "expected_pass_artifacts", + "expected_blocked_artifacts", + "batch_claim_boundary", + "public_safe_status", + "human_review_required", + "ai_disposition_authority", + "runtime_boundary", + "signal_boundary", + "proof_boundary", + "generated_outputs", + "next_gate", +} +FIXTURE_ALLOWED_FIELDS = { + "artifact_id", + "endpoint_mutation", + "description", + "events", + "expected_detection", + "fixture_id", + "fixture_kind", + "host", + "network_required", + "runtime_required", + "safe_fixture", + "schema_version", +} +SECURITY_FALSE_FIELDS = { + "ai_disposition_authority", + "analyst_disposition_authority", + "analyst_approval", + "case_closed", + "case_closure", + "endpoint_mutation", + "final_authorization", + "lifetime_ledger_changed", + "private_evidence_committed", + "public_proof_promoted", + "public_safe", + "runtime_active", + "runtime_proof", + "signal_observed", + "wazuh_mutation", +} +SECURITY_FALSE_KEY_TOKENS = { + re.sub(r"[^a-z0-9]", "", value.casefold()) for value in SECURITY_FALSE_FIELDS +} | { + "aiapproved", + "aiapproval", + "analystapproved", + "approvedbyanalyst", + "caseclosureapproved", + "finalapproved", + "publicsafeapproved", +} +SECURITY_FIXED_FIELDS: dict[str, Any] = { + "public_safe_status": PUBLIC_SAFE_STATUS, + "human_review_required": True, + "ai_disposition_authority": False, +} +REVIEW_OUTPUT_SECURITY_FIELDS: dict[str, Any] = { + **SECURITY_FIXED_FIELDS, + "endpoint_mutation": False, + "wazuh_mutation": False, + "runtime_proof": False, + "public_proof_promoted": False, + "lifetime_ledger_changed": False, + "private_evidence_committed": False, +} +COMPOSITIONAL_PROMOTION_KEYS = { + "aidisposition", + "aiauthority", + "analystapproval", + "analystauthority", + "productionlive", + "productionstatus", + "customerdeployment", + "customerstatus", + "socaasdeployment", + "socaasstatus", + "runtimestatus", + "signalstatus", + "approvalstatus", + "closurestatus", + "casestatus", + "publicsaferuntime", + "finalauthorized", + "finalauthorizationstatus", +} +BOUNDED_PROMOTION_VALUES = { + "blocked", + "controlledtestonly", + "false", + "notactive", + "notapproved", + "notauthorized", + "notclosed", + "notdeployed", + "notobserved", + "notpublicsafe", + "notready", + "unsupported", +} +REVIEW_MACHINE_STATE_BASE_FIELDS = { + "schema_version", + "engine_version", + "run_id", + "artifact_id", + "manifest_path", + "final_status", + "block_reason", + "stages", + "allowed_claim", + "requested_claims", + "blocked_claims", + "public_safe_status", + "human_review_required", + "ai_disposition_authority", + "endpoint_mutation", + "wazuh_mutation", + "runtime_proof", + "private_evidence_committed", + "public_proof_promoted", + "lifetime_ledger_changed", + "proof_boundary", + "runtime_boundary", + "signal_boundary", + "next_gate", + "outputs", + "product", + "output_digests", + "state_integrity_digest", +} +BATCH_MACHINE_STATE_FIELDS = { + "schema_version", + "engine_version", + "batch_id", + "index_id", + "index_path", + "artifacts", + "expected_pass_artifacts", + "expected_blocked_artifacts", + "final_status", + "actual_pass_artifacts", + "actual_blocked_artifacts", + "block_reason", + "batch_claim_boundary", + "proof_boundary", + "runtime_boundary", + "signal_boundary", + "public_safe_status", + "human_review_required", + "ai_disposition_authority", + "endpoint_mutation", + "wazuh_mutation", + "runtime_proof", + "private_evidence_committed", + "public_proof_promoted", + "lifetime_ledger_changed", + "website_changed", + "outputs", + "next_gate", + "product", + "source_manifest_digest", + "input_index_sha256", + "output_digests", + "batch_state_integrity_digest", +} +_AUTHORITY_BINDING_CACHE: dict[tuple[str, ...], dict[str, Any]] = {} class ReviewEngineError(ValueError): @@ -136,6 +429,624 @@ class ReviewBlocked(ReviewEngineError): """Raised for governed BLOCKED review outcomes.""" +class _UniqueKeyLoader(yaml.SafeLoader): + """Safe YAML loader that rejects exact and case-folded duplicate keys.""" + + +def _construct_unique_mapping(loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False) -> dict[Any, Any]: + pairs = loader.construct_pairs(node, deep=deep) + result: dict[Any, Any] = {} + seen: set[str] = set() + for key, value in pairs: + normalized = _normalized_key_identity(key) + if normalized in seen: + raise ReviewBlocked("structured input contains a duplicate key") + seen.add(normalized) + result[key] = value + return result + + +_UniqueKeyLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + _construct_unique_mapping, +) + + +def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + seen: set[str] = set() + for key, value in pairs: + normalized = _normalized_key_identity(key) + if normalized in seen: + raise ReviewBlocked("structured input contains a duplicate key") + seen.add(normalized) + result[key] = value + return result + + +def _require_exact_keys(value: dict[str, Any], allowed: set[str], label: str) -> None: + unknown = sorted(set(value) - allowed) + if unknown: + raise ReviewBlocked(f"{label} contains unsupported fields") + + +def _normalize_security_key(value: Any) -> str: + decoded = _decoded_text_variants( + unicodedata.normalize("NFKC", str(value)), + strict_base64=False, + )[-1] + return re.sub(r"[^a-z0-9]", "", decoded.casefold()) + + +def _normalized_key_identity(value: Any) -> str: + return _normalize_security_key(unicodedata.normalize("NFKC", str(value))) + + +def _security_scan_text(value: str) -> str: + normalized = unicodedata.normalize("NFKD", value) + scanned: list[str] = [] + for character in normalized: + if character in "\t\r\n": + scanned.append(" ") + continue + category = unicodedata.category(character) + if category.startswith(("C", "M")): + continue + scanned.append(character) + return "".join(scanned) + + +def _is_promotion_key(normalized_key: str) -> bool: + if normalized_key == "finalstatus": + return False + if any( + token in normalized_key + for token in SECURITY_FALSE_KEY_TOKENS | COMPOSITIONAL_PROMOTION_KEYS + ): + return True + anchored_prefixes = ("ai", "analyst", "review", "final") + contained_prefixes = ( + "production", "customer", "socaas", "runtime", "signal", "approval", + "closure", "case", "publicsafe", + ) + promotion_indicators = ( + "active", + "approved", + "authority", + "authorization", + "authorized", + "closed", + "decision", + "deployed", + "disposition", + "enabled", + "live", + "observed", + "proof", + "ready", + "state", + "status", + ) + authority_match = any(normalized_key.startswith(prefix) for prefix in anchored_prefixes) or any( + prefix in normalized_key for prefix in contained_prefixes + ) + return authority_match and any( + indicator in normalized_key for indicator in promotion_indicators + ) + + +def _is_promotion_path(path: str) -> bool: + segments = [ + _normalize_security_key(segment) + for segment in re.split(r"\.|\[\d+\]", path) + if segment + ] + if any(_is_promotion_key(segment) for segment in segments): + return True + authority_segments = { + "ai", "analyst", "review", "final", "production", "customer", "socaas", + "runtime", "signal", "approval", "closure", "case", "publicsafe", + } + indicator_segments = { + "active", "approved", "authority", "authorization", "authorized", "closed", + "decision", "deployed", "disposition", "enabled", "live", "observed", + "proof", "ready", "state", "status", + } + return any(segment in authority_segments for segment in segments) and any( + segment in indicator_segments for segment in segments + ) + + +def _is_bounded_promotion_value(value: Any) -> bool: + if value in (False, None, 0, "", [], {}): + return True + if isinstance(value, str): + return _normalize_security_key(value) in BOUNDED_PROMOTION_VALUES + return False + + +def _canonical_base64_text(value: str, *, strict: bool) -> str | None: + compact = value.strip() + if len(compact) < 8 or not re.fullmatch(r"[A-Za-z0-9+/=_-]+", compact): + return None + encoded_signal = "=" in compact or bool( + re.match(r"(?i)^(?:eyj|w3s|y2fz|chvi|chjp|zmlu|yw5h|quk|l3|qzpc)", compact) + ) + unpadded = compact.rstrip("=") + if "=" in unpadded or len(compact) - len(unpadded) > 2: + if strict and encoded_signal: + raise ReviewBlocked("structured input contains non-canonical base64") + return None + remainder = len(unpadded) % 4 + if remainder == 1: + # A one-character remainder can never be valid base64. Limit rejection + # to values that carry strong encoded-data signals so ordinary labels + # are not reclassified as encodings. + if encoded_signal: + if strict: + raise ReviewBlocked("structured input contains invalid base64 length") + return None + padded = unpadded + "=" * ((4 - remainder) % 4) + alphabet = "urlsafe" if re.search(r"[-_]", unpadded) else "standard" + try: + if alphabet == "urlsafe": + decoded_bytes = base64.b64decode(padded.translate(str.maketrans("-_", "+/")), validate=True) + canonical = base64.urlsafe_b64encode(decoded_bytes).decode("ascii").rstrip("=") + else: + decoded_bytes = base64.b64decode(padded, validate=True) + canonical = base64.b64encode(decoded_bytes).decode("ascii").rstrip("=") + except (binascii.Error, ValueError) as exc: + if strict and encoded_signal: + raise ReviewBlocked("structured input contains invalid base64") from exc + return None + if canonical != unpadded: + if strict and encoded_signal: + raise ReviewBlocked("structured input contains non-canonical base64") + return None + try: + decoded = decoded_bytes.decode("utf-8") + except UnicodeDecodeError: + return None + if not decoded or not all(char.isprintable() or char.isspace() for char in decoded): + return None + return decoded + + +def _decoded_text_variants(value: str, *, strict_base64: bool = True) -> list[str]: + initial = _security_scan_text(value) + variants = [initial] + frontier = [initial] + for _ in range(3): + next_frontier: list[str] = [] + for current in frontier: + decoded_url = _security_scan_text(unquote(current)) + if decoded_url != current and decoded_url not in variants: + variants.append(decoded_url) + next_frontier.append(decoded_url) + decoded_b64 = _canonical_base64_text(current, strict=strict_base64) + if decoded_b64 is not None: + decoded_b64 = _security_scan_text(decoded_b64) + if decoded_b64 not in variants: + variants.append(decoded_b64) + next_frontier.append(decoded_b64) + if not next_frontier: + break + frontier = next_frontier + return variants + + +def _string_has_unsafe_claim(value: str) -> bool: + for candidate in _decoded_text_variants(value): + if any(pattern.search(candidate) for pattern in PROHIBITED_CLAIM_PATTERNS.values()): + return True + stripped = candidate.strip() + if stripped.startswith(("{", "[")): + try: + nested = json.loads(stripped, object_pairs_hook=_unique_json_object) + except (json.JSONDecodeError, ReviewEngineError): + continue + try: + _validate_recursive_boundaries(nested, "encoded structured value") + except ReviewBlocked: + return True + return False + + +def _validate_recursive_boundaries(value: Any, label: str, path: str = "") -> None: + if isinstance(value, dict): + for key, item in value.items(): + key_path = f"{path}.{key}" if path else str(key) + normalized_key = _normalize_security_key(key) + canonical_final_status = normalized_key == "finalstatus" and item in { + "PASS", "MIXED", "BLOCKED", + } + if ( + not canonical_final_status + and (_is_promotion_key(normalized_key) or _is_promotion_path(key_path)) + ) and not isinstance(item, (dict, list)) and not _is_bounded_promotion_value(item): + raise ReviewBlocked(f"{label} contains prohibited authority promotion") + for canonical, expected in SECURITY_FIXED_FIELDS.items(): + if normalized_key == _normalize_security_key(canonical) and item != expected: + raise ReviewBlocked(f"{label} violates a fixed authority boundary") + _validate_recursive_boundaries(item, label, key_path) + return + if isinstance(value, list): + for index, item in enumerate(value): + _validate_recursive_boundaries(item, label, f"{path}[{index}]") + return + if isinstance(value, str): + if _is_promotion_path(path) and not _is_bounded_promotion_value(value): + raise ReviewBlocked(f"{label} contains prohibited authority promotion") + _validate_declared_string(value, label) + if _string_has_unsafe_claim(value): + raise ReviewBlocked(f"{label} contains an unsupported claim") + return + if _is_promotion_path(path) and not _is_bounded_promotion_value(value): + raise ReviewBlocked(f"{label} contains prohibited authority promotion") + + +def _is_bounded_negative_claim(value: str, *, path: str = "", label: str = "") -> bool: + normalized_path = _normalized_key_identity(path) + if re.fullmatch( + r"\s*-?\s*public[_ -]?safe[_ -]?status(?:\s+remains|\s*:)?\s*`?NOT_PUBLIC_SAFE`?\.?\s*", + unicodedata.normalize("NFKC", value), + re.IGNORECASE, + ): + return True + blocked_context = any( + token in normalized_path + for token in ( + "blockedclaims", + "blockedclaimclasses", + "missingevidence", + "saferwording", + "whathoxlineblocked", + ) + ) + if normalized_path.endswith( + ("path", "machinestate", "outputdir", "reviewerpack", "blockedreview", "runsummary") + ) and re.fullmatch(r"[A-Za-z0-9._/-]+", value): + return True + + local_negative = re.compile( + r"\b(?:not asserted|not prove|does not prove|not public proof|not runtime proof|" + r"unsupported|prohibited|remain false|remains false|claims block|blocked claim|" + r"claim authority blocks|do not say)\b", + re.IGNORECASE, + ) + inherited_negative = re.compile( + r"\b(?:not asserted|not prove|does not prove|claims block|blocked claim|" + r"claim authority blocks|do not say)\b", + re.IGNORECASE, + ) + trailing_negative = re.compile( + r"^\s*:?\s*(?:is|are|remains?)\s+(?:unsupported|prohibited|false|not asserted|NOT_PUBLIC_SAFE)\b", + re.IGNORECASE, + ) + clause_separator = re.compile( + r"[,;:/\u2013\u2014]|\b(?:while|but|however|and|plus|though)\b", + re.IGNORECASE, + ) + hard_separator = re.compile( + r"[;:/\u2013\u2014]|\b(?:while|but|however|though)\b", + re.IGNORECASE, + ) + authority_subject = ( + r"(?:public[-_ ]safe(?:[-_ ](?:runtime|proof|status))?|runtime|signal|production|" + r"customer(?:[-_ ](?:environment|deployment))?|SOCaaS(?:[-_ ]deployment)?|" + r"AI[-_ ](?:authority|approval|disposition)|analyst[-_ ](?:authority|approval|disposition)|" + r"final[-_ ]authorization|case[-_ ]closure)" + ) + affirmative_predicate = re.compile( + rf"\b{authority_subject}\b" + r"(?:\s+(?:is|are|was|were|has|have|becomes?|became))?\s+" + r"(?:enabled|granted|confirmed|received|approved|active|deployed|live|observed|closed|true)\b", + re.IGNORECASE, + ) + blocked_suffix = re.compile( + r"\b(?:claim|claims|claim classes|claim families|identified claims)\s+" + r"(?:remain|remains|are)\s+(?:blocked|unsupported|prohibited|not asserted)\b", + re.IGNORECASE, + ) + global_negative_predicate = re.compile( + r"\b(?:is|are|remains?)\s+" + r"(?:unsupported|prohibited|false|not asserted|NOT_PUBLIC_SAFE)\b", + re.IGNORECASE, + ) + exact_bounded_nouns = { + *(_security_scan_text(item).strip().casefold() for item in BLOCKED_CLAIM_FAMILIES), + "public signal proof", + "human review gate completion", + "final authorization record", + } + unsafe_seen = False + for candidate in _decoded_text_variants(value): + normalized = unicodedata.normalize("NFKC", candidate) + canonical_value = re.sub( + r"\s+", + " ", + normalized.strip().strip(" \t`*_-.:;"), + ).casefold() + explicit_affirmative = affirmative_predicate.search(normalized) + if blocked_context and canonical_value in exact_bounded_nouns: + return True + if blocked_context and ":" in normalized: + claim_label, bounded_explanation = normalized.split(":", 1) + canonical_label = re.sub( + r"\s+", + " ", + claim_label.strip().strip(" \t`*_-.:;"), + ).casefold() + if ( + canonical_label in exact_bounded_nouns + and global_negative_predicate.search(bounded_explanation) is not None + ): + return True + if explicit_affirmative is not None: + return False + if global_negative_predicate.search(normalized) is not None and any( + pattern.search(normalized) for pattern in PROHIBITED_CLAIM_PATTERNS.values() + ): + return True + if blocked_suffix.search(normalized) is not None and any( + pattern.search(normalized) for pattern in PROHIBITED_CLAIM_PATTERNS.values() + ): + return True + negative_intro = inherited_negative.search(normalized) + if ( + negative_intro is not None + and affirmative_predicate.search(normalized, negative_intro.end()) is not None + ): + # A denial governs only the authority nouns it directly scopes. Any + # later explicit affirmative authority predicate is a new claim, + # regardless of the connector used to join the clauses. + return False + for pattern in PROHIBITED_CLAIM_PATTERNS.values(): + for match in pattern.finditer(normalized): + unsafe_seen = True + normalized_folded = normalized.casefold() + token_start = normalized_folded.rfind("not_public_safe", 0, match.start() + 1) + if token_start >= 0 and token_start <= match.start() < token_start + len("not_public_safe"): + continue + separators = [item for item in clause_separator.finditer(normalized) if item.end() <= match.start()] + clause_start = separators[-1].end() if separators else 0 + prefix = normalized[clause_start : match.start()] + suffix = normalized[match.end() :] + if local_negative.search(prefix) or trailing_negative.search(suffix): + continue + # A scoped denial such as "does not prove production readiness, + # deployment, or approval" can govern a comma-delimited list. + # Weak phrases such as "unsupported note" do not carry across + # punctuation, and hard delimiters always reset the scope. + if separators and separators[-1].group().casefold() in {",", "and", "plus"}: + hard = [item for item in hard_separator.finditer(normalized) if item.end() <= match.start()] + hard_start = hard[-1].end() if hard else 0 + next_separator = next( + ( + item + for item in clause_separator.finditer(normalized, match.end()) + if item.start() >= match.end() + ), + None, + ) + fragment_end = next_separator.start() if next_separator else len(normalized) + fragment = normalized[clause_start:fragment_end] + if inherited_negative.search( + normalized[hard_start : match.start()] + ) and not affirmative_predicate.search(fragment): + continue + return False + return unsafe_seen + + +def _validate_generated_output_security(value: Any, label: str, path: str = "") -> None: + if isinstance(value, dict): + seen: set[str] = set() + for key, item in value.items(): + normalized_key = _normalized_key_identity(key) + if normalized_key in seen: + raise ReviewBlocked(f"{label} contains normalized-key collision") + seen.add(normalized_key) + key_path = f"{path}.{key}" if path else str(key) + canonical_final_status = normalized_key == "finalstatus" and item in { + "PASS", "MIXED", "BLOCKED", + } + if ( + not canonical_final_status + and (_is_promotion_key(normalized_key) or _is_promotion_path(key_path)) + ) and not isinstance(item, (dict, list)) and not _is_bounded_promotion_value(item): + raise ReviewBlocked(f"{label} contains prohibited authority promotion") + for canonical, expected in REVIEW_OUTPUT_SECURITY_FIELDS.items(): + if normalized_key == _normalized_key_identity(canonical) and item != expected: + raise ReviewBlocked(f"{label} violates a fixed authority boundary") + _validate_generated_output_security(item, label, key_path) + return + if isinstance(value, list): + for index, item in enumerate(value): + _validate_generated_output_security(item, label, f"{path}[{index}]") + return + if isinstance(value, str): + if _is_promotion_path(path) and not _is_bounded_promotion_value(value): + raise ReviewBlocked(f"{label} contains prohibited authority promotion") + _validate_declared_string(value, label) + value_is_bounded = ( + _string_has_unsafe_claim(value) + and _is_bounded_negative_claim(value, path=path, label=label) + ) + lines = value.splitlines() if "\n" in value or "\r" in value else [value] + markdown_section = "" + for line in lines: + if line.lstrip().startswith("#"): + markdown_section = line.lstrip("#").strip() + elif re.fullmatch(r"[A-Za-z][A-Za-z ]+:", line.strip()): + markdown_section = line.strip().rstrip(":") + line_path = f"{path}.{markdown_section}" if markdown_section else path + if _string_has_unsafe_claim(line) and not _is_bounded_negative_claim( + line, path=line_path, label=label + ) and not value_is_bounded: + raise ReviewBlocked(f"{label} contains an unsupported claim") + return + if _is_promotion_path(path) and not _is_bounded_promotion_value(value): + raise ReviewBlocked(f"{label} contains prohibited authority promotion") + + +def _expected_file_text(value: Any) -> str: + if isinstance(value, str): + return value + return json.dumps(value, indent=2, sort_keys=True) + "\n" + + +def _verify_generated_file(path: Path, expected: Any, label: str, errors: list[str]) -> None: + if not path.is_file() or path.is_symlink(): + return + try: + actual_text = path.read_text(encoding="utf-8") + if actual_text != _expected_file_text(expected): + errors.append(f"{label} does not match deterministic engine output") + if path.suffix == ".json": + actual: Any = json.loads(actual_text, object_pairs_hook=_unique_json_object) + else: + actual = actual_text + _validate_generated_output_security(actual, label) + _validate_no_private_markers(actual, label) + except (OSError, UnicodeError, json.JSONDecodeError, ReviewEngineError) as exc: + errors.append(f"{label} security validation failed: {_sanitize_block_reason(str(exc))}") + + +def _validate_declared_string(value: str, label: str) -> None: + for candidate in _decoded_text_variants(value): + if "\x00" in candidate or any(ord(char) < 32 and char not in "\t\r\n" for char in candidate): + raise ReviewBlocked(f"{label} contains an unsafe encoded value") + if _looks_like_local_or_escaping_path(candidate): + raise ReviewBlocked(f"{label} contains a prohibited local or escaping path") + + +def _looks_like_local_or_escaping_path(value: str) -> bool: + candidate = value.strip() + if not candidate: + return False + if re.match(r"(?i)^file:", candidate): + return True + parsed = urlsplit(candidate) + if parsed.scheme and len(parsed.scheme) > 1 and parsed.scheme.casefold() != "https": + return True + if re.match(r"(?i)^[a-z]:", candidate): + return True + if candidate.startswith(("\\\\", "//", "/", "\\")): + return True + normalized = candidate.replace("\\", "/") + if "\\" in candidate and "/" in candidate: + return True + segments = normalized.split("/") + if any(segment in {".", ".."} for segment in segments): + return True + return False + + +def _normalize_repo_relative_path(raw: Any, label: str) -> str: + if not isinstance(raw, str) or not raw.strip(): + raise ReviewBlocked(f"{label} must be a non-empty repository-relative path") + variants = _decoded_text_variants(raw) + if len(variants) > 1: + raise ReviewBlocked(f"{label} must not use encoded path characters") + value = variants[0] + if _looks_like_local_or_escaping_path(value) or "\\" in value: + raise ReviewBlocked(f"{label} must be a contained POSIX repository-relative path") + pure = PurePosixPath(value) + normalized = pure.as_posix() + if normalized != value or pure.is_absolute() or any(part in {"", ".", ".."} for part in pure.parts): + raise ReviewBlocked(f"{label} is not canonical") + if PureWindowsPath(value).is_absolute() or PureWindowsPath(value).drive: + raise ReviewBlocked(f"{label} must not be a Windows path") + return normalized + + +def _contained_file(base: Path, raw: Any, label: str, allowed_root: Path | None = None) -> Path: + normalized = _normalize_repo_relative_path(raw, label) + candidate = (base / normalized).resolve() + root = (allowed_root or base).resolve() + if not _is_relative_to(candidate, root): + raise ReviewBlocked(f"{label} escapes its allowed root") + if candidate.is_symlink(): + raise ReviewBlocked(f"{label} must not be a symbolic link") + return candidate + + +def _git(repo: Path, *args: str) -> str: + result = subprocess.run( + ["git", "-C", str(repo), *args], + check=False, + capture_output=True, + text=True, + env=sanitized_git_env(), + ) + if result.returncode != 0: + raise ReviewBlocked("authority repository Git identity could not be verified") + return result.stdout.strip() + + +def _canonical_origin(value: str) -> str: + normalized = value.strip().removesuffix(".git").replace("git@github.com:", "https://github.com/") + return normalized.casefold() + + +def _authority_file_identity(repo: Path, relative_path: str) -> dict[str, str]: + path = _contained_file(repo, relative_path, "authority path") + if not path.is_file(): + raise ReviewBlocked("authority path is missing") + tracked = _git(repo, "ls-files", "--error-unmatch", "--", relative_path) + if tracked.replace("\\", "/") != relative_path: + raise ReviewBlocked("authority path is not tracked at its canonical name") + blob = _git(repo, "rev-parse", f"HEAD:{relative_path}") + current_blob = _git(repo, "hash-object", "--", relative_path) + if blob != current_blob: + raise ReviewBlocked("authority source is dirty") + return { + "repository": repo.name, + "path": relative_path, + "git_blob_sha": blob, + "sha256": _sha256_file(path), + } + + +def _semantic_digest(value: Any) -> str: + encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def _state_integrity_digest(state: dict[str, Any], field: str) -> str: + payload = {key: value for key, value in state.items() if key != field} + return _semantic_digest(payload) + + +def _sanitize_block_reason(reason: str) -> str: + lowered = reason.casefold() + if "private" in lowered or "raw" in lowered: + return "input rejected by private-data boundary" + if "path" in lowered or "absolute" in lowered or "escape" in lowered or "symbolic" in lowered: + return "input rejected by path-containment boundary" + if any( + marker in lowered + for marker in ( + "claim", + "authority", + "approval", + "authorization", + "closure", + "customer", + "production", + "public_safe", + "public-safe", + "runtime", + "signal", + ) + ): + return "input rejected by claim-authority boundary" + if "duplicate" in lowered or "unsupported field" in lowered or "structured" in lowered: + return "input rejected by strict-structure boundary" + return re.sub(r"(?i)(?:[A-Z]:[\\/]|\\\\|/home/|/users/)\S*", "[redacted]", reason)[:240] + + def default_run_dir(repo_root: Path | None = None) -> Path: root = repo_root or Path.cwd() stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") @@ -148,21 +1059,247 @@ def default_batch_dir(repo_root: Path | None = None) -> Path: return root / ".hoxline" / "batch-runs" / stamp +def _review_repo_root(input_path: Path, explicit_root: Path | None = None) -> Path: + if explicit_root is not None: + return explicit_root.resolve() + resolved = input_path.resolve() + for candidate in (resolved.parent, *resolved.parents): + if (candidate / "pyproject.toml").is_file() and (candidate / "examples" / "review").is_dir(): + return candidate + package_root = Path(__file__).resolve().parents[2] + if ( + (package_root / "pyproject.toml").is_file() + and (package_root / "examples" / "review").is_dir() + ): + return package_root + return Path.cwd().resolve() + + +def _load_yaml_object(path: Path) -> dict[str, Any]: + try: + value = yaml.load(path.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader) + except (OSError, yaml.YAMLError, ReviewEngineError) as exc: + raise ReviewBlocked("authority YAML could not be parsed strictly") from exc + if not isinstance(value, dict): + raise ReviewBlocked("authority YAML must contain an object") + return value + + +def _single_entry(items: Any, key: str, expected: str, label: str) -> dict[str, Any]: + if not isinstance(items, list): + raise ReviewBlocked(f"{label} inventory must be a list") + matches = [item for item in items if isinstance(item, dict) and item.get(key) == expected] + if len(matches) != 1: + raise ReviewBlocked(f"{label} must contain exactly one matching identity") + return matches[0] + + +def _case_lists(value: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + cases = value.get("cases") + if isinstance(cases, dict): + positive = cases.get("positive") + negative = cases.get("negative") + else: + positive = value.get("positive") + negative = value.get("negative") + if not isinstance(positive, list) or not isinstance(negative, list) or not positive or not negative: + raise ReviewBlocked("validation-owned fixture contract must include positive and negative cases") + if not all(isinstance(item, dict) and isinstance(item.get("id"), str) for item in [*positive, *negative]): + raise ReviewBlocked("validation-owned fixture cases require explicit identifiers") + ids = [str(item["id"]).casefold() for item in [*positive, *negative]] + if len(ids) != len(set(ids)): + raise ReviewBlocked("validation-owned fixture case identifiers must be unique") + return positive, negative + + +def _owned_authority_binding(manifest: dict[str, Any], repo_root: Path) -> dict[str, Any]: + artifact_id = str(manifest["artifact_id"]) + if artifact_id.startswith("HO-NDR-"): + raise ReviewBlocked("boundary-contract artifact has no owned controlled-validation PASS authority") + org_root = repo_root.parent.resolve() + detection_repo = (org_root / "hawkinsoperations-detections").resolve() + validation_repo = (org_root / "hawkinsoperations-validation").resolve() + for repo_name, repo in ( + ("hawkinsoperations-detections", detection_repo), + ("hawkinsoperations-validation", validation_repo), + ): + if repo.parent != org_root or not (repo / ".git").exists(): + raise ReviewBlocked("required authority repository is missing") + origin = _canonical_origin(repo_origin(repo)) + if origin != _canonical_origin(CANONICAL_ORIGINS[repo_name]): + raise ReviewBlocked("authority repository origin is not canonical") + selection_errors = verify_selected_source_checkout(org_root, repo_name) + if selection_errors: + raise ReviewBlocked(f"authority repository selection is invalid: {selection_errors[0]}") + cache_key = ( + artifact_id, + str(detection_repo), + _git(detection_repo, "rev-parse", "HEAD"), + _git(detection_repo, "status", "--porcelain", "--untracked-files=no"), + str(validation_repo), + _git(validation_repo, "rev-parse", "HEAD"), + _git(validation_repo, "status", "--porcelain", "--untracked-files=no"), + _semantic_digest( + { + "expected_event_ids": manifest.get("expected_event_ids", []), + "expected_event_keys": manifest.get("expected_event_keys", []), + "expected_rule_ids": manifest.get("expected_rule_ids", []), + } + ), + ) + if cache_key in _AUTHORITY_BINDING_CACHE: + return deepcopy(_AUTHORITY_BINDING_CACHE[cache_key]) + + matrix_path = "detections/DETECTION_PROMOTION_MATRIX.yml" + registry_path = "validation/VALIDATION_REGISTRY.yml" + matrix = _load_yaml_object(detection_repo / matrix_path) + registry = _load_yaml_object(validation_repo / registry_path) + source_entry = _single_entry(matrix.get("entries"), "detection_id", artifact_id, "detection matrix") + validation_entry = _single_entry(registry.get("packages"), "detection_id", artifact_id, "validation registry") + + if source_entry.get("source_status") != "SOURCE_EXISTS": + raise ReviewBlocked("source-owned package is not SOURCE_EXISTS") + if source_entry.get("validation_expected_owner") != "hawkinsoperations-validation": + raise ReviewBlocked("source-owned validation handoff owner is invalid") + if source_entry.get("runtime_active") is not False or source_entry.get("signal_observed") is not False: + raise ReviewBlocked("source-owned entry exceeds the allowed runtime or signal boundary") + if source_entry.get("public_safe_status") != PUBLIC_SAFE_STATUS: + raise ReviewBlocked("source-owned entry exceeds the public-safe boundary") + + package_path = _normalize_repo_relative_path(source_entry.get("package_path"), "source package path") + package = (detection_repo / package_path).resolve() + if not _is_relative_to(package, detection_repo) or not package.is_dir(): + raise ReviewBlocked("source-owned package path is missing or outside its repository") + required_files = source_entry.get("required_files") + if not isinstance(required_files, list) or not required_files or not all(isinstance(item, str) for item in required_files): + raise ReviewBlocked("source-owned entry must declare required package files") + normalized_required: list[str] = [] + for item in required_files: + relative = _normalize_repo_relative_path(f"{package_path}/{item}", "source required file") + if relative.casefold() in {path.casefold() for path in normalized_required}: + raise ReviewBlocked("source required files contain a normalized duplicate") + normalized_required.append(relative) + if not (detection_repo / relative).is_file(): + raise ReviewBlocked("source required file is missing") + if f"{package_path}/rule.yml" not in normalized_required or f"{package_path}/status.yml" not in normalized_required: + raise ReviewBlocked("source package must include rule.yml and status.yml") + source_rule = _load_yaml_object(detection_repo / package_path / "rule.yml") + source_status = _load_yaml_object(detection_repo / package_path / "status.yml") + if source_rule.get("detection_id") != artifact_id or source_status.get("detection_id") != artifact_id: + raise ReviewBlocked("source package identity disagrees with the artifact identity") + + exact_validation = { + "validation_owner": "hawkinsoperations-validation", + "source_owner": "hawkinsoperations-detections", + "expected_result": "PASS", + "actual_result": "PASS", + "human_review_required": True, + "ai_disposition_authority": False, + "validation_kind": "controlled_validation", + "public_safe_status": PUBLIC_SAFE_STATUS, + "runtime_status": False, + "signal_status": False, + "source_dependency_required": True, + "ci_source_dependency_mode": "required", + } + for key, expected in exact_validation.items(): + if validation_entry.get(key) != expected: + raise ReviewBlocked("validation-owned registry entry is not eligible for fixture PASS") + expected_source_reference = f"hawkinsoperations-detections/{package_path}" + if validation_entry.get("source_reference") != expected_source_reference: + raise ReviewBlocked("validation-owned source handoff disagrees with the source package") + + validation_paths: list[str] = [] + for key in ( + "fixture_file", + "report_json", + "report_markdown", + "validator_script", + "parity_script", + "claim_boundary_script", + ): + relative = _normalize_repo_relative_path(validation_entry.get(key), f"validation {key}") + if relative.casefold() in {path.casefold() for path in validation_paths}: + raise ReviewBlocked("validation registry paths contain a normalized duplicate") + validation_paths.append(relative) + if not (validation_repo / relative).is_file(): + raise ReviewBlocked("validation-owned required file is missing") + + validation_fixture = _load_json(validation_repo / str(validation_entry["fixture_file"])) + validation_report = _load_json(validation_repo / str(validation_entry["report_json"])) + for value, label in ((validation_fixture, "validation fixture"), (validation_report, "validation report")): + if value.get("detection_id") != artifact_id: + raise ReviewBlocked(f"{label} identity disagrees with the artifact identity") + positive, negative = _case_lists(validation_fixture) + report_status = str(validation_report.get("status") or validation_report.get("result") or "").casefold() + if report_status != "pass": + raise ReviewBlocked("validation-owned report does not record PASS") + expected_positive = validation_entry.get("expected_positive_count") + expected_negative = validation_entry.get("expected_negative_count") + if expected_positive != len(positive) or expected_negative != len(negative): + raise ReviewBlocked("validation-owned registry case counts disagree with its fixture") + + searchable_source = "\n".join((detection_repo / path).read_text(encoding="utf-8") for path in normalized_required) + searchable_validation = json.dumps(validation_fixture, sort_keys=True) + for event_id in manifest.get("expected_event_ids", []): + if not re.search(rf"(? dict[str, Any]: - root = repo_root or Path(__file__).resolve().parents[2] out_dir = output_dir or default_run_dir(Path.cwd()) manifest_path = _resolve_path(artifact_path, Path.cwd()) + root = _review_repo_root(manifest_path, repo_root) manifest: dict[str, Any] = {} try: manifest = _load_json(manifest_path) _validate_manifest(manifest, manifest_path, root) fixture_paths = _fixture_paths(manifest, root) - review_outputs = _build_review_outputs(manifest, fixture_paths) - run = _build_pass_run(manifest, manifest_path, out_dir, review_outputs) + authority_binding = _owned_authority_binding(manifest, root) + review_outputs = _build_review_outputs(manifest, fixture_paths, authority_binding) + run = _build_pass_run(manifest, manifest_path, out_dir, review_outputs, fixture_paths, authority_binding) _write_pass_outputs(out_dir, run, force) return run except ReviewBlocked as exc: - run = _build_blocked_run(manifest, manifest_path, out_dir, str(exc)) + run = _build_blocked_run(manifest, manifest_path, out_dir, _sanitize_block_reason(str(exc))) _write_blocked_outputs(out_dir, run, force) return run @@ -174,19 +1311,54 @@ def verify_review_run(machine_state_path: Path) -> list[str]: except (OSError, ReviewEngineError) as exc: return [str(exc)] run_dir = machine_state_path.parent + if machine_state_path.is_symlink() or not _is_relative_to(machine_state_path.resolve(), run_dir.resolve()): + return ["machine-state path escapes its run root"] final_status = state.get("final_status") if final_status not in {"PASS", "BLOCKED"}: errors.append("machine-state final_status must be PASS or BLOCKED") + expected_state_fields = set(REVIEW_MACHINE_STATE_BASE_FIELDS) + if final_status == "PASS": + expected_state_fields.update({"authority_binding", "input_digests", "source_manifest_digest"}) + if set(state) != expected_state_fields: + errors.append("machine-state fields must exactly match the deterministic engine contract") if state.get("schema_version") != MACHINE_STATE_VERSION: errors.append(f"machine-state schema_version must be {MACHINE_STATE_VERSION}") if state.get("engine_version") != ENGINE_VERSION: errors.append(f"machine-state engine_version must be {ENGINE_VERSION}") + if state.get("run_id") != _safe_run_identity(run_dir.name): + errors.append("machine-state run_id must match the sanitized canonical run directory identity") + if final_status == "PASS" and state.get("block_reason") is not None: + errors.append("PASS machine-state block_reason must be null") if [stage.get("stage_name") for stage in state.get("stages", [])] != STAGE_REGISTRY: errors.append("machine-state stages must match review engine stage registry") - expected_outputs = EXPECTED_PASS_OUTPUTS if final_status == "PASS" else EXPECTED_BLOCKED_OUTPUTS + expected_roles = PASS_OUTPUT_ROLES if final_status == "PASS" else BLOCKED_OUTPUT_ROLES + expected_outputs = list(expected_roles.values()) + output_refs = state.get("outputs") + if output_refs != expected_roles: + errors.append("machine-state output role-to-path mapping must exactly match the engine contract") + declared_output_names = set(state.get("output_digests", {})) + expected_digest_names = set(expected_outputs) - {"machine-state.json"} + if declared_output_names != expected_digest_names: + errors.append("machine-state output digest inventory must exactly match generated outputs") for name in expected_outputs: - if not (run_dir / name).is_file(): + path = (run_dir / name).resolve() + if not _is_relative_to(path, run_dir.resolve()) or path.is_symlink(): + errors.append(f"output path is not contained: {name}") + elif not path.is_file(): errors.append(f"missing output file: {name}") + for name, expected_digest in state.get("output_digests", {}).items(): + try: + normalized = _normalize_repo_relative_path(name, "machine-state output path") + except ReviewBlocked as exc: + errors.append(str(exc)) + continue + path = (run_dir / normalized).resolve() + if not _is_relative_to(path, run_dir.resolve()) or path.is_symlink() or not path.is_file(): + errors.append(f"bound output is missing or escapes the run root: {name}") + elif expected_digest != _sha256_file(path): + errors.append(f"output digest mismatch: {name}") + if state.get("state_integrity_digest") != _state_integrity_digest(state, "state_integrity_digest"): + errors.append("machine-state integrity digest mismatch") for field, expected in { "public_safe_status": PUBLIC_SAFE_STATUS, "human_review_required": True, @@ -200,13 +1372,115 @@ def verify_review_run(machine_state_path: Path) -> list[str]: }.items(): if state.get(field) != expected: errors.append(f"machine-state field {field} must be {expected!r}") + try: + _validate_generated_output_security(state, "machine-state") + _validate_no_private_markers(state, "machine-state") + except ReviewBlocked as exc: + errors.append(_sanitize_block_reason(str(exc))) if final_status == "PASS": non_pass = [stage["stage_name"] for stage in state["stages"] if stage.get("status") != "PASS"] if non_pass: errors.append(f"PASS run has non-PASS stages: {', '.join(non_pass)}") _verify_pass_outputs(run_dir, state, errors) - if final_status == "BLOCKED" and not state.get("block_reason"): - errors.append("BLOCKED run must include block_reason") + try: + manifest = _load_json(run_dir / "artifact-manifest.json") + _validate_manifest(manifest, run_dir / "artifact-manifest.json", Path(__file__).resolve().parents[2]) + fixture_paths = _fixture_paths(manifest, Path(__file__).resolve().parents[2]) + authority = _owned_authority_binding(manifest, Path(__file__).resolve().parents[2]) + expected_inputs = { + "manifest": _semantic_digest(manifest), + "positive_fixture": _sha256_file(fixture_paths["positive"]), + "negative_fixture": _sha256_file(fixture_paths["negative"]), + "source_manifest": authority["source_manifest_digest"], + } + if state.get("input_digests") != expected_inputs: + errors.append("machine-state input digest contract does not match current owned inputs") + if state.get("source_manifest_digest") != authority["source_manifest_digest"]: + errors.append("machine-state source manifest digest mismatch") + if state.get("authority_binding") != authority: + errors.append("machine-state authority binding does not match current owned sources") + regenerated = _build_review_outputs(manifest, fixture_paths, authority) + expected_files: dict[str, Any] = { + "artifact-manifest.json": manifest, + "intake.json": regenerated["intake"], + "evidence-graph.json": regenerated["evidence_graph"], + "telemetry-contract-check.json": regenerated["telemetry_contract_check"], + "validation-result.json": regenerated["validation_result"], + "controlled-test-signal.json": regenerated["controlled_test_signal"], + "enrichment.json": regenerated["enrichment"], + "triage-summary.md": regenerated["triage_summary"], + "proofcard.json": regenerated["proofcard"], + "proofcard.md": regenerated["proofcard_markdown"], + "claim-authority.json": regenerated["claim_authority"], + "reviewer-pack.md": _reviewer_pack(manifest, state), + "run-summary.json": _run_summary(manifest, state, EXPECTED_PASS_OUTPUTS), + } + for name, expected in expected_files.items(): + _verify_generated_file(run_dir / name, expected, name, errors) + expected_state_fields = { + "run_id": _safe_run_identity(run_dir.name), + "artifact_id": manifest["artifact_id"], + "stages": _pass_stages(PASS_OUTPUT_ROLES), + "outputs": PASS_OUTPUT_ROLES, + "final_status": "PASS", + "block_reason": None, + "allowed_claim": _allowed_claims_for(manifest)[0], + "requested_claims": manifest["requested_claims"], + "blocked_claims": _blocked_claims(), + "proof_boundary": manifest["proof_boundary"], + "runtime_boundary": manifest["runtime_boundary"], + "signal_boundary": manifest["signal_boundary"], + "next_gate": manifest["next_gate"], + "product": PRODUCT, + } + for key, expected in expected_state_fields.items(): + if state.get(key) != expected: + errors.append(f"machine-state field {key} does not match deterministic engine output") + try: + recorded_manifest_path = _normalize_repo_relative_path( + state.get("manifest_path"), "machine-state manifest_path" + ) + recorded_manifest = (Path(__file__).resolve().parents[2] / recorded_manifest_path).resolve() + if not recorded_manifest.is_file() or _semantic_digest(_load_json(recorded_manifest)) != _semantic_digest(manifest): + errors.append("machine-state manifest_path does not identify the bound manifest content") + except (OSError, ReviewEngineError) as exc: + errors.append(f"machine-state manifest_path failed closed: {_sanitize_block_reason(str(exc))}") + except (OSError, ReviewEngineError) as exc: + errors.append(f"owned input replay failed closed: {_sanitize_block_reason(str(exc))}") + if final_status == "BLOCKED": + if not state.get("block_reason"): + errors.append("BLOCKED run must include block_reason") + else: + safe_manifest = _safe_blocked_manifest( + {"artifact_id": state.get("artifact_id")}, + Path("blocked-input.json"), + state["block_reason"], + ) + expected_state_fields = { + "run_id": _safe_run_identity(run_dir.name), + "artifact_id": safe_manifest["artifact_id"], + "manifest_path": "blocked-input.json", + "stages": _blocked_stages(state["block_reason"], BLOCKED_OUTPUT_ROLES), + "outputs": BLOCKED_OUTPUT_ROLES, + "final_status": "BLOCKED", + "allowed_claim": None, + "requested_claims": [], + "blocked_claims": _blocked_claims(), + "proof_boundary": "fixture-only; not public proof", + "runtime_boundary": "runtime prohibited", + "signal_boundary": "controlled-test fixture signal only", + "next_gate": "human_review_gate", + "product": PRODUCT, + } + for key, expected in expected_state_fields.items(): + if state.get(key) != expected: + errors.append(f"BLOCKED machine-state field {key} does not match deterministic engine output") + for name, expected in { + "artifact-manifest.json": safe_manifest, + "blocked-review.md": _blocked_review(state), + "run-summary.json": _run_summary(safe_manifest, state, EXPECTED_BLOCKED_OUTPUTS), + }.items(): + _verify_generated_file(run_dir / name, expected, name, errors) private_hits = _private_output_hits(run_dir) if private_hits: errors.append(f"private/raw markers found in review outputs: {', '.join(private_hits)}") @@ -266,9 +1540,9 @@ def render_run_console(run: dict[str, Any]) -> str: def run_batch_review(index_path: Path, output_dir: Path | None = None, force: bool = False, repo_root: Path | None = None) -> dict[str, Any]: - root = repo_root or Path(__file__).resolve().parents[2] out_dir = output_dir or default_batch_dir(Path.cwd()) resolved_index = _resolve_path(index_path, Path.cwd()) + root = _review_repo_root(resolved_index, repo_root) index: dict[str, Any] = {} try: index = _load_json(resolved_index) @@ -282,18 +1556,25 @@ def run_batch_review(index_path: Path, output_dir: Path | None = None, force: bo for artifact_entry in index["artifacts"]: artifact_id = artifact_entry["artifact_id"] manifest_path = _resolve_path(Path(artifact_entry["manifest_path"]), root) - artifact_out = artifacts_root / artifact_id + artifact_out = (artifacts_root / artifact_id).resolve() + if not _is_relative_to(artifact_out, artifacts_root.resolve()): + raise ReviewBlocked(f"artifact output path escapes batch root: {artifact_id}") run = run_review(manifest_path, artifact_out, force=True, repo_root=root) state = run["machine_state"] artifact_runs.append( { "artifact_id": artifact_id, - "manifest_path": str(manifest_path), - "output_dir": str(artifact_out.resolve()), - "machine_state": str((artifact_out / "machine-state.json").resolve()), - "reviewer_pack": str((artifact_out / "reviewer-pack.md").resolve()) if state["final_status"] == "PASS" else None, - "blocked_review": str((artifact_out / "blocked-review.md").resolve()) if state["final_status"] == "BLOCKED" else None, - "run_summary": str((artifact_out / "run-summary.json").resolve()), + "manifest_path": _display_path(manifest_path), + "output_dir": f"artifacts/{artifact_id}", + "machine_state": f"artifacts/{artifact_id}/machine-state.json", + "machine_state_sha256": _sha256_file(artifact_out / "machine-state.json"), + "manifest_sha256": state.get("input_digests", {}).get("manifest"), + "source_manifest_digest": state.get("source_manifest_digest"), + "state_integrity_digest": state.get("state_integrity_digest"), + "output_digests": state.get("output_digests", {}), + "reviewer_pack": f"artifacts/{artifact_id}/reviewer-pack.md" if state["final_status"] == "PASS" else None, + "blocked_review": f"artifacts/{artifact_id}/blocked-review.md" if state["final_status"] == "BLOCKED" else None, + "run_summary": f"artifacts/{artifact_id}/run-summary.json", "final_status": state["final_status"], "block_reason": state.get("block_reason"), "public_safe_status": state["public_safe_status"], @@ -305,6 +1586,7 @@ def run_batch_review(index_path: Path, output_dir: Path | None = None, force: bo "private_evidence_committed": state["private_evidence_committed"], "public_proof_promoted": state["public_proof_promoted"], "lifetime_ledger_changed": state["lifetime_ledger_changed"], + "next_gate": state["next_gate"], } ) @@ -317,8 +1599,9 @@ def run_batch_review(index_path: Path, output_dir: Path | None = None, force: bo return {"output_dir": str(out_dir), "index": index, "batch_machine_state": batch_state} except ReviewBlocked as exc: _prepare_output_dir(out_dir, force) - safe_index = _safe_blocked_index(index, resolved_index, str(exc)) - batch_state = _blocked_batch_machine_state(safe_index, resolved_index, out_dir, str(exc)) + safe_reason = _sanitize_block_reason(str(exc)) + safe_index = _safe_blocked_index(index, resolved_index, safe_reason) + batch_state = _blocked_batch_machine_state(safe_index, resolved_index, out_dir, safe_reason) _write_batch_outputs(out_dir, safe_index, batch_state) return {"output_dir": str(out_dir), "index": safe_index, "batch_machine_state": batch_state} @@ -330,15 +1613,44 @@ def verify_batch_run(batch_machine_state_path: Path) -> list[str]: except (OSError, ReviewEngineError) as exc: return [str(exc)] run_dir = batch_machine_state_path.parent + if batch_machine_state_path.is_symlink() or not _is_relative_to(batch_machine_state_path.resolve(), run_dir.resolve()): + return ["batch-machine-state path escapes its run root"] if state.get("schema_version") != BATCH_MACHINE_STATE_VERSION: errors.append(f"batch-machine-state schema_version must be {BATCH_MACHINE_STATE_VERSION}") if state.get("engine_version") != BATCH_ENGINE_VERSION: errors.append(f"batch-machine-state engine_version must be {BATCH_ENGINE_VERSION}") + if set(state) != BATCH_MACHINE_STATE_FIELDS: + errors.append("batch-machine-state fields must exactly match the deterministic engine contract") if state.get("final_status") not in {"PASS", "MIXED", "BLOCKED"}: errors.append("batch-machine-state final_status must be PASS, MIXED, or BLOCKED") + if state.get("batch_id") != _safe_run_identity(run_dir.name): + errors.append("batch-machine-state batch_id must match the sanitized canonical run directory identity") + if state.get("final_status") != "BLOCKED" and state.get("block_reason") is not None: + errors.append("non-BLOCKED batch-machine-state block_reason must be null") + if state.get("outputs") != BATCH_OUTPUT_ROLES: + errors.append("batch output role-to-path mapping must exactly match the engine contract") for name in BATCH_EXPECTED_OUTPUTS: - if not (run_dir / name).is_file(): + path = (run_dir / name).resolve() + if not _is_relative_to(path, run_dir.resolve()) or path.is_symlink(): + errors.append(f"batch output path is not contained: {name}") + elif not path.is_file(): errors.append(f"missing batch output file: {name}") + expected_digest_names = set(BATCH_EXPECTED_OUTPUTS) - {"batch-machine-state.json"} + if set(state.get("output_digests", {})) != expected_digest_names: + errors.append("batch output digest inventory must exactly match generated outputs") + for name, expected_digest in state.get("output_digests", {}).items(): + try: + normalized = _normalize_repo_relative_path(name, "batch output path") + except ReviewBlocked as exc: + errors.append(str(exc)) + continue + path = (run_dir / normalized).resolve() + if not _is_relative_to(path, run_dir.resolve()) or path.is_symlink() or not path.is_file(): + errors.append(f"bound batch output is missing or escapes the run root: {name}") + elif expected_digest != _sha256_file(path): + errors.append(f"batch output digest mismatch: {name}") + if state.get("batch_state_integrity_digest") != _state_integrity_digest(state, "batch_state_integrity_digest"): + errors.append("batch-machine-state integrity digest mismatch") for field, expected in { "public_safe_status": PUBLIC_SAFE_STATUS, "human_review_required": True, @@ -353,27 +1665,181 @@ def verify_batch_run(batch_machine_state_path: Path) -> list[str]: }.items(): if state.get(field) != expected: errors.append(f"batch-machine-state field {field} must be {expected!r}") + try: + _validate_generated_output_security(state, "batch-machine-state") + _validate_no_private_markers(state, "batch-machine-state") + except ReviewBlocked as exc: + errors.append(_sanitize_block_reason(str(exc))) if state.get("final_status") == "BLOCKED" and not state.get("block_reason"): errors.append("BLOCKED batch run must include block_reason") artifacts = state.get("artifacts", []) if state.get("final_status") != "BLOCKED" and not artifacts: errors.append("non-BLOCKED batch run must include artifact states") + artifact_ids: set[str] = set() + source_digests: list[str] = [] for artifact in artifacts: artifact_id = artifact.get("artifact_id", "UNKNOWN") - state_path = Path(str(artifact.get("machine_state", ""))) - if not state_path.is_absolute(): - state_path = run_dir / state_path + if artifact_id in artifact_ids: + errors.append(f"duplicate aggregate artifact identity: {artifact_id}") + artifact_ids.add(str(artifact_id)) + try: + state_relative = _normalize_repo_relative_path(artifact.get("machine_state"), "artifact machine-state path") + except ReviewBlocked as exc: + errors.append(f"{artifact_id}: {exc}") + continue + state_path = (run_dir / state_relative).resolve() + if not _is_relative_to(state_path, (run_dir / "artifacts").resolve()) or state_path.is_symlink(): + errors.append(f"{artifact_id}: child machine-state escapes artifacts root") + continue if not state_path.is_file(): errors.append(f"missing artifact machine-state for {artifact_id}") continue + if artifact.get("machine_state_sha256") != _sha256_file(state_path): + errors.append(f"{artifact_id}: machine-state hash mismatch") artifact_errors = verify_review_run(state_path) errors.extend(f"{artifact_id}: {error}" for error in artifact_errors) + child_state = _load_json(state_path) + if child_state.get("artifact_id") != artifact_id: + errors.append(f"{artifact_id}: aggregate artifact_id does not match child machine-state artifact_id") + for key in ( + "final_status", + "block_reason", + "public_safe_status", + "human_review_required", + "ai_disposition_authority", + "endpoint_mutation", + "wazuh_mutation", + "runtime_proof", + "private_evidence_committed", + "public_proof_promoted", + "lifetime_ledger_changed", + "next_gate", + "source_manifest_digest", + "state_integrity_digest", + "output_digests", + ): + if artifact.get(key) != child_state.get(key): + errors.append(f"{artifact_id}: aggregate {key} does not match child machine-state") + if artifact.get("manifest_sha256") != child_state.get("input_digests", {}).get("manifest"): + errors.append(f"{artifact_id}: aggregate manifest digest does not match child machine-state") + if child_state.get("source_manifest_digest"): + source_digests.append(str(child_state["source_manifest_digest"])) if artifact.get("public_safe_status") != PUBLIC_SAFE_STATUS: errors.append(f"{artifact_id}: public_safe_status must remain NOT_PUBLIC_SAFE") for false_field in ("endpoint_mutation", "wazuh_mutation", "runtime_proof", "public_proof_promoted", "lifetime_ledger_changed", "private_evidence_committed"): if artifact.get(false_field) is not False: errors.append(f"{artifact_id}: {false_field} must be false") + expected_artifact_root = f"artifacts/{artifact_id}" + expected_paths = { + "output_dir": expected_artifact_root, + "machine_state": f"{expected_artifact_root}/machine-state.json", + "run_summary": f"{expected_artifact_root}/run-summary.json", + "reviewer_pack": f"{expected_artifact_root}/reviewer-pack.md" + if child_state.get("final_status") == "PASS" + else None, + "blocked_review": f"{expected_artifact_root}/blocked-review.md" + if child_state.get("final_status") == "BLOCKED" + else None, + } + for key, expected in expected_paths.items(): + if artifact.get(key) != expected: + errors.append(f"{artifact_id}: aggregate {key} does not match canonical output role path") errors.extend(_batch_expectation_errors(state)) + expected_aggregate = _semantic_digest(sorted(source_digests)) if source_digests else None + if state.get("source_manifest_digest") != expected_aggregate: + errors.append("batch source manifest digest does not match child authority bindings") + input_index = run_dir / "input-index.json" + if input_index.is_file() and state.get("input_index_sha256") != _sha256_file(input_index): + errors.append("batch input-index digest mismatch") + if state.get("final_status") != "BLOCKED": + try: + index = _load_json(input_index) + _validate_batch_index(index, input_index, Path(__file__).resolve().parents[2]) + if state.get("index_id") != index.get("index_id"): + errors.append("batch index identity mismatch") + if state.get("expected_pass_artifacts") != index.get("expected_pass_artifacts"): + errors.append("batch expected PASS list does not match input index") + if state.get("expected_blocked_artifacts") != index.get("expected_blocked_artifacts"): + errors.append("batch expected BLOCKED list does not match input index") + expected_state_fields = { + "index_id": index["index_id"], + "batch_claim_boundary": index["batch_claim_boundary"], + "proof_boundary": index["proof_boundary"], + "runtime_boundary": index["runtime_boundary"], + "signal_boundary": index["signal_boundary"], + "next_gate": index["next_gate"], + "outputs": BATCH_OUTPUT_ROLES, + "product": PRODUCT, + } + for key, expected in expected_state_fields.items(): + if state.get(key) != expected: + errors.append(f"batch-machine-state field {key} does not match deterministic engine output") + recorded_index_path = _normalize_repo_relative_path( + state.get("index_path"), "batch-machine-state index_path" + ) + recorded_index = (Path(__file__).resolve().parents[2] / recorded_index_path).resolve() + if not recorded_index.is_file() or _semantic_digest(_load_json(recorded_index)) != _semantic_digest(index): + errors.append("batch-machine-state index_path does not identify the bound index content") + indexed_paths = { + entry["artifact_id"]: _normalize_repo_relative_path( + entry["manifest_path"], "batch artifact manifest path" + ) + for entry in index["artifacts"] + } + if [artifact.get("artifact_id") for artifact in artifacts] != [ + entry["artifact_id"] for entry in index["artifacts"] + ]: + errors.append("batch aggregate artifact order must match the bound input index") + for artifact in artifacts: + artifact_id = str(artifact.get("artifact_id")) + if artifact.get("manifest_path") != indexed_paths.get(artifact_id): + errors.append(f"{artifact_id}: aggregate manifest_path does not match input index") + for name, expected in { + "input-index.json": index, + "batch-summary.md": _batch_summary_markdown(state), + "batch-reviewer-pack.md": _batch_reviewer_pack(state), + "batch-run-summary.json": _batch_run_summary(state), + }.items(): + _verify_generated_file(run_dir / name, expected, name, errors) + except (OSError, ReviewEngineError) as exc: + errors.append(f"batch input-index replay failed closed: {_sanitize_block_reason(str(exc))}") + else: + try: + safe_index = _safe_blocked_index( + {"index_id": state.get("index_id")}, + Path("blocked-input-index.json"), + str(state.get("block_reason")), + ) + expected_state_fields = { + "batch_id": _safe_run_identity(run_dir.name), + "index_id": safe_index["index_id"], + "index_path": "blocked-input-index.json", + "artifacts": [], + "expected_pass_artifacts": [], + "expected_blocked_artifacts": [], + "actual_pass_artifacts": [], + "actual_blocked_artifacts": [], + "batch_claim_boundary": None, + "proof_boundary": safe_index["proof_boundary"], + "runtime_boundary": safe_index["runtime_boundary"], + "signal_boundary": safe_index["signal_boundary"], + "outputs": BATCH_OUTPUT_ROLES, + "next_gate": safe_index["next_gate"], + "product": PRODUCT, + "source_manifest_digest": None, + } + for key, expected in expected_state_fields.items(): + if state.get(key) != expected: + errors.append(f"blocked batch-machine-state field {key} does not match deterministic engine output") + for name, expected in { + "input-index.json": safe_index, + "batch-summary.md": _batch_summary_markdown(state), + "batch-reviewer-pack.md": _batch_reviewer_pack(state), + "batch-run-summary.json": _batch_run_summary(state), + }.items(): + _verify_generated_file(run_dir / name, expected, name, errors) + except (OSError, ReviewEngineError) as exc: + errors.append(f"blocked batch replay failed closed: {_sanitize_block_reason(str(exc))}") private_hits = _private_output_hits(run_dir) if private_hits: errors.append(f"private/raw markers found in batch outputs: {', '.join(private_hits)}") @@ -426,6 +1892,7 @@ def _validate_batch_index(index: dict[str, Any], index_path: Path, repo_root: Pa for field in required: if field not in index: raise ReviewBlocked(f"batch index missing required field: {field}") + _require_exact_keys(index, BATCH_INDEX_ALLOWED_FIELDS, "batch index") if index["index_version"] != BATCH_INDEX_VERSION: raise ReviewBlocked(f"index_version must be {BATCH_INDEX_VERSION}") if index.get("public_safe_status") != PUBLIC_SAFE_STATUS: @@ -435,29 +1902,65 @@ def _validate_batch_index(index: dict[str, Any], index_path: Path, repo_root: Pa if index.get("ai_disposition_authority") is not False: raise ReviewBlocked("batch ai_disposition_authority must be false") _validate_claims({"requested_claims": [index.get("batch_claim_boundary", "")], "blocked_claim_classes": BLOCKED_CLAIM_FAMILIES}) + _validate_recursive_boundaries(index, "batch index") _validate_no_private_markers(index, "batch index") artifacts = index.get("artifacts") if not isinstance(artifacts, list) or not artifacts: raise ReviewBlocked("batch index artifacts must be a non-empty list") seen: set[str] = set() + normalized_manifest_paths: set[str] = set() for item in artifacts: if not isinstance(item, dict): raise ReviewBlocked("batch index artifact entries must be objects") + _require_exact_keys(item, {"artifact_id", "manifest_path"}, "batch artifact entry") artifact_id = item.get("artifact_id") if not artifact_id: raise ReviewBlocked("batch index artifact entry missing artifact_id") + if not ARTIFACT_ID_PATTERN.fullmatch(str(artifact_id)): + raise ReviewBlocked(f"batch index artifact_id has invalid format: {artifact_id}") if artifact_id in seen: raise ReviewBlocked(f"duplicate artifact_id in batch index: {artifact_id}") seen.add(str(artifact_id)) manifest_path = item.get("manifest_path") if not manifest_path: raise ReviewBlocked(f"batch index artifact {artifact_id} missing manifest_path") - resolved = _resolve_path(Path(str(manifest_path)), repo_root) + canonical_manifest_path = _normalize_repo_relative_path(manifest_path, "batch manifest path") + if canonical_manifest_path.casefold() in normalized_manifest_paths: + raise ReviewBlocked("batch index contains a duplicate normalized manifest path") + normalized_manifest_paths.add(canonical_manifest_path.casefold()) + resolved = _contained_file( + repo_root, + canonical_manifest_path, + "batch manifest path", + repo_root / "examples" / "review", + ) if not resolved.is_file(): - raise ReviewBlocked(f"batch index manifest path missing for {artifact_id}: {resolved}") + raise ReviewBlocked("batch index manifest path is missing") allowed_root = (repo_root / "examples" / "review").resolve() if not _is_relative_to(resolved.resolve(), allowed_root): raise ReviewBlocked(f"batch index manifest path outside examples/review for {artifact_id}") + manifest = _load_json(resolved) + if manifest.get("artifact_id") != artifact_id: + raise ReviewBlocked( + f"batch index artifact_id {artifact_id} does not match manifest artifact_id {manifest.get('artifact_id')}" + ) + expected_pass = index.get("expected_pass_artifacts") + expected_blocked = index.get("expected_blocked_artifacts") + if not isinstance(expected_pass, list) or not isinstance(expected_blocked, list): + raise ReviewBlocked("batch expectations must be arrays") + if not all(isinstance(item, str) and ARTIFACT_ID_PATTERN.fullmatch(item) for item in [*expected_pass, *expected_blocked]): + raise ReviewBlocked("batch expectation identities are malformed") + if len({item.casefold() for item in expected_pass}) != len(expected_pass) or len( + {item.casefold() for item in expected_blocked} + ) != len(expected_blocked): + raise ReviewBlocked("batch expectations contain duplicate normalized identities") + if set(expected_pass) & set(expected_blocked): + raise ReviewBlocked("batch PASS and BLOCKED expectations must be disjoint") + if set(expected_pass) | set(expected_blocked) != seen: + raise ReviewBlocked("batch expectations must classify every declared artifact exactly once") + expected_generated = [name for name in BATCH_EXPECTED_OUTPUTS if name != "input-index.json"] + if index.get("generated_outputs") != expected_generated: + raise ReviewBlocked("batch generated_outputs must match the engine output contract") def _batch_machine_state(index: dict[str, Any], index_path: Path, output_dir: Path, artifacts: list[dict[str, Any]]) -> dict[str, Any]: @@ -466,13 +1969,19 @@ def _batch_machine_state(index: dict[str, Any], index_path: Path, output_dir: Pa return { "schema_version": BATCH_MACHINE_STATE_VERSION, "engine_version": BATCH_ENGINE_VERSION, - "batch_id": output_dir.name, + "batch_id": _safe_run_identity(output_dir.name), "index_id": index.get("index_id"), - "index_path": str(index_path), + "index_path": _display_path(index_path), "artifacts": artifacts, "expected_pass_artifacts": list(index.get("expected_pass_artifacts", [])), "expected_blocked_artifacts": list(index.get("expected_blocked_artifacts", [])), "final_status": final_status, + "actual_pass_artifacts": sorted( + artifact["artifact_id"] for artifact in artifacts if artifact.get("final_status") == "PASS" + ), + "actual_blocked_artifacts": sorted( + artifact["artifact_id"] for artifact in artifacts if artifact.get("final_status") == "BLOCKED" + ), "block_reason": None, "batch_claim_boundary": index.get("batch_claim_boundary"), "proof_boundary": index.get("proof_boundary"), @@ -488,15 +1997,24 @@ def _batch_machine_state(index: dict[str, Any], index_path: Path, output_dir: Pa "public_proof_promoted": False, "lifetime_ledger_changed": False, "website_changed": False, - "outputs": BATCH_EXPECTED_OUTPUTS, + "outputs": deepcopy(BATCH_OUTPUT_ROLES), "next_gate": index.get("next_gate"), - "created_at": datetime.now(timezone.utc).isoformat(), "product": PRODUCT, + "source_manifest_digest": _semantic_digest( + sorted( + str(artifact["source_manifest_digest"]) + for artifact in artifacts + if artifact.get("source_manifest_digest") + ) + ) + if artifacts + else None, } def _blocked_batch_machine_state(index: dict[str, Any], index_path: Path, output_dir: Path, block_reason: str) -> dict[str, Any]: state = _batch_machine_state(index, index_path, output_dir, []) + state["index_path"] = "blocked-input-index.json" state["final_status"] = "BLOCKED" state["block_reason"] = block_reason return state @@ -513,20 +2031,28 @@ def _batch_expectation_errors(state: dict[str, Any]) -> list[str]: errors.append(f"expected PASS artifacts {sorted(expected_pass)} but saw {sorted(actual_pass)}") if expected_blocked != actual_blocked: errors.append(f"expected BLOCKED artifacts {sorted(expected_blocked)} but saw {sorted(actual_blocked)}") + if state.get("actual_pass_artifacts") != sorted(actual_pass): + errors.append("recorded actual PASS artifacts do not match child states") + if state.get("actual_blocked_artifacts") != sorted(actual_blocked): + errors.append("recorded actual BLOCKED artifacts do not match child states") + expected_status = "PASS" if actual_pass and not actual_blocked else "MIXED" if actual_pass and actual_blocked else "BLOCKED" + if not errors and state.get("final_status") != expected_status: + errors.append(f"aggregate final status must be {expected_status}") return errors def _write_batch_outputs(output_dir: Path, index: dict[str, Any], state: dict[str, Any]) -> None: - _write_file_map( - output_dir, - { - "input-index.json": index, - "batch-machine-state.json": state, - "batch-summary.md": _batch_summary_markdown(state), - "batch-reviewer-pack.md": _batch_reviewer_pack(state), - "batch-run-summary.json": _batch_run_summary(state), - }, - ) + file_map = { + "input-index.json": index, + "batch-summary.md": _batch_summary_markdown(state), + "batch-reviewer-pack.md": _batch_reviewer_pack(state), + "batch-run-summary.json": _batch_run_summary(state), + } + _write_file_map(output_dir, file_map) + state["input_index_sha256"] = _sha256_file(output_dir / "input-index.json") + state["output_digests"] = {name: _sha256_file(output_dir / name) for name in sorted(file_map)} + state["batch_state_integrity_digest"] = _state_integrity_digest(state, "batch_state_integrity_digest") + _write_file_map(output_dir, {"batch-machine-state.json": state}) def _batch_summary_markdown(state: dict[str, Any]) -> str: @@ -623,7 +2149,7 @@ def _batch_run_summary(state: dict[str, Any]) -> dict[str, Any]: "final_status": state["final_status"], "block_reason": state.get("block_reason"), "artifacts": state.get("artifacts", []), - "outputs": BATCH_EXPECTED_OUTPUTS, + "outputs": deepcopy(BATCH_OUTPUT_ROLES), "public_safe_status": state["public_safe_status"], "human_review_required": state["human_review_required"], "ai_disposition_authority": state["ai_disposition_authority"], @@ -638,10 +2164,12 @@ def _batch_run_summary(state: dict[str, Any]) -> dict[str, Any]: def _safe_blocked_index(index: dict[str, Any], index_path: Path, block_reason: str) -> dict[str, Any]: + index_id = index.get("index_id") if index else None + safe_index_id = index_id if isinstance(index_id, str) and re.fullmatch(r"[A-Za-z0-9._-]{1,96}", index_id) else "UNKNOWN" return { "schema_version": "blocked-batch-index-v1", - "index_path": str(index_path), - "index_id": index.get("index_id", "UNKNOWN") if index else "UNKNOWN", + "index_path": "blocked-input-index.json", + "index_id": safe_index_id, "final_status": "BLOCKED", "block_reason": block_reason, "redaction": "Original hostile batch index content is not copied into blocked outputs.", @@ -651,29 +2179,22 @@ def _safe_blocked_index(index: dict[str, Any], index_path: Path, block_reason: s "human_review_required": True, "ai_disposition_authority": False, "runtime_boundary": "runtime prohibited", - "signal_boundary": "synthetic fixture signal only", + "signal_boundary": "controlled-test fixture signal only", "proof_boundary": "not public proof", "next_gate": "index_fix_required", } -def _build_pass_run(manifest: dict[str, Any], manifest_path: Path, output_dir: Path, review_outputs: dict[str, Any]) -> dict[str, Any]: - run_id = output_dir.name - output_refs = { - "artifact_manifest": "artifact-manifest.json", - "artifact_intake": "intake.json", - "evidence_graph": "evidence-graph.json", - "telemetry_contract_check": "telemetry-contract-check.json", - "controlled_validation": "validation-result.json", - "synthetic_signal": "synthetic-signal.json", - "enrichment": "enrichment.json", - "triage": "triage-summary.md", - "proofcard": "proofcard.json", - "claim_authority": "claim-authority.json", - "reviewer_pack": "reviewer-pack.md", - "machine_state": "machine-state.json", - "run_summary": "run-summary.json", - } +def _build_pass_run( + manifest: dict[str, Any], + manifest_path: Path, + output_dir: Path, + review_outputs: dict[str, Any], + fixture_paths: dict[str, Path], + authority_binding: dict[str, Any], +) -> dict[str, Any]: + run_id = _safe_run_identity(output_dir.name) + output_refs = deepcopy(PASS_OUTPUT_ROLES) state = _machine_state( manifest=manifest, manifest_path=manifest_path, @@ -682,6 +2203,14 @@ def _build_pass_run(manifest: dict[str, Any], manifest_path: Path, output_dir: P stages=_pass_stages(output_refs), outputs=output_refs, ) + state["source_manifest_digest"] = authority_binding["source_manifest_digest"] + state["authority_binding"] = authority_binding + state["input_digests"] = { + "manifest": _semantic_digest(manifest), + "positive_fixture": _sha256_file(fixture_paths["positive"]), + "negative_fixture": _sha256_file(fixture_paths["negative"]), + "source_manifest": authority_binding["source_manifest_digest"], + } reviewer_pack = _reviewer_pack(manifest, state) summary = _run_summary(manifest, state, EXPECTED_PASS_OUTPUTS) return { @@ -695,16 +2224,12 @@ def _build_pass_run(manifest: dict[str, Any], manifest_path: Path, output_dir: P def _build_blocked_run(manifest: dict[str, Any], manifest_path: Path, output_dir: Path, block_reason: str) -> dict[str, Any]: - run_id = output_dir.name - output_refs = { - "artifact_manifest": "artifact-manifest.json", - "machine_state": "machine-state.json", - "blocked_review": "blocked-review.md", - "run_summary": "run-summary.json", - } + run_id = _safe_run_identity(output_dir.name) + output_refs = deepcopy(BLOCKED_OUTPUT_ROLES) + safe_manifest = _safe_blocked_manifest(manifest, manifest_path, block_reason) state = _machine_state( - manifest=manifest, - manifest_path=manifest_path, + manifest=safe_manifest, + manifest_path=Path("blocked-input.json"), run_id=run_id, final_status="BLOCKED", stages=_blocked_stages(block_reason, output_refs), @@ -713,7 +2238,7 @@ def _build_blocked_run(manifest: dict[str, Any], manifest_path: Path, output_dir ) return { "output_dir": str(output_dir), - "manifest": _safe_blocked_manifest(manifest, manifest_path, block_reason), + "manifest": safe_manifest, "blocked_review": _blocked_review(state), "machine_state": state, "run_summary": _run_summary(manifest, state, EXPECTED_BLOCKED_OUTPUTS), @@ -722,10 +2247,12 @@ def _build_blocked_run(manifest: dict[str, Any], manifest_path: Path, output_dir def _safe_blocked_manifest(manifest: dict[str, Any], manifest_path: Path, block_reason: str) -> dict[str, Any]: + artifact_id = manifest.get("artifact_id") if manifest else None + safe_artifact_id = artifact_id if isinstance(artifact_id, str) and ARTIFACT_ID_PATTERN.fullmatch(artifact_id) else "UNKNOWN" return { "schema_version": "blocked-artifact-manifest-v1", - "manifest_path": str(manifest_path), - "artifact_id": manifest.get("artifact_id", "UNKNOWN") if manifest else "UNKNOWN", + "manifest_path": "blocked-input.json", + "artifact_id": safe_artifact_id, "final_status": "BLOCKED", "block_reason": block_reason, "redaction": "Original hostile manifest content is not copied into blocked outputs.", @@ -744,37 +2271,54 @@ def _write_pass_outputs(output_dir: Path, run: dict[str, Any], force: bool) -> N "evidence-graph.json": outputs["evidence_graph"], "telemetry-contract-check.json": outputs["telemetry_contract_check"], "validation-result.json": outputs["validation_result"], - "synthetic-signal.json": outputs["synthetic_signal"], + "controlled-test-signal.json": outputs["controlled_test_signal"], "enrichment.json": outputs["enrichment"], "triage-summary.md": outputs["triage_summary"], "proofcard.json": outputs["proofcard"], "proofcard.md": outputs["proofcard_markdown"], "claim-authority.json": outputs["claim_authority"], "reviewer-pack.md": run["reviewer_pack"], - "machine-state.json": run["machine_state"], - "run-summary.json": run["run_summary"], } _write_file_map(output_dir, file_map) + state = run["machine_state"] + state["output_digests"] = { + name: _sha256_file(output_dir / name) + for name in sorted(file_map) + } + summary = _run_summary(run["manifest"], state, EXPECTED_PASS_OUTPUTS) + _write_file_map(output_dir, {"run-summary.json": summary}) + state["output_digests"]["run-summary.json"] = _sha256_file(output_dir / "run-summary.json") + state["state_integrity_digest"] = _state_integrity_digest(state, "state_integrity_digest") + _write_file_map(output_dir, {"machine-state.json": state}) + run["run_summary"] = summary def _write_blocked_outputs(output_dir: Path, run: dict[str, Any], force: bool) -> None: _prepare_output_dir(output_dir, force) - _write_file_map( - output_dir, - { - "artifact-manifest.json": run["manifest"], - "machine-state.json": run["machine_state"], - "blocked-review.md": run["blocked_review"], - "run-summary.json": run["run_summary"], - }, - ) + file_map = { + "artifact-manifest.json": run["manifest"], + "blocked-review.md": run["blocked_review"], + } + _write_file_map(output_dir, file_map) + state = run["machine_state"] + state["output_digests"] = {name: _sha256_file(output_dir / name) for name in sorted(file_map)} + summary = _run_summary(run["manifest"], state, EXPECTED_BLOCKED_OUTPUTS) + _write_file_map(output_dir, {"run-summary.json": summary}) + state["output_digests"]["run-summary.json"] = _sha256_file(output_dir / "run-summary.json") + state["state_integrity_digest"] = _state_integrity_digest(state, "state_integrity_digest") + _write_file_map(output_dir, {"machine-state.json": state}) + run["run_summary"] = summary def _node(node_id: str, node_type: str, owner: str, status: str) -> dict[str, str]: return {"id": node_id, "type": node_type, "owner": owner, "status": status} -def _build_review_outputs(manifest: dict[str, Any], fixture_paths: dict[str, Path]) -> dict[str, Any]: +def _build_review_outputs( + manifest: dict[str, Any], + fixture_paths: dict[str, Path], + authority_binding: dict[str, Any], +) -> dict[str, Any]: positive_fixture = _load_json(fixture_paths["positive"]) negative_fixture = _load_json(fixture_paths["negative"]) _validate_review_fixture(positive_fixture, manifest, True) @@ -782,8 +2326,8 @@ def _build_review_outputs(manifest: dict[str, Any], fixture_paths: dict[str, Pat intake = _artifact_intake(manifest) telemetry = _telemetry_contract_check(manifest, positive_fixture) - validation = _controlled_validation(manifest, positive_fixture, negative_fixture, telemetry) - signal = _synthetic_signal(manifest, positive_fixture, validation) + validation = _controlled_validation(manifest, positive_fixture, negative_fixture, telemetry, authority_binding) + signal = _controlled_test_signal(manifest, positive_fixture, validation) enrichment = _enrichment(manifest, positive_fixture) triage = _triage(manifest, signal, enrichment, validation) proofcard = _proofcard(manifest, intake, telemetry, validation, signal, enrichment, triage) @@ -794,7 +2338,7 @@ def _build_review_outputs(manifest: dict[str, Any], fixture_paths: dict[str, Pat "evidence_graph": evidence_graph, "telemetry_contract_check": telemetry, "validation_result": validation, - "synthetic_signal": signal, + "controlled_test_signal": signal, "enrichment": enrichment, "triage_summary": _triage_markdown(manifest, triage), "proofcard": proofcard, @@ -818,7 +2362,7 @@ def _artifact_intake(manifest: dict[str, Any]) -> dict[str, Any]: "human_review_required": True, "ai_disposition_authority": False, "notes": [ - "Fixture is synthetic and local-only.", + "Fixture is controlled-test and local-only.", "No users, groups, endpoints, Wazuh systems, or private infrastructure are touched.", ], } @@ -826,15 +2370,19 @@ def _artifact_intake(manifest: dict[str, Any]) -> dict[str, Any]: def _telemetry_contract_check(manifest: dict[str, Any], fixture: dict[str, Any]) -> dict[str, Any]: contract = manifest["telemetry_contract"] - observed_event_ids = sorted({int(event["event_id"]) for event in fixture["events"]}) + observed_event_ids = sorted({int(event["event_id"]) for event in fixture["events"] if "event_id" in event}) + event_key_field = str(contract.get("event_key_field") or "event_key") + observed_event_keys = sorted({str(event[event_key_field]) for event in fixture["events"] if event_key_field in event}) return { "schema_version": "telemetry-contract-check-v0", "artifact_id": manifest["artifact_id"], "required_source": contract["source"], "event_ids": list(manifest["expected_event_ids"]), "fixture_event_ids": observed_event_ids, + "event_keys": list(manifest.get("expected_event_keys", [])), + "fixture_event_keys": observed_event_keys, "wazuh_rule_family": list(manifest["expected_rule_ids"]), - "required_fields": list(contract.get("required_fields", ["event_id", "channel", "action", "actor"])), + "required_fields": list(contract.get("required_fields", [_manifest_event_selector(manifest), "channel", "action", "actor"])), "missing_required_fields": [], "result": "pass", "scope": "pass for fixture only", @@ -848,6 +2396,7 @@ def _controlled_validation( fixture: dict[str, Any], negative_fixture: dict[str, Any], telemetry: dict[str, Any], + authority_binding: dict[str, Any], ) -> dict[str, Any]: positive_match = _fixture_matches_manifest(fixture, manifest) negative_match = _fixture_matches_manifest(negative_fixture, manifest) @@ -862,19 +2411,21 @@ def _controlled_validation( "matched_positive_cases": 1 if positive_match else 0, "false_positive_negative_cases": 1 if negative_match else 0, "result": result, + "owned_authority_binding": authority_binding, + "source_manifest_digest": authority_binding["source_manifest_digest"], "endpoint_mutation": False, "runtime_rerun": False, "wazuh_mutation": False, - "explanation": "Validation evaluates bundled synthetic fixture records only.", + "explanation": "Validation evaluates bundled controlled-test fixture records only.", } -def _synthetic_signal(manifest: dict[str, Any], fixture: dict[str, Any], validation: dict[str, Any]) -> dict[str, Any]: +def _controlled_test_signal(manifest: dict[str, Any], fixture: dict[str, Any], validation: dict[str, Any]) -> dict[str, Any]: artifact_id = manifest["artifact_id"] return { - "schema_version": "synthetic-signal-v0", + "schema_version": "controlled-test-signal-v0", "artifact_id": artifact_id, - "signal_id": f"synthetic-signal-{artifact_id.lower()}-review-v1", + "signal_id": f"controlled-test-signal-{artifact_id.lower()}-review-v1", "source": "safe bundled fixture", "detection_fired": validation["result"] == "pass" and _fixture_matches_manifest(fixture, manifest), "simulation_only": True, @@ -885,18 +2436,19 @@ def _synthetic_signal(manifest: dict[str, Any], fixture: dict[str, Any], validat def _enrichment(manifest: dict[str, Any], fixture: dict[str, Any]) -> dict[str, Any]: - event_mapping = {str(event_id): f"fixture event metadata for {manifest['artifact_id']}" for event_id in manifest["expected_event_ids"]} + selectors = [str(event_id) for event_id in manifest["expected_event_ids"]] + [str(key) for key in manifest.get("expected_event_keys", [])] + event_mapping = {selector: f"fixture event metadata for {manifest['artifact_id']}" for selector in selectors} return { "schema_version": "enrichment-v0", "artifact_id": manifest["artifact_id"], - "attack_mapping": list(manifest.get("attack_mapping", [{"technique_id": "T0000", "technique": "synthetic fixture review", "scope": "review mapping only"}])), + "attack_mapping": list(manifest.get("attack_mapping", [{"technique_id": "T0000", "technique": "controlled-test fixture review", "scope": "review mapping only"}])), "event_id_mapping": event_mapping, "source_mapping": { "channel": manifest["telemetry_contract"]["source"], "fixture_host": fixture["host"], - "fixture_scope": "synthetic demo host", + "fixture_scope": "controlled-test demo host", }, - "field_mapping": dict(manifest.get("field_mapping", {"event_id": "event identifier", "action": "review action", "actor": "synthetic actor label"})), + "field_mapping": dict(manifest.get("field_mapping", {"event_id": "event identifier", "action": "review action", "actor": "controlled-test actor label"})), "confidence": manifest.get("confidence", "bounded-demo-high"), "severity": manifest.get("severity", "medium"), } @@ -907,14 +2459,14 @@ def _triage(manifest: dict[str, Any], signal: dict[str, Any], enrichment: dict[s return { "schema_version": "triage-summary-v0", "artifact_id": artifact_id, - "what_happened": manifest.get("triage_what_happened", f"A synthetic fixture represented the {artifact_id} review pattern."), + "what_happened": manifest.get("triage_what_happened", f"A controlled-test fixture represented the {artifact_id} review pattern."), "why_it_matters": manifest.get("triage_why_it_matters", "The pattern can matter during security review when backed by appropriate evidence."), "evidence_exists": [ "artifact intake record", "evidence graph", "telemetry contract check", - "positive and negative synthetic fixtures", - "fixture-derived synthetic signal", + "positive and negative controlled-test fixtures", + "fixture-derived controlled-test signal", "enrichment mapping", "ProofCard", "Claim Authority decision", @@ -949,14 +2501,14 @@ def _proofcard( "review_version": "v1", "owner_split": { "source_truth": manifest["source_owner"], - "behavior_truth": "bundled synthetic fixture", + "behavior_truth": "bundled controlled-test fixture", "platform_runtime_truth": "not asserted", "proof_authority": "not asserted by review engine", "rendering": "local generated files only", }, "telemetry_contract": telemetry, "controlled_validation": validation, - "synthetic_signal": signal, + "controlled_test_signal": signal, "enrichment": enrichment, "triage": triage, "allowed_claims": _allowed_claims_for(manifest), @@ -1002,7 +2554,7 @@ def _evidence_graph( _node("artifact-intake", "artifact_intake", intake["source_owner"], "PASS"), _node("telemetry-contract-check", "telemetry_contract_check", "hoxline-review-fixture", telemetry["result"].upper()), _node("controlled-validation", "controlled_validation", "hoxline-review-fixture", validation["result"].upper()), - _node("synthetic-signal", "synthetic_signal", "hoxline-review-fixture", "PASS"), + _node("controlled-test-signal", "controlled_test_signal", "hoxline-review-fixture", "PASS"), _node("proofcard", "proofcard", proofcard["proof_owner"], "PASS"), _node("claim-authority", "claim_authority", "hoxline", "PASS"), ] @@ -1015,8 +2567,8 @@ def _evidence_graph( "edges": [ {"from": "artifact-intake", "to": "telemetry-contract-check", "relationship": "declares assumptions"}, {"from": "telemetry-contract-check", "to": "controlled-validation", "relationship": "bounds fixture validation"}, - {"from": "controlled-validation", "to": "synthetic-signal", "relationship": "creates fixture-only signal"}, - {"from": "synthetic-signal", "to": "proofcard", "relationship": "summarized by"}, + {"from": "controlled-validation", "to": "controlled-test-signal", "relationship": "creates fixture-only signal"}, + {"from": "controlled-test-signal", "to": "proofcard", "relationship": "summarized by"}, {"from": "proofcard", "to": "claim-authority", "relationship": "constrains claims"}, ], "missing_evidence": proofcard["missing_evidence"], @@ -1072,7 +2624,7 @@ def _validate_review_fixture(fixture: dict[str, Any], manifest: dict[str, Any], expected = { "schema_version": "hoxline-demo-fixture-v0", "artifact_id": manifest["artifact_id"], - "fixture_kind": "synthetic-demo-only", + "fixture_kind": "controlled-test-demo-only", "safe_fixture": True, "endpoint_mutation": False, "runtime_required": False, @@ -1089,7 +2641,10 @@ def _validate_review_fixture(fixture: dict[str, Any], manifest: dict[str, Any], for event in events: if not isinstance(event, dict): raise ReviewBlocked("fixture events must be objects") - for field in ("event_id", "channel", "action", "actor"): + required_fields = manifest.get("telemetry_contract", {}).get( + "required_fields", [_manifest_event_selector(manifest), "channel", "action", "actor"] + ) + for field in required_fields: if field not in event: raise ReviewBlocked(f"fixture event missing field: {field}") if expected_detection and not _fixture_matches_manifest(fixture, manifest): @@ -1102,14 +2657,24 @@ def _fixture_matches_manifest(fixture: dict[str, Any], manifest: dict[str, Any]) if fixture.get("expected_detection") is not True: return False expected_ids = {int(event_id) for event_id in manifest.get("expected_event_ids", [])} + expected_keys = {str(value) for value in manifest.get("expected_event_keys", [])} + event_key_field = _manifest_event_selector(manifest) for event in fixture.get("events", []): if not isinstance(event, dict): continue - if int(event.get("event_id", 0)) in expected_ids and event.get("channel") in {"Windows Security", "Windows System", "TaskScheduler Operational"}: + event_id = event.get("event_id") + if event_id is not None and expected_ids and int(event_id) in expected_ids: + return True + if expected_keys and str(event.get(event_key_field, "")) in expected_keys: return True return False +def _manifest_event_selector(manifest: dict[str, Any]) -> str: + contract = manifest.get("telemetry_contract") if isinstance(manifest.get("telemetry_contract"), dict) else {} + return "event_id" if manifest.get("expected_event_ids") else str(contract.get("event_key_field") or "event_key") + + def _allowed_claims_for(manifest: dict[str, Any]) -> list[str]: requested = manifest.get("requested_claims", []) if requested: @@ -1123,26 +2688,50 @@ def _validate_manifest(manifest: dict[str, Any], manifest_path: Path, repo_root: for field in REQUIRED_MANIFEST_FIELDS: if field not in manifest: raise ReviewBlocked(f"manifest missing required field: {field}") + _require_exact_keys(manifest, MANIFEST_ALLOWED_FIELDS, "artifact manifest") if manifest["manifest_version"] != MANIFEST_VERSION: raise ReviewBlocked(f"manifest_version must be {MANIFEST_VERSION}") - if manifest["artifact_id"] != ARTIFACT_ID and manifest.get("artifact_family") != "synthetic-review-only": - raise ReviewBlocked("unknown artifact requires explicit synthetic-review-only artifact_family") + if manifest["artifact_id"] != ARTIFACT_ID and manifest.get("artifact_family") != "controlled-test-review-only": + raise ReviewBlocked("unknown artifact requires explicit controlled-test-review-only artifact_family") if manifest.get("public_safe_status") != PUBLIC_SAFE_STATUS: raise ReviewBlocked("public_safe_status must remain NOT_PUBLIC_SAFE") if manifest.get("human_review_required") is not True: raise ReviewBlocked("human_review_required must be true") if manifest.get("ai_disposition_authority") is not False: raise ReviewBlocked("ai_disposition_authority must be false") + if manifest.get("expected_review_outcome") not in {None, "BLOCKED"}: + raise ReviewBlocked("expected_review_outcome may only declare BLOCKED") _validate_manifest_flags(manifest) - _validate_telemetry_contract(manifest) _validate_claims(manifest) + scan_manifest = { + key: value + for key, value in manifest.items() + if key != "blocked_claim_classes" + and not (key == "expected_block_reason" and manifest.get("expected_review_outcome") == "BLOCKED") + } + _validate_recursive_boundaries(scan_manifest, "artifact manifest") _validate_no_private_markers(manifest, "manifest") + if manifest.get("expected_review_outcome") == "BLOCKED": + raise ReviewBlocked("boundary-contract artifact remains expected BLOCKED") + _validate_telemetry_contract(manifest) paths = _fixture_paths(manifest, repo_root) for label, path in paths.items(): if not path.is_file(): - raise ReviewBlocked(f"{label} fixture path missing: {path}") + declared = (manifest.get("fixture_paths") or {}).get(label, "missing") + raise ReviewBlocked(f"{label} fixture path missing: {declared}") _validate_fixture_path(path, repo_root) fixture = _load_json(path) + _require_exact_keys(fixture, FIXTURE_ALLOWED_FIELDS, f"{label} fixture") + if not isinstance(fixture.get("events"), list) or not fixture["events"]: + raise ReviewBlocked(f"{label} fixture events must be a non-empty list") + if not all( + isinstance(event, dict) + and event + and all(isinstance(key, str) and isinstance(value, (str, int, float, bool, type(None))) for key, value in event.items()) + for event in fixture["events"] + ): + raise ReviewBlocked(f"{label} fixture event shape is unsupported") + _validate_recursive_boundaries(fixture, f"{label} fixture") _validate_no_private_markers(fixture, f"{label} fixture") @@ -1163,18 +2752,25 @@ def _validate_telemetry_contract(manifest: dict[str, Any]) -> None: contract = manifest.get("telemetry_contract") if not isinstance(contract, dict): raise ReviewBlocked("telemetry_contract must be an object") - if contract.get("source") != "Windows Security EventChannel": - raise ReviewBlocked("telemetry_contract.source must be Windows Security EventChannel") + _require_exact_keys(contract, TELEMETRY_CONTRACT_ALLOWED_FIELDS, "telemetry_contract") + if not isinstance(contract.get("source"), str) or not contract.get("source"): + raise ReviewBlocked("telemetry_contract.source must be a non-empty fixture metadata source") event_ids = contract.get("event_ids") expected_event_ids = manifest.get("expected_event_ids") - if not isinstance(event_ids, list) or not event_ids: - raise ReviewBlocked("telemetry_contract.event_ids must be a non-empty list") + event_keys = contract.get("event_keys", []) + expected_event_keys = manifest.get("expected_event_keys", []) + if not isinstance(event_ids, list): + raise ReviewBlocked("telemetry_contract.event_ids must be a list") if sorted(event_ids) != sorted(expected_event_ids or []): raise ReviewBlocked("telemetry_contract.event_ids must match expected_event_ids") + if not isinstance(event_keys, list) or sorted(event_keys) != sorted(expected_event_keys or []): + raise ReviewBlocked("telemetry_contract.event_keys must match expected_event_keys") + if not event_ids and not event_keys: + raise ReviewBlocked("telemetry contract must declare event_ids or event_keys") rule_ids = contract.get("wazuh_rule_ids") expected_rule_ids = manifest.get("expected_rule_ids") - if not isinstance(rule_ids, list) or not rule_ids: - raise ReviewBlocked("telemetry_contract.wazuh_rule_ids must be a non-empty list") + if not isinstance(rule_ids, list): + raise ReviewBlocked("telemetry_contract.wazuh_rule_ids must be a list") if sorted(rule_ids) != sorted(expected_rule_ids or []): raise ReviewBlocked("telemetry_contract.wazuh_rule_ids must match expected_rule_ids") @@ -1183,7 +2779,17 @@ def _validate_claims(manifest: dict[str, Any]) -> None: requested_claims = manifest.get("requested_claims") if not isinstance(requested_claims, list): raise ReviewBlocked("requested_claims must be a list") - requested_text = "\n".join(str(item) for item in requested_claims) + expected_owners = { + "source_owner": "hawkinsoperations-detections", + "validation_owner": "hawkinsoperations-validation", + "platform_owner": "hawkinsoperations-platform", + "proof_owner": "hawkinsoperations-proof", + "product_owner": "hoxline", + } + for field, expected in expected_owners.items(): + if field in manifest and manifest.get(field) != expected: + raise ReviewBlocked(f"{field} must be the exact source-owned repository {expected}") + requested_text = json.dumps(requested_claims, sort_keys=True) for label, pattern in PROHIBITED_CLAIM_PATTERNS.items(): if pattern.search(requested_text): raise ReviewBlocked(f"requested claim is unsupported and blocked: {label}") @@ -1197,37 +2803,71 @@ def _fixture_paths(manifest: dict[str, Any], repo_root: Path) -> dict[str, Path] raw = manifest.get("fixture_paths") if not isinstance(raw, dict): raise ReviewBlocked("fixture_paths must be an object") + _require_exact_keys(raw, {"positive", "negative"}, "fixture_paths") try: - return { - "positive": _resolve_path(Path(str(raw["positive"])), repo_root), - "negative": _resolve_path(Path(str(raw["negative"])), repo_root), + paths = { + "positive": _contained_file( + repo_root, + raw["positive"], + "positive fixture path", + repo_root / "examples", + ), + "negative": _contained_file( + repo_root, + raw["negative"], + "negative fixture path", + repo_root / "examples", + ), } except KeyError as exc: raise ReviewBlocked(f"fixture_paths missing key: {exc.args[0]}") from exc + if paths["positive"] == paths["negative"]: + raise ReviewBlocked("positive and negative fixtures must be different files") + return paths def _validate_fixture_path(path: Path, repo_root: Path) -> None: resolved = path.resolve() allowed_roots = [(repo_root / "examples" / "demo").resolve(), (repo_root / "examples" / "review").resolve()] if not any(_is_relative_to(resolved, allowed) for allowed in allowed_roots): - raise ReviewBlocked(f"fixture path outside allowed example roots: {path}") + raise ReviewBlocked("fixture path outside allowed example roots") def _validate_no_private_markers(value: Any, label: str, path: str = "") -> None: if isinstance(value, dict): for key, item in value.items(): full = f"{path}.{key}" if path else str(key) - for pattern in PRIVATE_FIELD_PATTERNS: - if pattern.search(str(key)): - raise ReviewBlocked(f"{label} contains prohibited private/raw field marker") + normalized_key = _normalize_security_key(key) + private_key = any(token in normalized_key for token in PRIVATE_KEY_TOKENS) or any( + pattern.search(candidate) + for candidate in _decoded_text_variants(str(key), strict_base64=False) + for pattern in PRIVATE_FIELD_PATTERNS + ) + if private_key and not ( + _is_promotion_key(normalized_key) and _is_bounded_promotion_value(item) + ): + raise ReviewBlocked(f"{label} contains prohibited private/raw field marker") _validate_no_private_markers(item, label, full) elif isinstance(value, list): for index, item in enumerate(value): _validate_no_private_markers(item, label, f"{path}[{index}]") elif isinstance(value, str): - for pattern in PRIVATE_VALUE_PATTERNS: - if pattern.search(value): + for candidate in _decoded_text_variants(value): + if ABSOLUTE_LOCAL_PATH.search(candidate) or _looks_like_local_or_escaping_path(candidate): + raise ReviewBlocked(f"{label} contains an absolute local path") + normalized_candidate = _normalize_security_key(candidate) + if any(token in normalized_candidate for token in PRIVATE_KEY_TOKENS): raise ReviewBlocked(f"{label} contains prohibited private/raw value marker") + for pattern in PRIVATE_VALUE_PATTERNS: + if pattern.search(candidate): + raise ReviewBlocked(f"{label} contains prohibited private/raw value marker") + stripped = candidate.strip() + if stripped.startswith(("{", "[")): + try: + nested = json.loads(stripped, object_pairs_hook=_unique_json_object) + except (json.JSONDecodeError, ReviewEngineError): + continue + _validate_no_private_markers(nested, label, path) def _machine_state( @@ -1245,7 +2885,7 @@ def _machine_state( "engine_version": ENGINE_VERSION, "run_id": run_id, "artifact_id": manifest.get("artifact_id", "UNKNOWN") if manifest else "UNKNOWN", - "manifest_path": str(manifest_path), + "manifest_path": _display_path(manifest_path), "stages": stages, "outputs": outputs, "final_status": final_status, @@ -1255,7 +2895,7 @@ def _machine_state( "blocked_claims": _blocked_claims(), "proof_boundary": manifest.get("proof_boundary", "fixture-only; not public proof") if manifest else "manifest blocked before proof boundary established", "runtime_boundary": manifest.get("runtime_boundary", "runtime prohibited") if manifest else "runtime prohibited", - "signal_boundary": manifest.get("signal_boundary", "synthetic fixture signal only") if manifest else "synthetic fixture signal only", + "signal_boundary": manifest.get("signal_boundary", "controlled-test fixture signal only") if manifest else "controlled-test fixture signal only", "public_safe_status": PUBLIC_SAFE_STATUS, "human_review_required": True, "ai_disposition_authority": False, @@ -1266,7 +2906,6 @@ def _machine_state( "public_proof_promoted": False, "lifetime_ledger_changed": False, "next_gate": manifest.get("next_gate", "human_review_gate") if manifest else "manifest_fix_required", - "created_at": datetime.now(timezone.utc).isoformat(), "product": PRODUCT, } @@ -1279,7 +2918,7 @@ def _stage(stage_name: str, status: str, output_ref: str | None, summary: str, f "output_ref": output_ref, "proof_boundary": "fixture-only; NOT_PUBLIC_SAFE; not runtime proof", "failure_mode": failure_mode, - "claim_boundary": "unsupported public, runtime, production, customer, SOCaaS, autonomous, approval, authorization, and case-closure claims block", + "claim_boundary": "claims block unsupported public, runtime, production, customer, SOCaaS, autonomous, approval, authorization, and case-closure wording", "summary": summary, } @@ -1288,9 +2927,9 @@ def _pass_stages(outputs: dict[str, str]) -> list[dict[str, Any]]: summaries = { "artifact_intake": "manifest accepted and intake created", "evidence_graph": "evidence graph linked local fixture review nodes", - "telemetry_contract_check": "Windows Security EventChannel event and rule metadata checked", + "telemetry_contract_check": "declared fixture selector and source metadata checked", "controlled_validation": "positive and negative bundled fixtures validated", - "synthetic_signal": "safe fixture signal simulated without endpoint mutation", + "controlled_test_signal": "safe fixture signal simulated without endpoint mutation", "enrichment": "ATT&CK, event, source, and field mapping attached", "triage": "reviewer-readable triage summary generated", "proofcard": "ProofCard rendered under fixture-only ceiling", @@ -1323,7 +2962,7 @@ def _reviewer_pack(manifest: dict[str, Any], state: dict[str, Any]) -> str: "", "## What Happened", "", - f"Artifact `{state['artifact_id']}` ran through artifact intake, evidence graph, telemetry contract check, controlled validation, synthetic signal, enrichment, triage, ProofCard, Claim Authority, reviewer pack, and machine-state stages.", + f"Artifact `{state['artifact_id']}` ran through artifact intake, evidence graph, telemetry contract check, controlled validation, controlled-test signal, enrichment, triage, ProofCard, Claim Authority, reviewer pack, and machine-state stages.", "", "## Stage Table", "", @@ -1349,7 +2988,7 @@ def _reviewer_pack(manifest: dict[str, Any], state: dict[str, Any]) -> str: "## What This Proves", "", "- The artifact manifest can be reviewed by deterministic local Hoxline stages.", - "- The engine can generate replayable machine-state and reviewer artifacts from public-safe synthetic fixtures.", + "- The engine can generate replayable machine-state and reviewer artifacts from public-safe controlled-test fixtures.", "- Claim Authority blocks unsupported public, runtime, production, customer, SOCaaS, autonomous, approval, authorization, and case-closure claims.", "", "## What This Does Not Prove", @@ -1426,20 +3065,22 @@ def _verify_pass_outputs(run_dir: Path, state: dict[str, Any], errors: list[str] manifest = _load_json(run_dir / "artifact-manifest.json") telemetry = _load_json(run_dir / "telemetry-contract-check.json") validation = _load_json(run_dir / "validation-result.json") - signal = _load_json(run_dir / "synthetic-signal.json") + signal = _load_json(run_dir / "controlled-test-signal.json") proofcard = _load_json(run_dir / "proofcard.json") claim_authority = _load_json(run_dir / "claim-authority.json") reviewer_pack = (run_dir / "reviewer-pack.md").read_text(encoding="utf-8") - if telemetry.get("required_source") != "Windows Security EventChannel": - errors.append("telemetry contract must use Windows Security EventChannel") + if telemetry.get("required_source") != manifest.get("telemetry_contract", {}).get("source"): + errors.append("telemetry contract source must match artifact manifest") if sorted(telemetry.get("event_ids", [])) != sorted(manifest.get("expected_event_ids", [])): errors.append("telemetry contract event IDs must match artifact manifest") + if sorted(telemetry.get("event_keys", [])) != sorted(manifest.get("expected_event_keys", [])): + errors.append("telemetry contract event keys must match artifact manifest") if sorted(telemetry.get("wazuh_rule_family", [])) != sorted(manifest.get("expected_rule_ids", [])): errors.append("telemetry contract Wazuh rule metadata must match artifact manifest") if validation.get("result") != "pass" or validation.get("endpoint_mutation") is not False: errors.append("controlled validation must pass without endpoint mutation") if signal.get("source") != "safe bundled fixture" or signal.get("detection_fired") is not True: - errors.append("synthetic signal must fire only from safe bundled fixture") + errors.append("controlled-test signal must fire only from safe bundled fixture") if proofcard.get("public_safe_status") != PUBLIC_SAFE_STATUS: errors.append("ProofCard must preserve NOT_PUBLIC_SAFE") blocked = {item.get("claim") for item in claim_authority.get("blocked_claims", [])} @@ -1475,16 +3116,33 @@ def _blocked_claims() -> list[dict[str, str]]: def _prepare_output_dir(output_dir: Path, force: bool) -> None: + resolved = output_dir.resolve() + if resolved == Path(resolved.anchor) or resolved == Path.cwd().resolve(): + raise ReviewEngineError("output directory must be a dedicated run directory") + if output_dir.is_symlink(): + raise ReviewEngineError("output directory must not be a symbolic link") + marker = output_dir / ".hoxline-review-output-v1" if output_dir.exists(): if not force: - raise ReviewEngineError(f"output directory already exists: {output_dir}") + raise ReviewEngineError(f"output directory already exists: {_display_path(output_dir)}") + if not marker.is_file(): + raise ReviewEngineError("refusing to replace a directory not created by Hoxline Review Engine") shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=False) + marker.write_text("generated review output; safe to replace with --force\n", encoding="utf-8") + + +def _sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() def _write_file_map(output_dir: Path, file_map: dict[str, Any]) -> None: for name, value in file_map.items(): - path = output_dir / name + normalized = _normalize_repo_relative_path(name, "generated output path") + path = (output_dir / normalized).resolve() + if not _is_relative_to(path, output_dir.resolve()): + raise ReviewEngineError("generated output path escapes its run root") + path.parent.mkdir(parents=True, exist_ok=True) if isinstance(value, str): path.write_text(value, encoding="utf-8") else: @@ -1493,16 +3151,104 @@ def _write_file_map(output_dir: Path, file_map: dict[str, Any]) -> None: def _private_output_hits(run_dir: Path) -> list[str]: hits: list[str] = [] - for path in run_dir.iterdir(): + for path in run_dir.rglob("*"): if not path.is_file() or path.suffix not in {".json", ".md"}: continue text = path.read_text(encoding="utf-8") + if ABSOLUTE_LOCAL_PATH.search(text): + hits.append(f"{path.name}:absolute-local-path") for pattern in PRIVATE_VALUE_PATTERNS: if pattern.search(text): hits.append(f"{path.name}:{pattern.pattern}") return hits +def _display_path(path: Path) -> str: + resolved = path.resolve() + repo_root = Path(__file__).resolve().parents[2] + if _is_relative_to(resolved, repo_root): + return resolved.relative_to(repo_root).as_posix() + return resolved.name + + +def _safe_run_identity(name: str) -> str: + normalized = _normalize_security_key(name) + if any(token in normalized for token in PRIVATE_KEY_TOKENS) or _string_has_unsafe_claim(name): + return f"sanitized-run-{hashlib.sha256(name.encode('utf-8')).hexdigest()[:12]}" + return name + + +def verify_tracked_vocabulary(repo_root: Path) -> list[str]: + root = repo_root.resolve() + try: + result = subprocess.run( + [ + "git", "-C", str(root), "ls-files", "--cached", "--others", + "--exclude-standard", "-z", + ], + check=False, + capture_output=True, + env=sanitized_git_env(), + ) + except OSError as exc: + return [f"tracked vocabulary inventory failed: {exc}"] + if result.returncode != 0: + detail = result.stderr.decode("utf-8", errors="replace").strip() + return [f"tracked vocabulary inventory failed: {detail or 'git ls-files failed'}"] + deleted_result = subprocess.run( + ["git", "-C", str(root), "ls-files", "--deleted", "-z"], + check=False, + capture_output=True, + env=sanitized_git_env(), + ) + if deleted_result.returncode != 0: + detail = deleted_result.stderr.decode("utf-8", errors="replace").strip() + return [f"tracked deleted-path inventory failed: {detail or 'git ls-files --deleted failed'}"] + try: + tracked = result.stdout.decode("utf-8", errors="strict").split("\0") + deleted = { + item + for item in deleted_result.stdout.decode("utf-8", errors="strict").split("\0") + if item + } + except UnicodeDecodeError as exc: + return [f"tracked filename inventory is not strict UTF-8: {exc}"] + + errors: list[str] = [] + for relative in sorted(item for item in tracked if item): + if relative in deleted: + continue + normalized_relative = _security_scan_text(relative) + if RETIRED_VOCABULARY in normalized_relative.casefold(): + errors.append(f"tracked filename contains retired vocabulary: {relative}") + path = root / Path(relative) + try: + if not path.is_file(): + errors.append(f"tracked path is missing or not a regular file: {relative}") + continue + suffix = path.suffix.casefold() + if suffix in TRACKED_BINARY_EXTENSIONS: + continue + if suffix not in TRACKED_TEXT_EXTENSIONS and path.name not in TRACKED_TEXT_FILENAMES: + errors.append(f"tracked file type is not explicitly classified: {relative}") + continue + payload = path.read_bytes() + except OSError as exc: + errors.append(f"tracked file read failed: {relative}: {exc}") + continue + if b"\0" in payload: + errors.append(f"tracked text file contains NUL bytes: {relative}") + continue + try: + text = payload.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + errors.append(f"tracked text file is not strict UTF-8: {relative}: {exc}") + continue + if RETIRED_VOCABULARY in _security_scan_text(text).casefold(): + errors.append(f"tracked file contains retired vocabulary: {relative}") + return errors + + def _resolve_path(path: Path, base: Path) -> Path: if path.is_absolute(): return path @@ -1518,8 +3264,11 @@ def _is_relative_to(path: Path, parent: Path) -> bool: def _load_json(path: Path) -> dict[str, Any]: - with path.open("r", encoding="utf-8") as handle: - data = json.load(handle) + try: + with path.open("r", encoding="utf-8") as handle: + data = json.load(handle, object_pairs_hook=_unique_json_object) + except json.JSONDecodeError as exc: + raise ReviewEngineError(f"input is not valid JSON: {_display_path(path)}") from exc if not isinstance(data, dict): - raise ReviewEngineError(f"input must be a JSON object: {path}") + raise ReviewEngineError(f"input must be a JSON object: {_display_path(path)}") return deepcopy(data) diff --git a/tests/fixtures/case_growth/README.md b/tests/fixtures/case_growth/README.md index 2c70b4b..20e61f9 100644 --- a/tests/fixtures/case_growth/README.md +++ b/tests/fixtures/case_growth/README.md @@ -1,3 +1,3 @@ # Case Growth Fixture -Minimal seven-repo fixture for the Hoxline Case Growth Index v0 tests. The fixture uses public release-safe synthetic files only and intentionally includes missing proof/card states. +Minimal seven-repo fixture for the Hoxline Case Growth Index v0 tests. The fixture uses public release-safe controlled-test files only and intentionally includes missing proof/card states. diff --git a/tests/test_action_contract.py b/tests/test_action_contract.py index bf5a300..150f5ab 100644 --- a/tests/test_action_contract.py +++ b/tests/test_action_contract.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +import re from claimfirewall.policy import load_policy from claimfirewall.scanner import scan_paths @@ -36,3 +37,35 @@ def test_docs_scan_cleanly() -> None: findings = scan_paths([ROOT / "README.md", ROOT / "CLAIM_BOUNDARY.md"], policy) assert findings == [] + + +def test_ci_uses_immutable_sibling_revisions_and_all_required_trust_checks() -> None: + workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + refs = re.findall(r"^\s+ref:\s+([0-9a-f]{40})\s*$", workflow, flags=re.MULTILINE) + reviewed_manifest = "governance/CONVERGENCE_SOURCE_MANIFEST.json" + command_center_ref = "5c6127f5acc1031bae2528df3ce1f197da882100" + assert refs == [command_center_ref] + assert ( + "HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA: " + f"{command_center_ref}" + ) in workflow + assert reviewed_manifest in workflow + assert "Resolve reviewed sibling revisions" in workflow + for output_name in ("detections", "validation", "platform", "proof", "website"): + assert f"steps.reviewed.outputs.{output_name}" in workflow + assert '**reviewed' in workflow + assert "ref: main" not in workflow + assert "ref: feature/" not in workflow + assert "persist-credentials: false" in workflow + assert "permissions:\n contents: read" in workflow + for required in ( + "python -B -m unittest discover -s tests", + "python -B -m pytest", + "case-growth index", + "case-growth verify", + "case-growth diff", + "review batch run", + "review batch verify", + "git diff --check", + ): + assert required in workflow diff --git a/tests/test_blocked_claims.py b/tests/test_blocked_claims.py index d74b18d..405e802 100644 --- a/tests/test_blocked_claims.py +++ b/tests/test_blocked_claims.py @@ -45,8 +45,9 @@ def test_failing_example_reports_blocked_claims() -> None: assert all(finding.line_number > 0 for finding in findings) -def test_directory_scan_and_exclude() -> None: +def test_directory_scan_and_exclude(tmp_path, monkeypatch) -> None: policy = load_policy(POLICY) + monkeypatch.chdir(tmp_path) findings = scan_paths([ROOT], policy, exclude_patterns=["examples/fail.md", "policy/blocked_claims.yml"]) diff --git a/tests/test_case_growth_index_v0.py b/tests/test_case_growth_index_v0.py index 872c1e1..25951fb 100644 --- a/tests/test_case_growth_index_v0.py +++ b/tests/test_case_growth_index_v0.py @@ -1,12 +1,16 @@ from __future__ import annotations import contextlib +import copy import io import json +import os import shutil +import subprocess import sys import tempfile import unittest +from unittest import mock from pathlib import Path try: @@ -14,7 +18,17 @@ except ImportError: # pragma: no cover - exercised only when optional test dep is absent jsonschema = None -from hoxline.case_growth.collector import BOUNDARY, ROW_FIELDS, build_case_growth_index +from hoxline.case_growth.collector import ( + BOUNDARY, + ROW_FIELDS, + _content_normalized_cases, + _reproducibility_hash, + build_case_growth_index, + diff_case_growth_snapshot, + verify_selected_source_checkout, + verify_case_growth_snapshot, +) +from hoxline.case_growth.discovery import REPO_NAMES, repo_dirty, repo_head_sha, repo_origin from hoxline.case_growth.render import render_case_growth_markdown from hoxline.cli import main @@ -22,7 +36,84 @@ ROOT = Path(__file__).resolve().parents[1] FIXTURE_ROOT = ROOT / "tests" / "fixtures" / "case_growth" / "org" SAMPLE_JSON = ROOT / "examples" / "case-growth" / "sample-case-growth-index.json" +CURRENT_JSON = ROOT / "examples" / "case-growth" / "current-case-growth-index.json" SCHEMA = ROOT / "schemas" / "case-growth-index-v0.schema.json" +CI_WORKFLOW = ROOT / ".github" / "workflows" / "ci.yml" + + +def _git(repo: Path, *args: str, input_text: str | None = None) -> str: + result = subprocess.run( + ["git", "-C", str(repo), *args], + input=input_text, + text=True, + capture_output=True, + check=True, + ) + return result.stdout.strip() + + +def _write_source_selection_manifest( + org_root: Path, + selected_repository: str, + revision: str, + reviewed_tree: str, +) -> None: + command_center = org_root / ".github" + command_authority_path = command_center / "governance" / "COMMAND_CENTER_INVARIANTS.json" + command_authority_path.parent.mkdir(parents=True) + command_authority_path.write_text( + json.dumps({"schema": "command-center-invariants-v1", "test_fixture": True}), + encoding="utf-8", + ) + _git(command_center, "init") + _git(command_center, "config", "user.name", "Hoxline Test") + _git(command_center, "config", "user.email", "hoxline-test@example.invalid") + _git(command_center, "remote", "add", "origin", "https://github.com/HawkinsOperations/.github.git") + _git(command_center, "add", "governance/COMMAND_CENTER_INVARIANTS.json") + _git(command_center, "commit", "-m", "command authority") + command_content_revision = _git(command_center, "rev-parse", "HEAD") + + entries: list[dict[str, object]] = [ + { + "repository": ".github", + "canonical_repository": "HawkinsOperations/.github", + "revision_source": "github_event_sha", + "authority_content_revision": command_content_revision, + "tree_source": "github_event_tree", + } + ] + for repository in REPO_NAMES: + if repository == ".github": + continue + entries.append( + { + "repository": repository, + "canonical_repository": f"HawkinsOperations/{repository}", + "revision": revision if repository == selected_repository else "0" * 40, + "authority_content_revision": ( + revision if repository == selected_repository else "0" * 40 + ), + "reviewed_tree_sha": reviewed_tree if repository == selected_repository else "0" * 40, + } + ) + manifest = { + "schema": "hawkinsoperations-convergence-source-manifest-v1", + "manifest_id": "TEST_EXACT_SEVEN_SOURCE_SELECTION", + "repositories": entries, + "constraints": { + "exact_repository_count": 7, + "read_only": True, + "default_branch_fallback": False, + "require_detached_exact_revision": True, + "record_checked_revisions": True, + "consumer_outputs_are_not_authority": True, + "proof_ceiling": "CONTROLLED_REPO_CONVERGENCE_AND_LOCAL_FIXTURE_REVIEW_ONLY", + }, + } + manifest_path = org_root / ".github" / "governance" / "CONVERGENCE_SOURCE_MANIFEST.json" + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + _git(command_center, "add", "governance/CONVERGENCE_SOURCE_MANIFEST.json") + _git(command_center, "commit", "-m", "source selection") def row_by_id(index: dict[str, object], case_id: str) -> dict[str, object]: @@ -99,13 +190,117 @@ def derived_health(summary: dict[str, int]) -> dict[str, float]: class CaseGrowthIndexV0Tests(unittest.TestCase): - def setUp(self) -> None: - self.index = build_case_growth_index(FIXTURE_ROOT, generated_at="2026-06-27T00:00:00Z") - self.rows = self.index["cases"] - assert isinstance(self.rows, list) + def test_ci_checks_the_exact_pr_head_and_all_seven_repositories(self) -> None: + workflow = CI_WORKFLOW.read_text(encoding="utf-8") + pr_head = "${{ github.event.pull_request.head.sha || github.sha }}" + + def issues(value: str) -> list[str]: + findings = [] + if value.count("uses: actions/checkout@") != 7: + findings.append("checkout_count") + for required in ( + f"HAWKINS_HOXLINE_EVENT_SHA: {pr_head}", + f"ref: {pr_head}", + '"hoxline": os.environ["HAWKINS_HOXLINE_EVENT_SHA"]', + "if sha != immutable[name]:", + ): + if required not in value: + findings.append(required) + return findings + + self.assertEqual([], issues(workflow)) + + merge_ref_attack = workflow.replace(f"ref: {pr_head}", "ref: ${{ github.sha }}", 1) + self.assertNotEqual([], issues(merge_ref_attack)) + + def test_repo_origin_rejects_ambient_instead_of_laundering(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = Path(temp_dir) / "repo" + repo.mkdir() + _git(repo, "init") + stored_origin = "C:/hostile/local-hoxline" + canonical = "https://github.com/HawkinsOperations/hoxline.git" + _git(repo, "remote", "add", "origin", stored_origin) + hostile_env = { + "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": f"url.{canonical}.insteadOf", + "GIT_CONFIG_VALUE_0": stored_origin, + } + with mock.patch.dict(os.environ, hostile_env, clear=False): + effective = _git(repo, "remote", "get-url", "origin") + self.assertEqual(effective, canonical) + self.assertEqual(repo_origin(repo), stored_origin) + + def test_repo_origin_rejects_empty_duplicate_in_both_orders(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = Path(temp_dir) / "repo" + repo.mkdir() + _git(repo, "init") + canonical = "https://github.com/HawkinsOperations/hoxline.git" + _git(repo, "config", "--add", "remote.origin.url", canonical) + _git(repo, "config", "--add", "remote.origin.url", "") + self.assertEqual(repo_origin(repo), "UNKNOWN") + _git(repo, "config", "--unset-all", "remote.origin.url") + _git(repo, "config", "--add", "remote.origin.url", "") + _git(repo, "config", "--add", "remote.origin.url", canonical) + self.assertEqual(repo_origin(repo), "UNKNOWN") + + def test_git_identity_ignores_ambient_repository_and_index_redirection(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + base = Path(temp_dir) + target = base / "target" + decoy = base / "decoy" + for repo, origin, content in ( + (target, "C:/hostile/target", "target\n"), + (decoy, "https://github.com/HawkinsOperations/hoxline.git", "decoy\n"), + ): + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + _git(repo, "remote", "add", "origin", origin) + (repo / "tracked.txt").write_text(content, encoding="utf-8") + _git(repo, "add", "tracked.txt") + _git(repo, "commit", "-m", "fixture") + + target_head = _git(target, "rev-parse", "HEAD") + decoy_head = _git(decoy, "rev-parse", "HEAD") + self.assertNotEqual(target_head, decoy_head) + with mock.patch.dict( + os.environ, + { + "GIT_DIR": str(decoy / ".git"), + "GIT_WORK_TREE": str(decoy), + "GIT_INDEX_FILE": str(decoy / ".git" / "index"), + "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": "core.repositoryformatversion", + "GIT_CONFIG_VALUE_0": "0", + }, + clear=False, + ): + self.assertEqual(repo_origin(target), "C:/hostile/target") + self.assertEqual(repo_head_sha(target), target_head) + + clean_index = base / "clean-index" + shutil.copy2(target / ".git" / "index", clean_index) + (target / "tracked.txt").write_text("changed\n", encoding="utf-8") + _git(target, "add", "tracked.txt") + (target / "tracked.txt").write_text("target\n", encoding="utf-8") + with mock.patch.dict( + os.environ, + {"GIT_INDEX_FILE": str(clean_index)}, + clear=False, + ): + self.assertTrue(repo_dirty(target)) + + @classmethod + def setUpClass(cls) -> None: + cls.index = build_case_growth_index(FIXTURE_ROOT, generated_at="2026-06-27T00:00:00Z") + cls.rows = cls.index["cases"] + assert isinstance(cls.rows, list) def test_fixture_repo_root_loads(self) -> None: - self.assertEqual(self.index["schema_version"], "case-growth-index-v0") + self.assertEqual(self.index["schema_version"], "case-growth-index-v1") self.assertGreaterEqual(self.index["summary"]["cases_total"], 1) def test_cli_prints_json(self) -> None: @@ -114,7 +309,7 @@ def test_cli_prints_json(self) -> None: status = main(["case-growth", "index", "--repo-root", str(FIXTURE_ROOT), "--format", "json"]) self.assertEqual(status, 0) payload = json.loads(stdout.getvalue()) - self.assertEqual(payload["schema_version"], "case-growth-index-v0") + self.assertEqual(payload["schema_version"], "case-growth-index-v1") def test_cli_prints_markdown(self) -> None: stdout = io.StringIO() @@ -123,8 +318,48 @@ def test_cli_prints_markdown(self) -> None: self.assertEqual(status, 0) self.assertIn("| case_id | source | validation | runtime_candidate |", stdout.getvalue()) - def test_schema_validates_sample_json(self) -> None: - sample = json.loads(SAMPLE_JSON.read_text(encoding="utf-8")) + def test_cli_writes_content_bound_json_markdown_pair(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + pair_base = Path(temp_dir) / "case-growth" + with contextlib.redirect_stdout(io.StringIO()): + status = main( + [ + "case-growth", + "index", + "--repo-root", + str(FIXTURE_ROOT), + "--format", + "json", + "--paired-output-base", + str(pair_base), + ] + ) + self.assertEqual(status, 0) + payload = json.loads(pair_base.with_suffix(".json").read_text(encoding="utf-8")) + self.assertEqual( + pair_base.with_suffix(".md").read_text(encoding="utf-8"), + render_case_growth_markdown(payload), + ) + pair_base.with_suffix(".md").write_text("tampered\n", encoding="utf-8") + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + verify_status = main( + [ + "case-growth", + "verify", + "--repo-root", + str(FIXTURE_ROOT), + "--snapshot", + str(pair_base.with_suffix(".json")), + "--format", + "json", + ] + ) + self.assertEqual(verify_status, 1) + self.assertIn("not the exact render", stdout.getvalue()) + + def test_schema_validates_fail_closed_diagnostic_index(self) -> None: + sample = self.index schema = json.loads(SCHEMA.read_text(encoding="utf-8")) if jsonschema is not None: jsonschema.validate(sample, schema) @@ -132,6 +367,33 @@ def test_schema_validates_sample_json(self) -> None: for field in schema["required"]: self.assertIn(field, sample) + def test_schema_validates_checked_current_and_historical_states(self) -> None: + schema = json.loads(SCHEMA.read_text(encoding="utf-8")) + current = json.loads(CURRENT_JSON.read_text(encoding="utf-8")) + historical = copy.deepcopy(current) + historical["historical_snapshot"] = True + historical["current_authority"] = False + historical["snapshot_state"]["historical_snapshot"] = True + historical["snapshot_state"]["current_authority"] = False + if jsonschema is not None: + jsonschema.validate(current, schema) + jsonschema.validate(historical, schema) + else: + self.assertFalse(current["historical_snapshot"]) + self.assertTrue(current["current_authority"]) + self.assertTrue(historical["historical_snapshot"]) + self.assertFalse(historical["current_authority"]) + + def test_schema_rejects_historical_snapshot_claiming_current_authority(self) -> None: + if jsonschema is None: + self.skipTest("jsonschema is not installed") + schema = json.loads(SCHEMA.read_text(encoding="utf-8")) + contradictory = json.loads(CURRENT_JSON.read_text(encoding="utf-8")) + contradictory["historical_snapshot"] = True + contradictory["current_authority"] = True + with self.assertRaises(jsonschema.ValidationError): + jsonschema.validate(contradictory, schema) + def test_rows_include_every_required_field(self) -> None: for row in self.rows: for field in ROW_FIELDS: @@ -221,10 +483,8 @@ def test_closed_count_only_counts_explicit_closed(self) -> None: self.assertTrue(all(row["case_state"] != "CLOSED" for row in self.rows)) def test_website_evidence_never_creates_proof_status(self) -> None: - website_only = row_by_id(self.index, "HO-DET-999") - self.assertEqual(website_only["source_status"], "NOT_FOUND") - self.assertEqual(website_only["proof_record_status"], "NOT_PROVEN") - self.assertEqual(website_only["proofcard_status"], "NOT_PROVEN") + case_ids = {row["case_id"] for row in self.rows} + self.assertNotIn("HO-DET-999", case_ids) def test_metrics_available_for_hox_gauntlet_fixture(self) -> None: gauntlet = row_by_id(self.index, "HOX-GAUNTLET-001") @@ -251,6 +511,8 @@ def test_output_contains_at_least_one_case_row(self) -> None: def test_generated_markdown_includes_table_headers(self) -> None: markdown = render_case_growth_markdown(self.index) self.assertIn("| Metric | Count |", markdown) + self.assertIn("## Source Revisions", markdown) + self.assertIn("## Convergence Findings", markdown) self.assertIn("## Case Growth Health", markdown) self.assertIn("| Health metric | Value |", markdown) self.assertIn("| Top bottleneck |", markdown) @@ -264,6 +526,604 @@ def test_current_sample_json_has_summary_cases_boundary(self) -> None: self.assertIn("cases", sample) self.assertIn("boundary", sample) + def test_v1_source_revisions_are_exactly_seven_and_sanitized(self) -> None: + revisions = self.index["source_revisions"] + self.assertEqual(len(revisions), 7) + self.assertEqual({item["repository"] for item in revisions}, set(REPO_NAMES)) + serialized = json.dumps(self.index) + self.assertNotIn("C:\\\\Raylee\\\\", serialized) + self.assertNotIn("C:/Raylee/", serialized) + for item in revisions: + self.assertIn("authority_role", item) + self.assertIn("source_commit_sha", item) + self.assertIn("source_file_sha256", item) + self.assertIn("source_freshness_state", item) + self.assertIn("next_legal_action", item) + + def test_reproducibility_hash_ignores_only_generated_at(self) -> None: + first = build_case_growth_index(FIXTURE_ROOT, generated_at="2026-06-27T00:00:00Z") + second = build_case_growth_index(FIXTURE_ROOT, generated_at="2030-01-01T00:00:00Z") + self.assertEqual(first["reproducibility_sha256"], second["reproducibility_sha256"]) + + def test_verify_rejects_absolute_path_duplicate_and_promotion(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["repo_root"] = r"C:\Raylee\Repo\HawkinsOperations" + hostile["cases"].append(json.loads(json.dumps(hostile["cases"][0]))) + hostile["cases"][0]["public_safe_status"] = "PUBLIC_SAFE" + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("absolute local path" in error for error in errors)) + self.assertTrue(any("duplicate case ID" in error for error in errors)) + self.assertTrue(any("unauthorized public-safe status" in error for error in errors)) + + def test_verify_rejects_forged_counts_and_historical_current_conflict(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["summary"]["proof_records_count"] += 99 + hostile["historical_snapshot"] = True + hostile["current_authority"] = True + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertIn("snapshot cannot be both historical_snapshot=true and current_authority=true", errors) + self.assertTrue(any("summary counts disagree" in error for error in errors)) + + def test_diff_classifies_explicit_historical_changes_as_context(self) -> None: + historical = json.loads(json.dumps(self.index)) + historical["historical_snapshot"] = True + historical["current_authority"] = False + historical["summary"]["proof_records_count"] = -1 + report = diff_case_growth_snapshot(FIXTURE_ROOT, historical) + change = next(item for item in report["changes"] if item["field"] == "summary.proof_records_count") + self.assertEqual(change["classification"], "EXPECTED_HISTORICAL_CONTEXT") + + def test_diff_classifies_head_rewrite_with_same_content_as_observation_only(self) -> None: + snapshot = json.loads(json.dumps(self.index)) + hoxline_revision = next(item for item in snapshot["source_revisions"] if item["repository"] == "hoxline") + hoxline_revision["source_commit_sha"] = "a" * 40 + hoxline_revision["source_observed_head_sha"] = "a" * 40 + hoxline_revision["current_observed_head_sha"] = "a" * 40 + report = diff_case_growth_snapshot(FIXTURE_ROOT, snapshot) + change = next( + item + for item in report["changes"] + if item["source_owner"] == "hoxline" and item["field"] == "source_commit_sha" + ) + self.assertEqual(change["classification"], "OBSERVATION_ONLY_CONTENT_CURRENT") + self.assertTrue(report["next_legal_action"].startswith("none;")) + + def test_case_content_normalization_ignores_only_rewritten_commit_clock(self) -> None: + before = json.loads(json.dumps(self.rows)) + after = json.loads(json.dumps(self.rows)) + after[0]["last_updated"] = "2035-01-02T03:04:05+00:00" + self.assertEqual(_content_normalized_cases(before), _content_normalized_cases(after)) + + after[0]["source_status"] = "FORGED_SOURCE_STATUS" + self.assertNotEqual(_content_normalized_cases(before), _content_normalized_cases(after)) + + def test_cli_verify_fails_closed_on_hostile_snapshot(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["repo_root"] = r"C:\Users\operator\snapshot.json" + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + with tempfile.TemporaryDirectory() as temp_dir: + snapshot = Path(temp_dir) / "hostile.json" + snapshot.write_text(json.dumps(hostile), encoding="utf-8") + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + status = main( + [ + "case-growth", + "verify", + "--repo-root", + str(FIXTURE_ROOT), + "--snapshot", + str(snapshot), + "--format", + "json", + ] + ) + self.assertEqual(status, 1) + payload = json.loads(stdout.getvalue()) + self.assertEqual(payload["status"], "FAIL") + self.assertTrue(any("absolute local path" in error for error in payload["errors"])) + + def test_cli_diff_emits_reviewer_readable_json(self) -> None: + historical = json.loads(json.dumps(self.index)) + historical["historical_snapshot"] = True + historical["current_authority"] = False + historical["summary"]["proof_records_count"] = -1 + with tempfile.TemporaryDirectory() as temp_dir: + snapshot = Path(temp_dir) / "historical.json" + snapshot.write_text(json.dumps(historical), encoding="utf-8") + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + status = main( + [ + "case-growth", + "diff", + "--repo-root", + str(FIXTURE_ROOT), + "--snapshot", + str(snapshot), + "--format", + "json", + ] + ) + self.assertEqual(status, 0) + payload = json.loads(stdout.getvalue()) + self.assertEqual(payload["schema_version"], "case-growth-diff-v1") + self.assertTrue(payload["changes"]) + + def test_verify_requires_exact_unique_seven_repository_set(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["source_revisions"][-1] = json.loads(json.dumps(hostile["source_revisions"][0])) + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("repository names must be unique" in error for error in errors)) + self.assertTrue(any("exact seven-repository set" in error for error in errors)) + + def test_verify_rejects_forged_runtime_and_claim_authority_fields(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["cases"][0]["runtime_candidate_status"] = "RUNTIME_ACTIVE" + hostile["cases"][0]["claim_authority_status"] = "ANALYST_APPROVED" + hostile["cases"][0]["next_gate"] = "final authorization and case closure" + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("unauthorized runtime_candidate_status" in error for error in errors)) + self.assertTrue(any("unauthorized claim_authority_status" in error for error in errors)) + self.assertTrue(any("final authorization wording" in error for error in errors)) + self.assertTrue(any("case closure wording" in error for error in errors)) + + def test_missing_authorization_metric_token_is_bounded_context(self) -> None: + bounded = json.loads(json.dumps(self.index)) + bounded["cases"][0]["notes"] = ["missing_human_final_authorization"] + bounded["reproducibility_sha256"] = _reproducibility_hash(bounded) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, bounded) + self.assertFalse(any("final authorization wording" in error for error in errors)) + + def test_unavailable_observed_sha_does_not_replace_content_authority(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["source_revisions"][0]["source_commit_sha"] = "f" * 40 + hostile["source_revisions"][0]["source_observed_head_sha"] = "f" * 40 + hostile["source_revisions"][0]["current_observed_head_sha"] = "f" * 40 + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertFalse(any("authoritative Git blob disagrees" in error for error in errors)) + + def test_generation_head_observation_is_separate_from_content_identity(self) -> None: + current = json.loads(json.dumps(self.index)) + revision = current["source_revisions"][0] + revision["current_observed_head_sha"] = "e" * 40 + current["reproducibility_sha256"] = _reproducibility_hash(current) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, current) + self.assertFalse( + any( + "observed-head fields must identify the same reviewed source commit" in error + for error in errors + ) + ) + self.assertFalse( + any( + "current_observed_head_sha must be a 40-character Git SHA" in error + for error in errors + ) + ) + + def test_selected_source_checkout_accepts_exact_detached_and_content_equivalent_rewrite(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + org_root = Path(temp_dir) + repository = "hawkinsoperations-detections" + repo = org_root / repository + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + (repo / "detections").mkdir() + (repo / "detections" / "DETECTION_PROMOTION_MATRIX.yml").write_text( + "schema: detection-promotion-matrix-v1\nentries: []\n", + encoding="utf-8", + ) + (repo / "authority.yml").write_text("authority: detection\n", encoding="utf-8") + _git(repo, "add", "authority.yml", "detections/DETECTION_PROMOTION_MATRIX.yml") + _git(repo, "commit", "-m", "authority") + (repo / "review.txt").write_text("reviewed selection\n", encoding="utf-8") + _git(repo, "add", "review.txt") + _git(repo, "commit", "-m", "reviewed selection") + selected = _git(repo, "rev-parse", "HEAD") + reviewed_tree = _git(repo, "rev-parse", "HEAD^{tree}") + _write_source_selection_manifest(org_root, repository, selected, reviewed_tree) + + _git(repo, "checkout", "--detach", selected) + self.assertEqual(verify_selected_source_checkout(org_root, repository), []) + + selection_path = org_root / ".github" / "governance" / "CONVERGENCE_SOURCE_MANIFEST.json" + selection_text = selection_path.read_text(encoding="utf-8") + selection_path.write_text(selection_text + "\n", encoding="utf-8") + self.assertTrue( + any( + "must be tracked and clean" in error + for error in verify_selected_source_checkout(org_root, repository) + ) + ) + selection_path.write_text(selection_text, encoding="utf-8") + + (repo / "post-selection.txt").write_text("merge descendant observation\n", encoding="utf-8") + _git(repo, "add", "post-selection.txt") + _git(repo, "commit", "-m", "merge descendant") + self.assertEqual(verify_selected_source_checkout(org_root, repository), []) + + rewritten = _git(repo, "commit-tree", reviewed_tree, input_text="rewritten identity\n") + _git(repo, "checkout", "--detach", rewritten) + self.assertEqual(verify_selected_source_checkout(org_root, repository), []) + + def test_detached_seven_source_generation_records_exact_resolved_refs(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + org_root = Path(temp_dir) / "org" + shutil.copytree(FIXTURE_ROOT, org_root) + missing_authority_fixtures = { + ".github/governance/COMMAND_CENTER_INVARIANTS.json": "{}\n", + ( + "hawkinsoperations-platform/contracts/" + "public-status-source-contract-v1.json" + ): "{}\n", + "hawkinsoperations-website/schemas/public-status-v0.schema.json": "{}\n", + "hoxline/src/hoxline/case_growth/collector.py": ( + "# detached authority fixture\n" + ), + } + for relative, content in missing_authority_fixtures.items(): + authority_path = org_root / relative + authority_path.parent.mkdir(parents=True, exist_ok=True) + authority_path.write_text(content, encoding="utf-8") + expected_heads: dict[str, str] = {} + for repository in REPO_NAMES: + repo = org_root / repository + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + _git( + repo, + "remote", + "add", + "origin", + f"https://github.com/HawkinsOperations/{repository}.git", + ) + _git(repo, "add", ".") + _git(repo, "commit", "-m", "detached authority fixture") + head = _git(repo, "rev-parse", "HEAD") + expected_heads[repository] = head + _git(repo, "checkout", "--detach", head) + + selections: list[dict[str, object]] = [] + for repository in REPO_NAMES: + head = expected_heads[repository] + if repository == ".github": + selections.append( + { + "repository": repository, + "canonical_repository": "HawkinsOperations/.github", + "revision_source": "github_event_sha", + "authority_content_revision": head, + "tree_source": "github_event_tree", + } + ) + else: + selections.append( + { + "repository": repository, + "canonical_repository": ( + f"HawkinsOperations/{repository}" + ), + "revision": head, + "authority_content_revision": head, + "reviewed_tree_sha": _git( + org_root / repository, + "rev-parse", + "HEAD^{tree}", + ), + } + ) + manifest = { + "schema": "hawkinsoperations-convergence-source-manifest-v1", + "manifest_id": "TEST_DETACHED_EXACT_SEVEN_SOURCE_SELECTION", + "repositories": selections, + "constraints": { + "exact_repository_count": 7, + "read_only": True, + "default_branch_fallback": False, + "require_detached_exact_revision": True, + "record_checked_revisions": True, + "consumer_outputs_are_not_authority": True, + "proof_ceiling": ( + "CONTROLLED_REPO_CONVERGENCE_AND_LOCAL_FIXTURE_REVIEW_ONLY" + ), + }, + } + manifest_path = ( + org_root + / ".github" + / "governance" + / "CONVERGENCE_SOURCE_MANIFEST.json" + ) + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + command_center = org_root / ".github" + _git( + command_center, + "add", + "governance/CONVERGENCE_SOURCE_MANIFEST.json", + ) + _git(command_center, "commit", "-m", "detached source selection") + expected_heads[".github"] = _git(command_center, "rev-parse", "HEAD") + + with mock.patch.dict( + "os.environ", + { + "HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": ( + expected_heads[".github"] + ) + }, + ): + generated = build_case_growth_index( + org_root, + generated_at="2026-06-27T00:00:00Z", + ) + for revision in generated["source_revisions"]: + repository = revision["repository"] + with self.subTest(repository=repository): + self.assertEqual( + expected_heads[repository], + revision["resolved_ref"], + ) + self.assertEqual( + revision["current_observed_head_sha"], + revision["resolved_ref"], + ) + self.assertFalse( + str(revision["resolved_ref"]).startswith( + "UNKNOWN_WITH_REASON:" + ) + ) + + with mock.patch.dict( + "os.environ", + { + "HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": ( + expected_heads[".github"] + ) + }, + ): + errors, _ = verify_case_growth_snapshot(org_root, generated) + self.assertEqual([], errors, errors) + + def test_dynamic_command_center_selection_requires_exact_detached_observation(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + org_root = Path(temp_dir) + repository = "hawkinsoperations-detections" + repo = org_root / repository + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + (repo / "detections").mkdir() + (repo / "detections" / "DETECTION_PROMOTION_MATRIX.yml").write_text( + "schema: detection-promotion-matrix-v1\nentries: []\n", + encoding="utf-8", + ) + (repo / "authority.yml").write_text("authority: detection\n", encoding="utf-8") + _git(repo, "add", "authority.yml", "detections/DETECTION_PROMOTION_MATRIX.yml") + _git(repo, "commit", "-m", "authority") + selected = _git(repo, "rev-parse", "HEAD") + reviewed_tree = _git(repo, "rev-parse", "HEAD^{tree}") + _write_source_selection_manifest(org_root, repository, selected, reviewed_tree) + + command_center = org_root / ".github" + command_head = _git(command_center, "rev-parse", "HEAD") + with mock.patch.dict( + "os.environ", + {"HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": ""}, + ): + self.assertEqual( + verify_selected_source_checkout(org_root, ".github"), + [], + ) + + _git(command_center, "checkout", "--detach", command_head) + with mock.patch.dict( + "os.environ", + {"HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": ""}, + ): + errors = verify_selected_source_checkout(org_root, ".github") + self.assertTrue(any("requires HAWKINS_COMMAND_CENTER" in error for error in errors)) + + with mock.patch.dict( + "os.environ", + {"HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": command_head}, + ): + self.assertEqual( + verify_selected_source_checkout(org_root, ".github"), + [], + ) + + with mock.patch.dict( + "os.environ", + {"HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": "f" * 40}, + ): + errors = verify_selected_source_checkout(org_root, ".github") + self.assertTrue(any("differs from the immutable workflow observation" in error for error in errors)) + + def test_command_center_content_identity_survives_exact_rewritten_event_tree(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + org_root = Path(temp_dir) + repository = "hawkinsoperations-detections" + repo = org_root / repository + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + (repo / "detections").mkdir() + (repo / "detections" / "DETECTION_PROMOTION_MATRIX.yml").write_text( + "schema: detection-promotion-matrix-v1\nentries: []\n", + encoding="utf-8", + ) + _git(repo, "add", "detections/DETECTION_PROMOTION_MATRIX.yml") + _git(repo, "commit", "-m", "authority") + selected = _git(repo, "rev-parse", "HEAD") + reviewed_tree = _git(repo, "rev-parse", "HEAD^{tree}") + _write_source_selection_manifest(org_root, repository, selected, reviewed_tree) + + command_center = org_root / ".github" + command_tree = _git(command_center, "rev-parse", "HEAD^{tree}") + rewritten = _git( + command_center, + "commit-tree", + command_tree, + input_text="rewritten command event\n", + ) + _git(command_center, "checkout", "--detach", rewritten) + with mock.patch.dict( + "os.environ", + {"HAWKINS_COMMAND_CENTER_IMMUTABLE_OBSERVED_SHA": rewritten}, + ): + self.assertEqual( + verify_selected_source_checkout(org_root, ".github"), + [], + ) + + def test_content_commit_must_belong_to_selected_reviewed_lineage(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + org_root = Path(temp_dir) + repository = "hawkinsoperations-detections" + repo = org_root / repository + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + (repo / "detections").mkdir() + authority_path = repo / "detections" / "DETECTION_PROMOTION_MATRIX.yml" + authority_path.write_text( + "schema: detection-promotion-matrix-v1\nentries: []\n", + encoding="utf-8", + ) + _git(repo, "add", "detections/DETECTION_PROMOTION_MATRIX.yml") + _git(repo, "commit", "-m", "authority content") + content_revision = _git(repo, "rev-parse", "HEAD") + content_tree = _git(repo, "rev-parse", "HEAD^{tree}") + (repo / "review.txt").write_text("reviewed final\n", encoding="utf-8") + _git(repo, "add", "review.txt") + _git(repo, "commit", "-m", "reviewed final") + selected = _git(repo, "rev-parse", "HEAD") + reviewed_tree = _git(repo, "rev-parse", "HEAD^{tree}") + _write_source_selection_manifest(org_root, repository, selected, reviewed_tree) + + command_center = org_root / ".github" + manifest_path = command_center / "governance" / "CONVERGENCE_SOURCE_MANIFEST.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + selected_entry = next( + entry + for entry in manifest["repositories"] + if entry["repository"] == repository + ) + selected_entry["authority_content_revision"] = content_revision + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + _git(command_center, "add", "governance/CONVERGENCE_SOURCE_MANIFEST.json") + _git(command_center, "commit", "-m", "select content ancestor") + + rewritten = _git(repo, "commit-tree", reviewed_tree, input_text="rewritten final\n") + _git(repo, "checkout", "--detach", rewritten) + self.assertEqual(verify_selected_source_checkout(org_root, repository), []) + + unrelated_content = _git( + repo, + "commit-tree", + content_tree, + input_text="unrelated same authority content\n", + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + selected_entry = next( + entry + for entry in manifest["repositories"] + if entry["repository"] == repository + ) + selected_entry["authority_content_revision"] = unrelated_content + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + _git(command_center, "add", "governance/CONVERGENCE_SOURCE_MANIFEST.json") + _git(command_center, "commit", "-m", "hostile unrelated content") + errors = verify_selected_source_checkout(org_root, repository) + self.assertTrue( + any( + "outside the reviewed current lineage" in error + for error in errors + ) + ) + + def test_selected_source_checkout_rejects_older_same_authority_blob_ancestor(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + org_root = Path(temp_dir) + repository = "hawkinsoperations-detections" + repo = org_root / repository + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Hoxline Test") + _git(repo, "config", "user.email", "hoxline-test@example.invalid") + (repo / "detections").mkdir() + (repo / "detections" / "DETECTION_PROMOTION_MATRIX.yml").write_text( + "schema: detection-promotion-matrix-v1\nentries: []\n", + encoding="utf-8", + ) + (repo / "authority.yml").write_text("authority: detection\n", encoding="utf-8") + _git(repo, "add", "authority.yml", "detections/DETECTION_PROMOTION_MATRIX.yml") + _git(repo, "commit", "-m", "authority") + older = _git(repo, "rev-parse", "HEAD") + authority_blob = _git(repo, "rev-parse", "HEAD:authority.yml") + (repo / "review.txt").write_text("reviewed selection\n", encoding="utf-8") + _git(repo, "add", "review.txt") + _git(repo, "commit", "-m", "reviewed selection") + selected = _git(repo, "rev-parse", "HEAD") + reviewed_tree = _git(repo, "rev-parse", "HEAD^{tree}") + self.assertEqual(_git(repo, "rev-parse", "HEAD:authority.yml"), authority_blob) + _write_source_selection_manifest(org_root, repository, selected, reviewed_tree) + + _git(repo, "checkout", "--detach", older) + errors = verify_selected_source_checkout(org_root, repository) + self.assertTrue(any("behind the explicit selected revision" in error for error in errors)) + + def test_current_snapshot_rejects_forged_content_identity(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["source_revisions"][0]["authoritative_git_blob_sha"] = "a" * 40 + hostile["source_revisions"][0]["authoritative_content_fingerprint"] = "0" * 64 + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("authoritative Git blob disagrees" in error for error in errors)) + self.assertTrue(any("semantic fingerprint disagrees" in error for error in errors)) + + def test_current_snapshot_rejects_worktree_modified_freshness(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hostile["current_authority"] = True + hostile["snapshot_state"]["current_authority"] = True + hostile["source_revisions"][0]["source_freshness_state"] = "WORKTREE_MODIFIED" + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("source_freshness_state must be one of" in error for error in errors)) + + def test_generated_consumers_cannot_become_self_referential_authority(self) -> None: + hostile = json.loads(json.dumps(self.index)) + hoxline_revision = next( + revision for revision in hostile["source_revisions"] if revision["repository"] == "hoxline" + ) + hoxline_revision["self_referential"] = True + hoxline_revision["revision_scope"] = "authoritative_source_at_commit" + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("generated consumers must not be self-referential authority" in error for error in errors)) + self.assertTrue(any("revision_scope must be content_addressed_authority" in error for error in errors)) + + def test_verify_rejects_drive_unc_and_posix_absolute_paths(self) -> None: + for leaked_path in (r"D:\private\evidence.json", r"\\private-host\share\evidence.json", "/home/reviewer/evidence.json"): + with self.subTest(leaked_path=leaked_path): + hostile = json.loads(json.dumps(self.index)) + hostile["cases"][0]["source_evidence_refs"] = [leaked_path] + hostile["reproducibility_sha256"] = _reproducibility_hash(hostile) + errors, _ = verify_case_growth_snapshot(FIXTURE_ROOT, hostile) + self.assertTrue(any("absolute local path" in error for error in errors)) + def test_anti_vague_output_has_numeric_counts_and_evidence_refs(self) -> None: self.assertGreater(self.index["summary"]["cases_total"], 0) self.assertTrue(any(row["source_evidence_refs"] or row["validation_evidence_refs"] for row in self.rows)) diff --git a/tests/test_gauntlet_metrics_v0.py b/tests/test_gauntlet_metrics_v0.py index 425a90c..6070bc2 100644 --- a/tests/test_gauntlet_metrics_v0.py +++ b/tests/test_gauntlet_metrics_v0.py @@ -12,21 +12,21 @@ sys.path.insert(0, str(ROOT / "src")) from hoxline.cli import main -from hoxline.metrics import build_work_impact_report, evaluate_detection_fixture, load_synthetic_events +from hoxline.metrics import build_work_impact_report, evaluate_detection_fixture, load_controlled_test_events ARTIFACT = ROOT / "examples" / "gauntlet" / "sample-artifact.json" CLAIM_OUTPUT = ROOT / "examples" / "gauntlet" / "sample-claim-authority-output.json" EXPECTED_RESULTS = ROOT / "examples" / "gauntlet" / "expected-detection-results.json" -EVENTS = ROOT / "examples" / "gauntlet" / "synthetic-events.json" +EVENTS = ROOT / "examples" / "gauntlet" / "controlled-test-events.json" PROOFCARD = ROOT / "examples" / "gauntlet" / "sample-proofcard.json" SAMPLE_METRICS = ROOT / "examples" / "gauntlet" / "sample-work-impact-metrics.json" SCHEMA = ROOT / "schemas" / "work-impact-metrics-v0.schema.json" class HoxlineGauntletMetricsV0Test(unittest.TestCase): - def test_synthetic_event_fixture_loads(self) -> None: - fixture = load_synthetic_events(EVENTS) + def test_controlled_test_event_fixture_loads(self) -> None: + fixture = load_controlled_test_events(EVENTS) events = fixture["events"] self.assertEqual(fixture["artifact_id"], "HOX-GAUNTLET-001") @@ -36,7 +36,7 @@ def test_synthetic_event_fixture_loads(self) -> None: self.assertGreaterEqual(sum(1 for event in events if not event["expected_detection_match"]), 6) self.assertTrue( any( - event["parent_process_name"] == "synthetic_browser.exe" + event["parent_process_name"] == "controlled_test_browser.exe" and event["process_name"] == "notepad.exe" and event["expected_detection_match"] is False for event in events diff --git a/tests/test_gauntlet_v0.py b/tests/test_gauntlet_v0.py index b5cd197..864c9b9 100644 --- a/tests/test_gauntlet_v0.py +++ b/tests/test_gauntlet_v0.py @@ -27,14 +27,14 @@ class HoxlineGauntletV0Test(unittest.TestCase): - def test_sample_artifact_is_synthetic_splunk_soc_detection_artifact(self) -> None: + def test_sample_artifact_is_controlled_test_splunk_soc_detection_artifact(self) -> None: artifact = _load_json(ARTIFACT) self.assertEqual(artifact["artifact_id"], "HOX-GAUNTLET-001") self.assertTrue(artifact["ai_assisted"]) self.assertEqual(artifact["detection_artifact"]["platform"], "Splunk") self.assertEqual(artifact["proof_ceiling"], "CONTROLLED_VALIDATION_PRODUCT_DEMO_ONLY") - self.assertTrue(artifact["public_safety"]["synthetic_only"]) + self.assertTrue(artifact["public_safety"]["controlled_test_only"]) self.assertFalse(artifact["public_safety"]["contains_malware_code"]) self.assertFalse(artifact["public_safety"]["contains_exploit_instructions"]) diff --git a/tests/test_multi_artifact_review_manifest_expansion_v1.py b/tests/test_multi_artifact_review_manifest_expansion_v1.py index cd5a4e3..d3a679d 100644 --- a/tests/test_multi_artifact_review_manifest_expansion_v1.py +++ b/tests/test_multi_artifact_review_manifest_expansion_v1.py @@ -1,12 +1,13 @@ from __future__ import annotations import json +import hashlib from pathlib import Path import subprocess import sys from hoxline.cli import main -from hoxline.review_engine import verify_batch_run +from hoxline.review_engine import _review_repo_root, verify_batch_run ROOT = Path(__file__).resolve().parents[1] @@ -17,7 +18,14 @@ "HO-DET-010": ROOT / "examples" / "review" / "ho-det-010-artifact-manifest-v1.json", "HO-DET-011": ROOT / "examples" / "review" / "ho-det-011-artifact-manifest-v1.json", "HO-DET-012": ROOT / "examples" / "review" / "ho-det-012-artifact-manifest-v1.json", + "HO-DET-013": ROOT / "examples" / "review" / "ho-det-013-artifact-manifest-v1.json", + "AWS-DET-001": ROOT / "examples" / "review" / "aws-det-001-artifact-manifest-v1.json", + "ID-DET-001": ROOT / "examples" / "review" / "id-det-001-artifact-manifest-v1.json", + "ID-DET-002": ROOT / "examples" / "review" / "id-det-002-artifact-manifest-v1.json", + "ID-DET-003": ROOT / "examples" / "review" / "id-det-003-artifact-manifest-v1.json", + "ID-DET-004": ROOT / "examples" / "review" / "id-det-004-artifact-manifest-v1.json", } +BLOCKED_MANIFEST = ROOT / "examples" / "review" / "ho-ndr-001-artifact-manifest-v1.json" def _json(path: Path) -> dict[str, object]: @@ -25,6 +33,10 @@ def _json(path: Path) -> dict[str, object]: return json.load(handle) +def test_review_repo_root_is_derived_from_index_not_installed_module() -> None: + assert _review_repo_root(INDEX) == ROOT + + def test_single_artifact_manifests_pass(tmp_path) -> None: for artifact_id, manifest in MANIFESTS.items(): output_dir = tmp_path / artifact_id @@ -63,10 +75,10 @@ def test_batch_machine_state_contains_all_artifacts_and_boundaries(tmp_path) -> state = _json(output_dir / "batch-machine-state.json") artifacts = {item["artifact_id"]: item for item in state["artifacts"]} - assert set(artifacts) == {"HO-DET-009", "HO-DET-010", "HO-DET-011", "HO-DET-012"} - assert state["final_status"] == "PASS" - assert state["expected_pass_artifacts"] == ["HO-DET-009", "HO-DET-010", "HO-DET-011", "HO-DET-012"] - assert state["expected_blocked_artifacts"] == [] + assert set(artifacts) == {*MANIFESTS, "HO-NDR-001"} + assert state["final_status"] == "MIXED" + assert set(state["expected_pass_artifacts"]) == set(MANIFESTS) + assert state["expected_blocked_artifacts"] == ["HO-NDR-001"] assert state["public_safe_status"] == "NOT_PUBLIC_SAFE" assert state["human_review_required"] is True assert state["ai_disposition_authority"] is False @@ -76,9 +88,128 @@ def test_batch_machine_state_contains_all_artifacts_and_boundaries(tmp_path) -> assert state["public_proof_promoted"] is False assert state["lifetime_ledger_changed"] is False for artifact_id, artifact in artifacts.items(): - assert Path(artifact["machine_state"]).is_file(), artifact_id - assert Path(artifact["reviewer_pack"]).is_file(), artifact_id - assert artifact["final_status"] == "PASS" + assert (output_dir / artifact["machine_state"]).is_file(), artifact_id + assert len(artifact["machine_state_sha256"]) == 64 + if artifact_id == "HO-NDR-001": + assert (output_dir / artifact["blocked_review"]).is_file(), artifact_id + assert artifact["reviewer_pack"] is None + assert artifact["final_status"] == "BLOCKED" + else: + assert (output_dir / artifact["reviewer_pack"]).is_file(), artifact_id + assert artifact["final_status"] == "PASS" + + +def test_batch_replay_rejects_tampered_artifact_hash(tmp_path) -> None: + output_dir = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(INDEX), "--output", str(output_dir), "--force"]) == 0 + state_path = output_dir / "batch-machine-state.json" + state = _json(state_path) + state["artifacts"][0]["machine_state_sha256"] = "0" * 64 + state_path.write_text(json.dumps(state, indent=2), encoding="utf-8") + errors = verify_batch_run(state_path) + assert any("machine-state hash mismatch" in error for error in errors) + + +def test_batch_rejects_path_traversal_without_deleting_outside_file(tmp_path) -> None: + sentinel = tmp_path / "sentinel.txt" + sentinel.write_text("preserve", encoding="utf-8") + index = _json(INDEX) + index["artifacts"][0]["artifact_id"] = "../sentinel" + hostile = tmp_path / "traversal.json" + hostile.write_text(json.dumps(index), encoding="utf-8") + output_dir = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(hostile), "--output", str(output_dir), "--force"]) == 1 + assert sentinel.read_text(encoding="utf-8") == "preserve" + state = _json(output_dir / "batch-machine-state.json") + assert state["block_reason"] == "input rejected by path-containment boundary" + + +def test_batch_rejects_index_manifest_artifact_id_mismatch(tmp_path) -> None: + index = _json(INDEX) + index["artifacts"][0]["artifact_id"] = "HO-DET-008" + hostile = tmp_path / "mismatch.json" + hostile.write_text(json.dumps(index), encoding="utf-8") + output_dir = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(hostile), "--output", str(output_dir), "--force"]) == 1 + state = _json(output_dir / "batch-machine-state.json") + assert "does not match manifest artifact_id" in state["block_reason"] + + +def test_boundary_contract_artifact_is_honestly_blocked(tmp_path) -> None: + output_dir = tmp_path / "ho-ndr-001" + assert main(["review", "run", "--artifact", str(BLOCKED_MANIFEST), "--output", str(output_dir), "--force"]) == 1 + state = _json(output_dir / "machine-state.json") + assert state["final_status"] == "BLOCKED" + assert state["block_reason"] == "boundary-contract artifact remains expected BLOCKED" + assert state["public_safe_status"] == "NOT_PUBLIC_SAFE" + assert state["human_review_required"] is True + assert state["ai_disposition_authority"] is False + + +def test_named_cross_domain_fixture_selector_is_replayable(tmp_path) -> None: + output_dir = tmp_path / "aws" + assert main(["review", "run", "--artifact", str(MANIFESTS["AWS-DET-001"]), "--output", str(output_dir), "--force"]) == 0 + telemetry = _json(output_dir / "telemetry-contract-check.json") + assert telemetry["event_ids"] == [] + assert telemetry["event_keys"] == ["CreateUser"] + assert telemetry["fixture_event_keys"] == ["CreateUser"] + + +def test_absolute_path_and_cross_domain_live_claims_fail_closed(tmp_path) -> None: + base = _json(MANIFESTS["AWS-DET-001"]) + attacks = { + "absolute-path": r"C:\Raylee\private\fixture.json", + "drive-absolute-path": r"D:\private\evidence.json", + "unc-absolute-path": r"\\private-host\share\evidence.json", + "posix-absolute-path": "/home/reviewer/private/evidence.json", + "live-cloud": "live AWS proof", + "live-identity": "live identity runtime", + "live-security-onion": "live Security Onion proof", + "unsafe-next-gate": "analyst-approved disposition", + } + for name, claim in attacks.items(): + hostile = json.loads(json.dumps(base)) + if name == "unsafe-next-gate": + hostile["next_gate"] = claim + else: + hostile["requested_claims"] = [claim] + manifest = tmp_path / f"{name}.json" + manifest.write_text(json.dumps(hostile), encoding="utf-8") + output_dir = tmp_path / f"run-{name}" + assert main(["review", "run", "--artifact", str(manifest), "--output", str(output_dir), "--force"]) == 1 + assert _json(output_dir / "machine-state.json")["final_status"] == "BLOCKED" + + +def test_prohibited_claims_in_owner_and_nested_rendered_fields_fail_closed(tmp_path) -> None: + base = _json(MANIFESTS["AWS-DET-001"]) + hostile_values = { + "source-owner": ("source_owner", "ANALYST_APPROVED final authorization"), + "detection-family": ("detection_family", "case closure"), + "telemetry-source": ("telemetry_contract.source", "live AWS proof"), + "nested-field-mapping": ("field_mapping.actor", "AI-approved disposition"), + } + for name, (field_path, claim) in hostile_values.items(): + hostile = json.loads(json.dumps(base)) + target = hostile + parts = field_path.split(".") + for part in parts[:-1]: + target = target[part] + target[parts[-1]] = claim + manifest = tmp_path / f"hostile-{name}.json" + manifest.write_text(json.dumps(hostile), encoding="utf-8") + output_dir = tmp_path / f"run-hostile-{name}" + assert main(["review", "run", "--artifact", str(manifest), "--output", str(output_dir), "--force"]) == 1 + assert _json(output_dir / "machine-state.json")["final_status"] == "BLOCKED" + + +def test_generated_batch_artifacts_do_not_leak_absolute_workstation_paths(tmp_path) -> None: + output_dir = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(INDEX), "--output", str(output_dir), "--force"]) == 0 + for path in output_dir.rglob("*"): + if path.is_file() and path.suffix in {".json", ".md"}: + text = path.read_text(encoding="utf-8") + assert "C:\\Raylee\\" not in text + assert "C:\\Users\\" not in text def test_aggregate_reviewer_pack_explains_pass_and_block_sections(tmp_path) -> None: @@ -113,8 +244,8 @@ def test_hostile_batch_indexes_fail_closed(tmp_path) -> None: def test_expected_pass_artifact_blocking_causes_nonzero(tmp_path) -> None: index = _json(INDEX) - hostile_manifest = ROOT / "examples" / "review" / "hostile" / "missing-telemetry-contract.json" - index["artifacts"] = [{"artifact_id": "HO-DET-010", "manifest_path": str(hostile_manifest)}] + hostile_manifest = "examples/review/hostile/missing-telemetry-contract.json" + index["artifacts"] = [{"artifact_id": "HO-DET-010", "manifest_path": hostile_manifest}] index["expected_pass_artifacts"] = ["HO-DET-010"] index["expected_blocked_artifacts"] = [] temp_index = tmp_path / "expected-pass-blocks.json" @@ -175,3 +306,229 @@ def test_batch_generated_outputs_remain_ignored() -> None: ) assert result.returncode == 0 + + +def _semantic_digest(value: object) -> str: + return hashlib.sha256( + json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + + +def test_batch_index_rejects_duplicate_keys_unknown_shapes_and_nested_authority(tmp_path) -> None: + original = INDEX.read_text(encoding="utf-8") + duplicate = original.replace( + '"ai_disposition_authority": false,', + '"ai_disposition_authority": false, "AI_Disposition_Authority": true,', + 1, + ) + duplicate_path = tmp_path / "duplicate-index.json" + duplicate_path.write_text(duplicate, encoding="utf-8") + duplicate_out = tmp_path / "duplicate" + assert main(["review", "batch", "run", "--index", str(duplicate_path), "--output", str(duplicate_out), "--force"]) == 1 + assert _json(duplicate_out / "batch-machine-state.json")["block_reason"] == "input rejected by strict-structure boundary" + + for name, mutator in { + "top-extension": lambda value: value.update({"metadata": {"public_safe_approved": True}}), + "artifact-extension": lambda value: value["artifacts"][0].update( + {"metadata": {"approvedByAnalyst": True}} + ), + "encoded-claim": lambda value: value.update( + {"next_gate": "final%2520authorization"} + ), + }.items(): + index = _json(INDEX) + mutator(index) + path = tmp_path / f"{name}.json" + path.write_text(json.dumps(index), encoding="utf-8") + output = tmp_path / name + assert main(["review", "batch", "run", "--index", str(path), "--output", str(output), "--force"]) == 1 + assert _json(output / "batch-machine-state.json")["final_status"] == "BLOCKED" + + for index_number, attack in enumerate( + ( + {"final": {"review": {"authorization": True}}}, + {"ai": {"metadata": {"authority": True}}}, + {"review": {"metadata": {"disposition": "APPROVED"}}}, + ) + ): + index = _json(INDEX) + index["batch_claim_boundary"] = json.dumps(attack, separators=(",", ":")) + path = tmp_path / f"neutral-wrapper-index-{index_number}.json" + path.write_text(json.dumps(index), encoding="utf-8") + output = tmp_path / f"neutral-wrapper-index-{index_number}" + assert main(["review", "batch", "run", "--index", str(path), "--output", str(output), "--force"]) == 1 + assert _json(output / "batch-machine-state.json")["final_status"] == "BLOCKED" + + for index_number, attack in enumerate( + ( + {"production_live": [True]}, + {"ai_authority": ["APPROVED"]}, + {"review_disposition": [True]}, + {"final_authorization": [1]}, + ) + ): + index = _json(INDEX) + index["batch_claim_boundary"] = json.dumps(attack, separators=(",", ":")) + path = tmp_path / f"promotion-array-index-{index_number}.json" + path.write_text(json.dumps(index), encoding="utf-8") + output = tmp_path / f"promotion-array-index-{index_number}" + assert main(["review", "batch", "run", "--index", str(path), "--output", str(output), "--force"]) == 1 + assert _json(output / "batch-machine-state.json")["final_status"] == "BLOCKED" + + +def test_batch_replay_rescans_tampered_input_even_after_hash_recalculation(tmp_path) -> None: + output = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(INDEX), "--output", str(output), "--force"]) == 0 + state_path = output / "batch-machine-state.json" + state = _json(state_path) + input_path = output / "input-index.json" + hostile = _json(input_path) + hostile["metadata"] = {"approvedByAnalyst": True} + input_path.write_text(json.dumps(hostile, sort_keys=True), encoding="utf-8") + state["input_index_sha256"] = hashlib.sha256(input_path.read_bytes()).hexdigest() + state["output_digests"]["input-index.json"] = state["input_index_sha256"] + state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(state), encoding="utf-8") + errors = verify_batch_run(state_path) + assert any("input-index replay failed closed" in error for error in errors) + + +def test_batch_replay_rejects_child_path_escape_and_aggregate_laundering(tmp_path) -> None: + output = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(INDEX), "--output", str(output), "--force"]) == 0 + state_path = output / "batch-machine-state.json" + baseline = _json(state_path) + for field, value in { + "machine_state": "../batch-machine-state.json", + "final_status": "BLOCKED", + "public_safe_status": "PUBLIC_SAFE", + "ai_disposition_authority": True, + "next_gate": "case closure", + }.items(): + hostile = json.loads(json.dumps(baseline)) + hostile["artifacts"][0][field] = value + hostile["batch_state_integrity_digest"] = _semantic_digest( + {key: item for key, item in hostile.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile), encoding="utf-8") + assert verify_batch_run(state_path), field + state_path.write_text(json.dumps(baseline), encoding="utf-8") + assert verify_batch_run(state_path) == [] + + +def test_batch_replay_rejects_rehashed_output_and_role_laundering(tmp_path) -> None: + output = tmp_path / "batch" + assert main(["review", "batch", "run", "--index", str(INDEX), "--output", str(output), "--force"]) == 0 + state_path = output / "batch-machine-state.json" + baseline = _json(state_path) + + for name in baseline["output_digests"]: + path = output / name + original = path.read_text(encoding="utf-8") + if path.suffix == ".json": + hostile = json.loads(original) + hostile["replay_extension"] = {"approvedByAnalyst": True} + path.write_text(json.dumps(hostile, indent=2, sort_keys=True) + "\n", encoding="utf-8") + else: + path.write_text(original + "\nproduction ready\n", encoding="utf-8") + state = json.loads(json.dumps(baseline)) + state["output_digests"][name] = hashlib.sha256(path.read_bytes()).hexdigest() + state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path), name + path.write_text(original, encoding="utf-8") + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + hostile_state = json.loads(json.dumps(baseline)) + hostile_state["outputs"]["summary"] = "batch-reviewer-pack.md" + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path) + + hostile_state = json.loads(json.dumps(baseline)) + hostile_state["replay_extension"] = "bounded-looking extra field" + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path) + + for field, value in { + "created_at": "2099-01-01T00:00:00Z", + "batch_id": "laundered-batch", + "block_reason": "unexpected non-blocked reason", + }.items(): + hostile_state = json.loads(json.dumps(baseline)) + hostile_state[field] = value + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: item for key, item in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path), field + + hostile_state = json.loads(json.dumps(baseline)) + hostile_state["artifacts"] = list(reversed(hostile_state["artifacts"])) + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path) + + +def test_blocked_batch_replay_rejects_rehashed_output_and_role_laundering(tmp_path) -> None: + index = HOSTILE_BATCH_DIR / "unsafe-batch-public-safe-claim-index.json" + output = tmp_path / "blocked-batch" + assert main(["review", "batch", "run", "--index", str(index), "--output", str(output), "--force"]) == 1 + state_path = output / "batch-machine-state.json" + baseline = _json(state_path) + assert baseline["final_status"] == "BLOCKED" + assert verify_batch_run(state_path) == [] + + for name in baseline["output_digests"]: + path = output / name + original = path.read_text(encoding="utf-8") + if path.suffix == ".json": + hostile = json.loads(original) + hostile["replay_extension"] = {"approvedByAnalyst": True} + path.write_text(json.dumps(hostile, indent=2, sort_keys=True) + "\n", encoding="utf-8") + else: + path.write_text(original + "\ncase closure\n", encoding="utf-8") + state = json.loads(json.dumps(baseline)) + state["output_digests"][name] = hashlib.sha256(path.read_bytes()).hexdigest() + state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path), name + path.write_text(original, encoding="utf-8") + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + hostile_state = json.loads(json.dumps(baseline)) + hostile_state["outputs"]["summary"] = "batch-reviewer-pack.md" + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path) + + hostile_state = json.loads(json.dumps(baseline)) + hostile_state["replay_extension"] = "bounded-looking extra field" + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path) + + hostile_state = json.loads(json.dumps(baseline)) + hostile_state["created_at"] = "2099-01-01T00:00:00Z" + hostile_state["batch_state_integrity_digest"] = _semantic_digest( + {key: value for key, value in hostile_state.items() if key != "batch_state_integrity_digest"} + ) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_batch_run(state_path) diff --git a/tests/test_one_command_reviewer_demo_v0.py b/tests/test_one_command_reviewer_demo_v0.py index 33e9503..dd2bf67 100644 --- a/tests/test_one_command_reviewer_demo_v0.py +++ b/tests/test_one_command_reviewer_demo_v0.py @@ -59,7 +59,7 @@ def test_detection_fires_only_from_safe_fixture(tmp_path) -> None: output_dir = tmp_path / "demo-run" assert main(["demo", "quickstart", "--output", str(output_dir), "--force"]) == 0 - signal = _json(output_dir / "synthetic-signal.json") + signal = _json(output_dir / "controlled-test-signal.json") validation = _json(output_dir / "validation-result.json") assert signal["source"] == "safe bundled fixture" assert signal["detection_fired"] is True diff --git a/tests/test_review_engine_v1.py b/tests/test_review_engine_v1.py index fa7350c..8b2d50a 100644 --- a/tests/test_review_engine_v1.py +++ b/tests/test_review_engine_v1.py @@ -1,12 +1,27 @@ from __future__ import annotations import json +import base64 +from copy import deepcopy +import hashlib from pathlib import Path import subprocess import sys +from urllib.parse import quote + +import pytest from hoxline.cli import main -from hoxline.review_engine import EXPECTED_PASS_OUTPUTS, STAGE_REGISTRY, verify_review_run +from hoxline.review_engine import ( + EXPECTED_PASS_OUTPUTS, + STAGE_REGISTRY, + ReviewBlocked, + _validate_generated_output_security, + _validate_no_private_markers, + _validate_recursive_boundaries, + verify_tracked_vocabulary, + verify_review_run, +) ROOT = Path(__file__).resolve().parents[1] @@ -19,6 +34,16 @@ def _json(path: Path) -> dict[str, object]: return json.load(handle) +def _integrity(value: dict[str, object], field: str = "state_integrity_digest") -> str: + return hashlib.sha256( + json.dumps( + {key: item for key, item in value.items() if key != field}, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + + def test_valid_ho_det_010_manifest_run_passes(tmp_path, capsys) -> None: output_dir = tmp_path / "review-run" @@ -75,16 +100,16 @@ def test_ho_det_010_event_and_rule_metadata_represented(tmp_path) -> None: telemetry = _json(output_dir / "telemetry-contract-check.json") manifest = _json(output_dir / "artifact-manifest.json") assert telemetry["required_source"] == "Windows Security EventChannel" - assert telemetry["event_ids"] == [4720, 4725, 4726, 4732, 4733, 4738] + assert telemetry["event_ids"] == [4732, 4733] assert telemetry["wazuh_rule_family"] == [910101, 910102, 910103] assert manifest["telemetry_contract"]["wazuh_rule_ids"] == [910101, 910102, 910103] -def test_synthetic_signal_only_and_blocked_claims_enforced(tmp_path) -> None: +def test_controlled_test_signal_only_and_blocked_claims_enforced(tmp_path) -> None: output_dir = tmp_path / "review-run" assert main(["review", "run", "--artifact", str(MANIFEST), "--output", str(output_dir), "--force"]) == 0 - signal = _json(output_dir / "synthetic-signal.json") + signal = _json(output_dir / "controlled-test-signal.json") state = _json(output_dir / "machine-state.json") blocked = {item["claim"] for item in state["blocked_claims"]} assert signal["source"] == "safe bundled fixture" @@ -164,7 +189,7 @@ def test_blocked_outputs_do_not_echo_private_or_raw_field_names(tmp_path) -> Non assert "raw_alert" not in combined assert "private_execution_id" not in combined assert "private_evidence\"" not in combined - assert "prohibited private/raw" in combined + assert "input rejected by strict-structure boundary" in combined def test_hostile_manifest_names_cover_required_block_classes() -> None: names = {path.name for path in HOSTILE_DIR.glob("*.json")} assert "missing-telemetry-contract.json" in names @@ -226,3 +251,658 @@ def test_local_generated_outputs_remain_ignored() -> None: ) assert result.returncode == 0 + + +def test_duplicate_and_unknown_manifest_fields_fail_closed(tmp_path) -> None: + text = MANIFEST.read_text(encoding="utf-8") + duplicate = text.replace( + '"ai_disposition_authority": false,', + '"ai_disposition_authority": false, "AI_Disposition_Authority": true,', + 1, + ) + duplicate_path = tmp_path / "duplicate.json" + duplicate_path.write_text(duplicate, encoding="utf-8") + duplicate_out = tmp_path / "duplicate-run" + assert main(["review", "run", "--artifact", str(duplicate_path), "--output", str(duplicate_out), "--force"]) == 1 + assert _json(duplicate_out / "machine-state.json")["final_status"] == "BLOCKED" + + unknown = _json(MANIFEST) + unknown["extension"] = {"harmless": True} + unknown_path = tmp_path / "unknown.json" + unknown_path.write_text(json.dumps(unknown), encoding="utf-8") + unknown_out = tmp_path / "unknown-run" + assert main(["review", "run", "--artifact", str(unknown_path), "--output", str(unknown_out), "--force"]) == 1 + assert _json(unknown_out / "machine-state.json")["block_reason"] == "input rejected by strict-structure boundary" + + +def test_nested_and_encoded_authority_laundering_fails_closed(tmp_path) -> None: + def encoded(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + + encoded_claims = [ + "public_safe_approved", + "analyst%2Dapproved%20disposition", + "final%2520authorization", + base64.urlsafe_b64encode(b"case closure").decode("ascii"), + encoded(b"case closure!"), + encoded(b"final authorization!"), + encoded(encoded(b"AI-approved disposition").encode("ascii")), + encoded(b"private execution ID"), + encoded(b"C:\\private\\fixture.json"), + '{"nested":{"ai_approved":true}}', + ] + for index, claim in enumerate(encoded_claims): + manifest = _json(MANIFEST) + manifest["field_mapping"] = {"nested": claim} + path = tmp_path / f"encoded-{index}.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / f"encoded-{index}" + assert main(["review", "run", "--artifact", str(path), "--output", str(output), "--force"]) == 1 + state = _json(output / "machine-state.json") + assert state["block_reason"] in { + "input rejected by claim-authority boundary", + "input rejected by private-data boundary", + "input rejected by path-containment boundary", + } + combined = "\n".join(item.read_text(encoding="utf-8") for item in output.glob("*.json")) + assert claim not in combined + + manifest = _json(MANIFEST) + manifest["field_mapping"] = {"nested_authority": {"approvedByAnalyst": True}} + path = tmp_path / "nested-authority.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / "nested-authority" + assert main(["review", "run", "--artifact", str(path), "--output", str(output), "--force"]) == 1 + + canonical = encoded(b"case closure!") + noncanonical = canonical[:-1] + chr(ord(canonical[-1]) + 1) + for index, hostile_value in enumerate((noncanonical, "eyJaaaaaa")): + manifest = _json(MANIFEST) + manifest["field_mapping"] = {"encoded": hostile_value} + path = tmp_path / f"invalid-base64-{index}.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / f"invalid-base64-{index}" + assert main(["review", "run", "--artifact", str(path), "--output", str(output), "--force"]) == 1 + + +def test_nfkc_security_keys_and_normalized_collisions_fail_closed(tmp_path) -> None: + attacks = [ + {"public_safe": True}, + {"ai_disposition_authority": True}, + {"public_safe": True}, + {"public_safe": False, "public_safe": True}, + ] + for index, attack in enumerate(attacks): + manifest = _json(MANIFEST) + manifest["field_mapping"] = attack + path = tmp_path / f"nfkc-{index}.json" + path.write_text(json.dumps(manifest, ensure_ascii=False), encoding="utf-8") + output = tmp_path / f"nfkc-{index}" + assert main(["review", "run", "--artifact", str(path), "--output", str(output), "--force"]) == 1 + assert _json(output / "machine-state.json")["final_status"] == "BLOCKED" + + +def test_generic_negation_cannot_launder_an_adjacent_positive_claim(tmp_path) -> None: + connector_attacks = [ + f"does not prove runtime {connector} customer deployment is active" + for connector in ( + "because", + "therefore", + "meanwhile", + "furthermore", + "also", + "nevertheless", + "nonetheless", + "except", + "despite that", + "in fact", + "whereas", + "yet", + ) + ] + attacks = [ + "pending documentation, production is live", + "unsupported note \u2014 customer environment deployed", + "future issue: signal was observed", + "missing receipt while production is live", + "no proof currently, customer environment deployed", + "not approved / production is live", + "does not prove runtime, customer deployment is active", + "does not prove runtime, AI authority is enabled", + "does not prove runtime, analyst approval granted", + "does not prove runtime, public safe is confirmed", + "does not prove runtime, final authorization received", + "does not prove runtime, case closure approved", + "does not prove runtime and customer deployment is active", + "does not prove runtime plus public safe is confirmed", + "does not prove runtime though case closure is approved", + "runtime is active and not simulated", + "signal is observed and not inferred", + *connector_attacks, + ] + for attack in attacks: + with pytest.raises(ReviewBlocked): + _validate_recursive_boundaries({"note": attack}, "hostile direct input") + with pytest.raises(ReviewBlocked): + _validate_generated_output_security({"note": attack}, "hostile generated output") + + for bounded in ( + "This does not prove production readiness, customer deployment, or signal-observed proof.", + "Production readiness is unsupported.", + "Claims block customer deployment.", + "This does not prove runtime, customer deployment, AI approval, final authorization, or case closure.", + "This does not prove runtime-active status, signal-observed status, production-ready status, public-safe status, AI-approved status, or analyst-approved status.", + "This does not prove runtime, customer deployment, AI approval, or case closure. These claims remain blocked.", + "This does not prove runtime, customer deployment, AI approval, or case closure; all identified claims remain blocked.", + "This does not prove runtime,\ncustomer deployment,\tAI approval, or case closure.", + "Public-safe runtime proof remains NOT_PUBLIC_SAFE.", + ): + _validate_generated_output_security({"note": bounded}, "bounded generated output") + + blocked_paths = ( + "blocked_claims", + "blocked_claim_classes", + "missing_evidence", + "safer_wording", + "what_hoxline_blocked", + ) + path_laundering_claims = ( + "customer deployment is active", + "AI authority is enabled", + "public safe is confirmed", + "case closure approved", + "runtime is active", + ) + for blocked_path in blocked_paths: + for claim in path_laundering_claims: + with pytest.raises(ReviewBlocked): + _validate_generated_output_security( + {blocked_path: [claim]}, + "hostile blocked-path output", + ) + + for bounded in ( + "production ready", + "public-safe runtime proof", + "final authorization record", + "Runtime, signal, public-safe, production, customer, AI approval, final authorization, and case closure claims remain blocked.", + "Runtime, signal, public-safe, live IdP, production identity coverage, autonomous SOC, AI-approved disposition, and analyst-approved disposition claims remain blocked.", + ): + _validate_generated_output_security( + {"blocked_claims": [bounded]}, + "bounded blocked-path output", + ) + + with pytest.raises(ReviewBlocked): + _validate_generated_output_security( + {"extra": "not_public_safe; production is live"}, + "hostile generated output", + ) + + output = tmp_path / "review-run" + assert main(["review", "run", "--artifact", str(MANIFEST), "--output", str(output), "--force"]) == 0 + state_path = output / "machine-state.json" + baseline = _json(state_path) + reviewer_pack = output / "reviewer-pack.md" + original = reviewer_pack.read_text(encoding="utf-8") + for attack in attacks: + reviewer_pack.write_text(original + f"\n{attack}\n", encoding="utf-8") + hostile_state = deepcopy(baseline) + hostile_state["output_digests"]["reviewer-pack.md"] = hashlib.sha256( + reviewer_pack.read_bytes() + ).hexdigest() + hostile_state["state_integrity_digest"] = _integrity(hostile_state) + state_path.write_text( + json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + errors = verify_review_run(state_path) + assert errors, attack + assert any("security validation failed" in error for error in errors), attack + reviewer_pack.write_text(original, encoding="utf-8") + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def test_compositional_promotion_keys_and_embedded_structures_fail_closed(tmp_path) -> None: + keys = [ + "production_live", + "customer_deployment", + "socaas_deployment", + "runtime_status", + "signal_status", + "approval_status", + "closure_status", + "case_status", + "public_safe_runtime", + "final_authorized", + "%70roduction_live", + "production_live_flag", + "customer_deployment_enabled", + "runtime_status_value", + "signal_observed_state", + "public_safe_runtime_flag", + "final_authorized_flag", + "case_closed_value", + "ai_approval_state", + "ai_disposition_enabled", + "analyst_authority_state", + "analyst_approval_value", + "approved_by_analyst_flag", + "runtime_state", + "approval_state", + "extension_production_active_metadata", + "extension_runtime_state_metadata", + "extension_approval_state_metadata", + "%72untime_state", + "%2572untime_state", + ] + for key in keys: + payload = {"metadata": {key: True}} + encoded_json = json.dumps(payload, separators=(",", ":")) + variants = [ + payload, + encoded_json, + quote(encoded_json, safe=""), + base64.urlsafe_b64encode(encoded_json.encode("utf-8")).decode("ascii").rstrip("="), + ] + for variant in variants: + with pytest.raises(ReviewBlocked): + _validate_recursive_boundaries({"extension": variant}, "hostile input") + with pytest.raises(ReviewBlocked): + _validate_generated_output_security({"extension": variant}, "hostile output") + + _validate_recursive_boundaries( + {"metadata": {"production_live": False, "case_status": "BLOCKED"}}, + "bounded input", + ) + _validate_generated_output_security( + {"metadata": {"production_live": False, "case_status": "BLOCKED"}}, + "bounded output", + ) + + for payload in ( + {"runtime": {"state": True}}, + {"approval": {"status": True}}, + {"production": {"active": True}}, + ): + encoded_json = json.dumps(payload, separators=(",", ":")) + for variant in ( + payload, + encoded_json, + quote(encoded_json, safe=""), + quote(quote(encoded_json, safe=""), safe=""), + base64.urlsafe_b64encode(encoded_json.encode("utf-8")).decode("ascii").rstrip("="), + ): + with pytest.raises(ReviewBlocked): + _validate_recursive_boundaries({"extension": variant}, "hostile split input") + with pytest.raises(ReviewBlocked): + _validate_generated_output_security({"extension": variant}, "hostile split output") + + _validate_recursive_boundaries( + {"runtime": {"state": False}, "approval": {"status": "BLOCKED"}}, + "bounded split input", + ) + _validate_recursive_boundaries( + {"final_status": "PASS", "artifacts": [{"final_status": "PASS"}]}, + "canonical final status", + ) + _validate_generated_output_security( + {"final_status": "PASS", "artifacts": [{"final_status": "PASS"}]}, + "canonical final status", + ) + + neutral_wrapper_attacks = [ + {"final": {"review": {"authorization": True}}}, + {"ai": {"metadata": {"authority": True}}}, + {"review": {"metadata": {"disposition": "APPROVED"}}}, + ] + for index, attack in enumerate(neutral_wrapper_attacks): + with pytest.raises(ReviewBlocked): + _validate_recursive_boundaries(attack, "hostile neutral wrapper") + with pytest.raises(ReviewBlocked): + _validate_generated_output_security(attack, "hostile neutral wrapper") + + manifest = _json(MANIFEST) + manifest["field_mapping"] = attack + manifest_path = tmp_path / f"neutral-wrapper-manifest-{index}.json" + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / f"neutral-wrapper-manifest-{index}" + assert main( + ["review", "run", "--artifact", str(manifest_path), "--output", str(output), "--force"] + ) == 1 + + _validate_recursive_boundaries( + { + "final": {"review": {"authorization": False}}, + "ai": {"metadata": {"authority": False}}, + "review": {"metadata": {"disposition": "BLOCKED"}}, + }, + "bounded neutral wrappers", + ) + + array_attacks = [ + {"production_live": [True]}, + {"ai_authority": ["APPROVED"]}, + {"review_disposition": [True]}, + {"final_authorization": [1]}, + ] + for index, attack in enumerate(array_attacks): + with pytest.raises(ReviewBlocked): + _validate_recursive_boundaries(attack, "hostile promotion array") + with pytest.raises(ReviewBlocked): + _validate_generated_output_security(attack, "hostile promotion array") + + manifest = _json(MANIFEST) + manifest["field_mapping"] = attack + manifest_path = tmp_path / f"promotion-array-manifest-{index}.json" + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / f"promotion-array-manifest-{index}" + assert main( + ["review", "run", "--artifact", str(manifest_path), "--output", str(output), "--force"] + ) == 1 + + bounded_array = { + "production_live": [False], + "ai_authority": ["NOT_APPROVED"], + "review_disposition": ["BLOCKED"], + "final_authorization": [False], + } + _validate_recursive_boundaries(bounded_array, "bounded promotion array") + _validate_generated_output_security(bounded_array, "bounded promotion array") + + +def test_private_marker_keys_fail_closed_through_recursive_encodings() -> None: + keys = [ + "raw_wazuh", + "private_evidence", + "%72aw_wazuh", + "%2572aw_wazuh", + "%70rivate_evidence", + "%2570rivate_evidence", + ] + for key in keys: + payload = {"metadata": {key: "hostile marker"}} + encoded_json = json.dumps(payload, separators=(",", ":")) + variants = [ + payload, + encoded_json, + quote(encoded_json, safe=""), + quote(quote(encoded_json, safe=""), safe=""), + base64.urlsafe_b64encode(encoded_json.encode("utf-8")).decode("ascii").rstrip("="), + ] + for variant in variants: + with pytest.raises(ReviewBlocked): + _validate_no_private_markers({"extension": variant}, "hostile private input") + + for value in ( + "raw_wazuh", + "private_evidence", + "endpoint_log", + "generated_password", + "private_payload", + "raw-alert", + ): + for variant in ( + value, + quote(value, safe=""), + base64.urlsafe_b64encode(value.encode("utf-8")).decode("ascii").rstrip("="), + ): + with pytest.raises(ReviewBlocked): + _validate_no_private_markers({"note": variant}, "hostile private value") + + +def test_unicode_format_characters_cannot_obfuscate_claims_or_private_markers(tmp_path) -> None: + scan_ignorable_characters = ( + "\u200b", "\u200c", "\u200d", "\u2060", "\ufeff", + "\u034f", "\u0301", "\ufe0f", + "\x00", "\x08", "\x1f", "\x7f", + ) + claim_templates = ( + "public{format} safe is confirmed", + "case{format} closure approved", + "AI{format} authority is enabled", + "runtime{format} active", + ) + attacks = [ + template.format(format=character) + for character in scan_ignorable_characters + for template in claim_templates + ] + for attack in attacks: + for variant in (attack, quote(attack, safe="")): + nested = {"nested": [{"note": variant}]} + with pytest.raises(ReviewBlocked): + _validate_recursive_boundaries(nested, "format-obfuscated input") + with pytest.raises(ReviewBlocked): + _validate_generated_output_security(nested, "format-obfuscated output") + + for character in scan_ignorable_characters: + for marker in ( + f"PRIVATE{character}_EVIDENCE", + f"RAW{character}_WAZUH", + f"CUSTOMER{character}_IDENTIFIER", + ): + with pytest.raises(ReviewBlocked): + _validate_no_private_markers({"note": marker}, "format-obfuscated private marker") + + raw_plain = "public safe is confirmed" + raw_obfuscated = "public\u200b safe is confirmed" + assert hashlib.sha256(raw_plain.encode("utf-8")).hexdigest() != hashlib.sha256( + raw_obfuscated.encode("utf-8") + ).hexdigest() + _validate_generated_output_security( + { + "note": "valid family emoji: \U0001f468\u200d\U0001f469\u200d\U0001f467\ufe0f", + "accented": "r\u00e9sum\u00e9 review \U0001f469\u200d\U0001f4bb only", + "multiline": "controlled-test\nreview\tcomplete\rbounded", + }, + "valid emoji control", + ) + + output = tmp_path / "review-run" + assert main(["review", "run", "--artifact", str(MANIFEST), "--output", str(output), "--force"]) == 0 + state_path = output / "machine-state.json" + baseline = _json(state_path) + reviewer_pack = output / "reviewer-pack.md" + original = reviewer_pack.read_text(encoding="utf-8") + for attack in attacks: + reviewer_pack.write_text(original + f"\n{attack}\n", encoding="utf-8") + hostile_state = deepcopy(baseline) + hostile_state["output_digests"]["reviewer-pack.md"] = hashlib.sha256( + reviewer_pack.read_bytes() + ).hexdigest() + hostile_state["state_integrity_digest"] = _integrity(hostile_state) + state_path.write_text( + json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + errors = verify_review_run(state_path) + assert errors, attack + assert any("security validation failed" in error for error in errors), attack + reviewer_pack.write_text(original, encoding="utf-8") + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def _tracked_vocabulary_repo(tmp_path: Path, directory: str, name: str, content: bytes) -> Path: + root = tmp_path / directory + root.mkdir() + subprocess.run(["git", "init", "-q", str(root)], check=True) + (root / name).write_bytes(content) + subprocess.run(["git", "-C", str(root), "add", "--", name], check=True) + return root + + +def test_tracked_vocabulary_guard_is_nfkc_filename_utf8_and_nul_fail_closed(tmp_path) -> None: + accepted = _tracked_vocabulary_repo( + tmp_path, + "accepted", + "fixture.md", + "controlled-test résumé 中文 family 👩‍💻\n".encode("utf-8"), + ) + assert verify_tracked_vocabulary(accepted) == [] + + retired = "syn" + "thetic" + cases = [ + ("ascii", "fixture.md", f"retired {retired} fixture\n".encode("utf-8")), + ( + "nfkc", + "fixture.md", + "retired \uff53\uff59\uff4e\uff54\uff48\uff45\uff54\uff49\uff43 fixture\n".encode("utf-8"), + ), + ("filename", f"{retired}-name.md", b"controlled-test fixture\n"), + ("format-filename", "syn\u200bthetic-name.md", b"controlled-test fixture\n"), + ("mark-filename", "synthe\u0301tic-name.md", b"controlled-test fixture\n"), + ("format-content", "fixture.md", "retired syn\u200bthetic fixture\n".encode("utf-8")), + ("mark-content", "fixture.md", "retired synthe\u0301tic fixture\n".encode("utf-8")), + ("control-content", "fixture.md", b"retired syn\x08thetic fixture\n"), + ("utf16", "fixture.md", f"{retired} fixture".encode("utf-16")), + ("nul-byte", "fixture.md", b"controlled-test\0fixture"), + ("unknown-extension", "fixture.blobx", b"controlled-test fixture"), + ] + for directory, name, content in cases: + root = _tracked_vocabulary_repo(tmp_path, directory, name, content) + assert verify_tracked_vocabulary(root), directory + + +def test_encoded_mixed_and_drive_relative_paths_fail_closed(tmp_path) -> None: + attacks = [ + r"C:relative\fixture.json", + r"C:\private\fixture.json", + r"\\host\share\fixture.json", + "/tmp/fixture.json", + r"examples/review\../private.json", + "examples/review/%2e%2e/private.json", + "examples/review/%252e%252e/private.json", + "file:///C:/private/fixture.json", + ] + for index, attack in enumerate(attacks): + manifest = _json(MANIFEST) + manifest["fixture_paths"]["positive"] = attack + path = tmp_path / f"path-{index}.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / f"path-{index}" + assert main(["review", "run", "--artifact", str(path), "--output", str(output), "--force"]) == 1 + state = _json(output / "machine-state.json") + assert state["block_reason"] == "input rejected by path-containment boundary" + assert attack not in json.dumps(state) + + +def test_single_replay_binds_every_output_and_machine_state_field(tmp_path) -> None: + output = tmp_path / "review-run" + assert main(["review", "run", "--artifact", str(MANIFEST), "--output", str(output), "--force"]) == 0 + state_path = output / "machine-state.json" + baseline = _json(state_path) + + for field, value in { + "final_status": "BLOCKED", + "public_safe_status": "PUBLIC_SAFE", + "human_review_required": False, + "ai_disposition_authority": True, + "next_gate": "case closure", + "source_manifest_digest": "0" * 64, + }.items(): + hostile = deepcopy(baseline) + hostile[field] = value + state_path.write_text(json.dumps(hostile), encoding="utf-8") + assert verify_review_run(state_path), field + state_path.write_text(json.dumps(baseline), encoding="utf-8") + + for name in baseline["output_digests"]: + path = output / name + original = path.read_bytes() + path.write_bytes(original + b"\n") + assert any("digest mismatch" in error for error in verify_review_run(state_path)), name + path.write_bytes(original) + assert verify_review_run(state_path) == [] + + +def test_single_replay_rejects_rehashed_output_and_guardrail_laundering(tmp_path) -> None: + output = tmp_path / "review-run" + assert main(["review", "run", "--artifact", str(MANIFEST), "--output", str(output), "--force"]) == 0 + state_path = output / "machine-state.json" + baseline = _json(state_path) + + for name in baseline["output_digests"]: + path = output / name + original = path.read_text(encoding="utf-8") + if path.suffix == ".json": + hostile = json.loads(original) + hostile["replay_extension"] = {"approvedByAnalyst": True} + path.write_text(json.dumps(hostile, indent=2, sort_keys=True) + "\n", encoding="utf-8") + else: + path.write_text(original + "\nproduction ready\n", encoding="utf-8") + state = deepcopy(baseline) + state["output_digests"][name] = hashlib.sha256(path.read_bytes()).hexdigest() + state["state_integrity_digest"] = _integrity(state) + state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path), name + path.write_text(original, encoding="utf-8") + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + for mutator in ( + lambda state: state["outputs"].update({"proofcard_markdown": "proofcard.json"}), + lambda state: state["stages"][0].update({"proof_boundary": "production ready"}), + lambda state: state["blocked_claims"][0].update({"safer_wording": "production ready"}), + lambda state: state.update({"proof_boundary": "production ready"}), + lambda state: state.update({"replay_extension": "bounded-looking extra field"}), + lambda state: state.update({"created_at": "2099-01-01T00:00:00Z"}), + ): + hostile_state = deepcopy(baseline) + mutator(hostile_state) + hostile_state["state_integrity_digest"] = _integrity(hostile_state) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path) + + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path) == [] + + +def test_blocked_replay_rejects_rehashed_output_and_role_laundering(tmp_path) -> None: + manifest = _json(MANIFEST) + manifest["requested_claims"] = {"nested": {"approvedByAnalyst": True}} + manifest_path = tmp_path / "blocked-input.json" + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + output = tmp_path / "blocked-run" + assert main(["review", "run", "--artifact", str(manifest_path), "--output", str(output), "--force"]) == 1 + state_path = output / "machine-state.json" + baseline = _json(state_path) + assert baseline["final_status"] == "BLOCKED" + assert verify_review_run(state_path) == [] + + for name in baseline["output_digests"]: + path = output / name + original = path.read_text(encoding="utf-8") + if path.suffix == ".json": + hostile = json.loads(original) + hostile["replay_extension"] = {"publicSafe": True} + path.write_text(json.dumps(hostile, indent=2, sort_keys=True) + "\n", encoding="utf-8") + else: + path.write_text(original + "\nfinal authorization\n", encoding="utf-8") + state = deepcopy(baseline) + state["output_digests"][name] = hashlib.sha256(path.read_bytes()).hexdigest() + state["state_integrity_digest"] = _integrity(state) + state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path), name + path.write_text(original, encoding="utf-8") + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + hostile_state = deepcopy(baseline) + hostile_state["outputs"]["blocked_review"] = "run-summary.json" + hostile_state["state_integrity_digest"] = _integrity(hostile_state) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path) + + hostile_state = deepcopy(baseline) + hostile_state["replay_extension"] = "bounded-looking extra field" + hostile_state["state_integrity_digest"] = _integrity(hostile_state) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path) + + hostile_state = deepcopy(baseline) + hostile_state["created_at"] = "2099-01-01T00:00:00Z" + hostile_state["state_integrity_digest"] = _integrity(hostile_state) + state_path.write_text(json.dumps(hostile_state, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path) + + state_path.write_text(json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8") + assert verify_review_run(state_path) == []