From 2c10c98cae60dfcd00ebdc8fe895e853b95b4c53 Mon Sep 17 00:00:00 2001 From: Jeff Larson Date: Sat, 1 Aug 2026 15:07:18 -0700 Subject: [PATCH 1/2] ci(guard): enforce the committed-content self-containment guardrail + sweep breadcrumbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds engine/tests/self_containment_guard.rs, a repo-wide nextest guard (alongside file_size_guard.rs) that fails the build on a JEF-nnn ticket reference or a linear.app URL in tracked source comments/docstrings and docs, with narrow, documented exemptions for the guardrail's own naming text in CLAUDE.md and for scripts/judge_bakeoff_cutchoice.py (mid-edit on a concurrent branch). Sweeps every existing JEF-nnn/linear.app breadcrumb the guard would flag out of tracked source and docs, replacing each with the ADR it records, the module it lives beside, or the inline reasoning already next to it — preserving the rationale, never the ticket number. Comment/doc-prose only; no code-logic change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP --- .github/workflows/agent.yml | 6 +- .github/workflows/chart.yml | 28 +-- .github/workflows/e2e.yml | 2 +- .github/workflows/rust.yml | 20 +-- Dockerfile | 10 +- README.md | 4 +- agent/Dockerfile | 8 +- agent/common/src/lib.rs | 28 +-- agent/protector-agent-ebpf/src/main.rs | 88 +++++----- agent/protector-agent-ebpf/src/vmlinux.rs | 26 +-- agent/protector-agent/src/coalesce.rs | 8 +- agent/protector-agent/src/coalesce/tests.rs | 2 +- agent/protector-agent/src/linkage.rs | 6 +- agent/protector-agent/src/main.rs | 30 ++-- agent/protector-agent/src/observer.rs | 89 +++++----- .../src/observer/ebpf/observer_ebpf_tests.rs | 14 +- agent/protector-agent/src/pod.rs | 14 +- agent/protector-agent/src/reporter.rs | 30 ++-- behavior/src/elf.rs | 10 +- behavior/src/elf/tests.rs | 6 +- behavior/src/lib.rs | 68 ++++---- behavior/src/tests.rs | 30 ++-- charts/protector/README.md | 8 +- charts/protector/templates/_helpers.tpl | 4 +- .../protector/templates/agent-daemonset.yaml | 4 +- charts/protector/templates/clusterrole.yaml | 6 +- charts/protector/templates/deployment.yaml | 42 ++--- charts/protector/templates/journal-pvc.yaml | 2 +- charts/protector/templates/role.yaml | 4 +- charts/protector/values.yaml | 30 ++-- docs/STYLEGUIDE.md | 4 +- docs/adr/0001-async-mitigation-engine.md | 10 +- docs/adr/0003-capability-ports.md | 12 +- docs/adr/0004-graph-representation.md | 4 +- docs/adr/0009-asymmetric-action-bar.md | 6 +- docs/adr/0014-behavioral-telemetry-ebpf.md | 10 +- docs/adr/0015-advisory-evidence-egress.md | 40 ++--- docs/adr/0016-severity-vs-urgency.md | 2 +- ...lation-persists-on-the-breach-condition.md | 6 +- ...tor-configured-redacted-breach-notifier.md | 10 +- ...-dashboard-v3-presentation-architecture.md | 6 +- docs/adr/0020-signature-continuity.md | 46 ++--- ...ne-the-entry-is-the-default-containment.md | 16 +- docs/adr/0023-delta-aware-adjudication.md | 18 +- ...no-redundant-by-construction-predicates.md | 10 +- .../0025-dashboard-v4-preact-client-render.md | 16 +- .../adr/0026-adjudication-judge-qwen3-1.7b.md | 8 +- ...-dashboard-root-only-shell-client-strip.md | 2 +- ...board-client-local-state-simplification.md | 4 +- ...9-adjudication-verdict-is-authoritative.md | 8 +- ...oidc-verification-supersedes-edge-trust.md | 4 +- ...1-read-only-mcp-server-tiered-redaction.md | 6 +- docs/adr/0032-model-is-incident-responder.md | 8 +- docs/adr/0033-cut-choice-judge-tier.md | 14 +- docs/adr/0034-cut-choice-contract.md | 14 +- docs/adr/README.md | 16 +- docs/alerts.md | 2 +- docs/ebpf-testing-on-nodes.md | 2 +- docs/ideas/cut-choice-contract.md | 14 +- docs/ideas/model-as-incident-responder.md | 10 +- docs/model-calibration.md | 16 +- engine/examples/dashboard_preview/fixtures.rs | 4 +- engine/examples/dashboard_preview/main.rs | 2 +- engine/examples/dashboard_preview/render.rs | 4 +- .../examples/dashboard_preview/sample_data.rs | 8 +- .../dashboard_preview/scenarios/breach.rs | 4 +- .../dashboard_preview/scenarios/watching.rs | 2 +- engine/src/engine/adj_gate.rs | 32 ++-- engine/src/engine/adj_gate_tests.rs | 15 +- engine/src/engine/adj_pass.rs | 78 ++++----- engine/src/engine/adj_pass_tests.rs | 2 +- engine/src/engine/churn_diag.rs | 12 +- engine/src/engine/cut_divergence_tests.rs | 2 +- engine/src/engine/dashboard/api_json_tests.rs | 6 +- engine/src/engine/dashboard/auth/claims.rs | 14 +- engine/src/engine/dashboard/auth/enforce.rs | 8 +- .../engine/dashboard/auth/enforce_tests.rs | 10 +- engine/src/engine/dashboard/auth/mod.rs | 20 +-- .../src/engine/dashboard/auth/test_support.rs | 4 +- engine/src/engine/dashboard/auth/tests.rs | 2 +- .../dashboard/auth/tier_grants_tests.rs | 4 +- engine/src/engine/dashboard/mod.rs | 42 ++--- engine/src/engine/dashboard/page.rs | 6 +- engine/src/engine/dashboard/page_tests.rs | 8 +- .../src/engine/dashboard/view_model/access.rs | 2 +- .../dashboard/view_model/access/tests.rs | 2 +- .../dashboard/view_model/action/tests.rs | 2 +- .../engine/dashboard/view_model/admission.rs | 4 +- .../src/engine/dashboard/view_model/alerts.rs | 12 +- .../engine/dashboard/view_model/findings.rs | 14 +- .../dashboard/view_model/findings/tests.rs | 10 +- engine/src/engine/dashboard/view_model/mod.rs | 14 +- .../dashboard/view_model/props/access.rs | 2 +- .../dashboard/view_model/props/admission.rs | 10 +- .../dashboard/view_model/props/alerts.rs | 10 +- .../dashboard/view_model/props/findings.rs | 16 +- .../dashboard/view_model/props/readiness.rs | 8 +- .../view_model/props/serialize_tests.rs | 8 +- .../dashboard/view_model/props/signing.rs | 64 +++---- .../dashboard/view_model/props/status.rs | 44 ++--- .../engine/dashboard/view_model/readiness.rs | 4 +- .../dashboard/view_model/readiness/tests.rs | 4 +- .../dashboard/view_model/signing_inventory.rs | 70 ++++---- .../signing_inventory/enforcement_tests.rs | 14 +- .../view_model/signing_inventory/tests.rs | 24 +-- .../src/engine/dashboard/view_model/strip.rs | 20 +-- engine/src/engine/graph/mod.rs | 34 ++-- engine/src/engine/graph/tests.rs | 8 +- engine/src/engine/journal/mod.rs | 42 ++--- engine/src/engine/journal/tests.rs | 12 +- engine/src/engine/journal_tests.rs | 47 +++--- engine/src/engine/judge_freshness_tests.rs | 6 +- engine/src/engine/mcp/access_audit.rs | 4 +- engine/src/engine/mcp/access_audit/tests.rs | 2 +- engine/src/engine/mcp/audit.rs | 10 +- engine/src/engine/mcp/dispatch.rs | 2 +- engine/src/engine/mcp/mod.rs | 6 +- engine/src/engine/mcp/render.rs | 4 +- engine/src/engine/mcp/server.rs | 4 +- engine/src/engine/mcp/state.rs | 2 +- engine/src/engine/mcp/tests.rs | 4 +- engine/src/engine/mcp/tiering.rs | 8 +- engine/src/engine/mcp/tools.rs | 6 +- engine/src/engine/mcp/transport.rs | 2 +- engine/src/engine/metrics.rs | 16 +- engine/src/engine/mod.rs | 114 ++++++------- engine/src/engine/model.rs | 46 ++--- engine/src/engine/notify.rs | 34 ++-- .../engine/observe/adapter/audit_secret.rs | 8 +- engine/src/engine/observe/adapter/enrich.rs | 28 +-- .../adapter/enrich/host_credential_tests.rs | 6 +- .../engine/observe/adapter/enrich/tests.rs | 28 +-- engine/src/engine/observe/adapter/findings.rs | 12 +- engine/src/engine/observe/adapter/mod.rs | 10 +- engine/src/engine/observe/adapter/workload.rs | 2 +- engine/src/engine/observe/alarm_class.rs | 18 +- engine/src/engine/observe/asn.rs | 6 +- engine/src/engine/observe/asn_tests.rs | 2 +- engine/src/engine/observe/audit.rs | 10 +- engine/src/engine/observe/audit/tests.rs | 2 +- engine/src/engine/observe/elf.rs | 8 +- engine/src/engine/observe/epss.rs | 2 +- engine/src/engine/observe/exec_class.rs | 26 +-- engine/src/engine/observe/feed_reload.rs | 4 +- .../engine/observe/host_credential_class.rs | 10 +- engine/src/engine/observe/ingest_guard.rs | 6 +- .../src/engine/observe/ingest_guard/tests.rs | 4 +- engine/src/engine/observe/ip_index.rs | 2 +- engine/src/engine/observe/ip_index/memo.rs | 4 +- .../src/engine/observe/ip_index/memo/tests.rs | 2 +- engine/src/engine/observe/mod.rs | 32 ++-- engine/src/engine/observe/peer_class.rs | 28 +-- engine/src/engine/observe/runtime.rs | 26 +-- engine/src/engine/observe/trivy.rs | 12 +- engine/src/engine/observe/trivy_config.rs | 2 +- engine/src/engine/observe/trivy_rbac.rs | 8 +- engine/src/engine/observe/trivy_secret.rs | 4 +- engine/src/engine/policy_log.rs | 47 +++--- .../engine/reason/adjudicate/downstream.rs | 16 +- .../src/engine/reason/adjudicate/evidence.rs | 74 ++++---- engine/src/engine/reason/adjudicate/guards.rs | 20 +-- .../reason/adjudicate/incident/fixtures.rs | 6 +- .../reason/adjudicate/incident/guards.rs | 4 +- .../adjudicate/incident/guards_tests.rs | 2 +- .../engine/reason/adjudicate/incident/menu.rs | 6 +- .../reason/adjudicate/incident/menu_tests.rs | 2 +- .../engine/reason/adjudicate/incident/mod.rs | 14 +- .../reason/adjudicate/incident/mod_tests.rs | 2 +- engine/src/engine/reason/adjudicate/mod.rs | 16 +- .../engine/reason/adjudicate/model_call.rs | 14 +- engine/src/engine/reason/adjudicate/prompt.rs | 68 ++++---- .../src/engine/reason/adjudicate/surface.rs | 10 +- .../engine/reason/adjudicate/tests/delta.rs | 2 +- .../reason/adjudicate/tests/downstream.rs | 14 +- .../engine/reason/adjudicate/tests/group_1.rs | 54 +++--- .../engine/reason/adjudicate/tests/group_2.rs | 18 +- .../engine/reason/adjudicate/tests/group_3.rs | 60 +++---- .../src/engine/reason/adjudicate/tests/mod.rs | 6 +- .../reason/adjudicate/tests/sections.rs | 2 +- engine/src/engine/reason/backoff.rs | 4 +- engine/src/engine/reason/mod.rs | 2 +- engine/src/engine/reason/proof/chain.rs | 22 +-- engine/src/engine/reason/proof/corroborate.rs | 78 ++++----- .../corroborate_anon_inode_exec_tests.rs | 4 +- .../reason/proof/corroborate_context_tests.rs | 6 +- .../proof/corroborate_drop_exec_tests.rs | 2 +- .../corroborate_host_credential_tests.rs | 2 +- .../proof/corroborate_module_load_tests.rs | 6 +- .../proof/corroborate_objective_tests.rs | 34 ++-- .../reason/proof/corroborate_privesc_tests.rs | 2 +- .../reason/proof/corroborate_ptrace_tests.rs | 6 +- .../engine/reason/proof/corroborate_tests.rs | 8 +- engine/src/engine/reason/proof/mod.rs | 22 +-- .../reason/proof/pivot_quarantine_tests.rs | 24 +-- engine/src/engine/reason/proof/tests.rs | 2 +- engine/src/engine/respond/actuator/mod.rs | 4 +- engine/src/engine/respond/actuator/render.rs | 2 +- engine/src/engine/respond/actuator/tests.rs | 2 +- engine/src/engine/respond/decisions_tests.rs | 2 +- engine/src/engine/respond/mod.rs | 26 +-- engine/src/engine/respond/tests.rs | 28 +-- engine/src/engine/restore.rs | 12 +- engine/src/engine/run_loop.rs | 110 ++++++------ engine/src/engine/run_loop/tests.rs | 16 +- engine/src/engine/scope_preview_tests.rs | 2 +- engine/src/engine/state/agent_liveness.rs | 14 +- .../src/engine/state/agent_liveness/stall.rs | 8 +- .../state/agent_liveness/stall_tests.rs | 2 +- .../src/engine/state/agent_liveness/tests.rs | 4 +- engine/src/engine/state/evidence.rs | 28 +-- engine/src/engine/state/findings.rs | 110 ++++++------ engine/src/engine/state/judgement.rs | 2 +- engine/src/engine/state/readiness.rs | 38 ++--- engine/src/engine/state/readiness_tests.rs | 14 +- engine/src/engine/state/recency.rs | 30 ++-- engine/src/engine/state/report.rs | 35 ++-- engine/src/engine/state/report_tests.rs | 8 +- engine/src/engine/state/reversion.rs | 2 +- engine/src/engine/state/signing_baseline.rs | 54 +++--- .../engine/state/signing_baseline_tests.rs | 18 +- engine/src/engine/state/verdict_store.rs | 110 ++++++------ .../src/engine/state/verdict_store_tests.rs | 24 +-- engine/src/engine/supply_chain/mod.rs | 34 ++-- .../engine/supply_chain/provenance_drift.rs | 14 +- .../supply_chain/provenance_drift_tests.rs | 2 +- .../engine/supply_chain/provenance_sweep.rs | 14 +- .../supply_chain/provenance_sweep_tests.rs | 2 +- .../supply_chain/signing_baseline_strength.rs | 4 +- .../src/engine/supply_chain/signing_drift.rs | 46 ++--- .../supply_chain/signing_drift_tests.rs | 10 +- .../src/engine/supply_chain/signing_rekor.rs | 14 +- .../supply_chain/signing_rekor_tests.rs | 2 +- .../src/engine/supply_chain/signing_sweep.rs | 58 +++---- .../src/engine/supply_chain/signing_trust.rs | 12 +- .../supply_chain/signing_trust_tests.rs | 2 +- engine/src/engine/tests.rs | 32 ++-- engine/src/main.rs | 44 ++--- engine/src/policies/mesh.rs | 6 +- engine/src/policies/signature/auth.rs | 6 +- engine/src/policies/signature/auth_tests.rs | 8 +- .../signature/classify_continuity_tests.rs | 10 +- engine/src/policies/signature/continuity.rs | 12 +- .../policies/signature/continuity_tests.rs | 2 +- engine/src/policies/signature/cosign.rs | 34 ++-- engine/src/policies/signature/cosign_tests.rs | 10 +- engine/src/policies/signature/mod.rs | 20 +-- engine/src/policies/signature/posture.rs | 37 ++-- .../src/policies/signature/posture_tests.rs | 4 +- engine/src/policies/signature/provenance.rs | 12 +- .../policies/signature/provenance_tests.rs | 2 +- engine/src/policies/signature/rekor.rs | 6 +- engine/src/policies/signature/rekor_tests.rs | 2 +- engine/src/policies/signature/tests.rs | 10 +- engine/src/policies/signature/tuf_tmpdir.rs | 4 +- engine/src/policy.rs | 38 ++--- engine/tests/dashboard_guards.rs | 28 +-- engine/tests/file_size_guard.rs | 4 +- engine/tests/self_containment_guard.rs | 159 ++++++++++++++++++ engine/web/dist/dashboard.css | 54 +++--- engine/web/eslint.config.js | 2 +- engine/web/src/access/view.jsx | 2 +- engine/web/src/action/judgement.jsx | 4 +- engine/web/src/action/view.jsx | 4 +- engine/web/src/admission/decisions.jsx | 2 +- engine/web/src/admission/signing.jsx | 8 +- engine/web/src/admission/signing_detail.jsx | 2 +- engine/web/src/admission/signing_glyphs.js | 2 +- engine/web/src/admission/view.jsx | 4 +- engine/web/src/alerts/view.jsx | 2 +- engine/web/src/app.jsx | 28 +-- engine/web/src/findings/detail.jsx | 6 +- engine/web/src/findings/empty.jsx | 2 +- engine/web/src/findings/evidence.jsx | 4 +- engine/web/src/findings/glyphs.js | 2 +- engine/web/src/findings/row.jsx | 4 +- engine/web/src/findings/table.jsx | 4 +- engine/web/src/keys.js | 6 +- engine/web/src/main.jsx | 4 +- engine/web/src/poll.js | 10 +- engine/web/src/readiness/nodes.jsx | Bin 2494 -> 2457 bytes engine/web/src/readiness/view.jsx | 6 +- engine/web/src/strip.jsx | 10 +- engine/web/test/a11y-routes.test.jsx | 4 +- engine/web/test/access.test.jsx | 2 +- engine/web/test/action.test.jsx | 4 +- engine/web/test/admission.test.jsx | 4 +- engine/web/test/alerts.test.jsx | 2 +- engine/web/test/app-refetch.test.jsx | 4 +- engine/web/test/app-status.test.jsx | 6 +- engine/web/test/app.test.jsx | 4 +- engine/web/test/auth-gate.test.jsx | 2 +- engine/web/test/blind-caveat.test.jsx | 4 +- engine/web/test/cut-set.test.jsx | 4 +- engine/web/test/escaping.test.jsx | 4 +- engine/web/test/fixtures.js | 12 +- engine/web/test/honesty.test.jsx | 2 +- engine/web/test/poll.test.js | 10 +- engine/web/test/readiness.test.jsx | 6 +- engine/web/test/state-preservation.test.jsx | 8 +- engine/web/test/strip.test.jsx | 10 +- engine/web/test/transitional-states.test.jsx | 8 +- engine/web/vitest.config.js | 2 +- scripts/churn_analysis.py | 10 +- scripts/e2e.sh | 22 +-- scripts/judge_bakeoff.py | 28 +-- scripts/start-sccache-docker.sh | 6 +- 306 files changed, 2467 insertions(+), 2306 deletions(-) create mode 100644 engine/tests/self_containment_guard.rs diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml index 70249ab0..28b1bf38 100644 --- a/.github/workflows/agent.yml +++ b/.github/workflows/agent.yml @@ -55,7 +55,7 @@ jobs: contents: read packages: write id-token: write - attestations: write # JEF-275: write the Sigstore-signed SLSA build-provenance attestation + attestations: write # write the Sigstore-signed SLSA build-provenance attestation steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -114,7 +114,7 @@ jobs: push: ${{ github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # sccache's R2 backend for the in-image cargo build (JEF-584, ADR-0020). + # sccache's R2 backend for the in-image cargo build (ADR-0020). # `secret-envs` (key=envname) reads these straight out of the RUNNER POD's env, # where the `sccache-r2` Secret is injected via envFrom (cluster repo: # charts/actions/runners/values-protector.yaml) — the repo has no Actions secret @@ -150,7 +150,7 @@ jobs: [[ "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "missing/invalid build digest: '$DIGEST'" >&2; exit 1; } echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} # Sigstore-signed SLSA build-provenance attestation (keyless: Fulcio + Rekor) — what - # protector's provenance observer verifies to the "verified" posture (JEF-275). The buildx + # protector's provenance observer verifies to the "verified" posture. The buildx # `provenance: true` above is the unsigned variant the observer never reads. - name: Attest build provenance (Sigstore-signed SLSA) if: ${{ github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') }} diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index 75021d54..2c8bac63 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -99,9 +99,9 @@ jobs: --set mode=enforce >/dev/null 2>&1; then echo "mode: enforce with empty enforceScope was not refused"; exit 1 fi - - name: "Assert namespace-scoped enforce tightens NetworkPolicy write to per-namespace Roles (JEF-277)" + - name: "Assert namespace-scoped enforce tightens NetworkPolicy write to per-namespace Roles" run: | - # JEF-277: NetworkPolicy is namespaced and the engine only writes cuts inside + # NetworkPolicy is namespaced and the engine only writes cuts inside # enforceScope. A NAMESPACE-only enforceScope must derive a per-namespace # Role+RoleBinding for networkpolicies create/delete/patch in EACH listed namespace, # and must NOT grant the cluster-wide networkpolicies write on the ClusterRole. @@ -168,7 +168,7 @@ jobs: echo "ok" - name: Assert feed-fetcher sidecar is ON by default and auto-wires the engine run: | - # JEF-238 default-on: a stock install must render the feed-fetcher NATIVE SIDECAR + # default-on: a stock install must render the feed-fetcher NATIVE SIDECAR # (an initContainer with restartPolicy: Always) on the engine pod, a shared feeds # emptyDir, and the engine auto-wired to the KEV file via PROTECTOR_KEV_FILE — with # NO CronJob, NO ConfigMap, and NO feed-sync RBAC (the sidecar makes no apiserver @@ -189,7 +189,7 @@ jobs: || { echo "engine not auto-wired to KEV by default"; exit 1; } echo "$render" | grep -q 'kev.json' \ || { echo "engine not pointed at the sidecar kev.json"; exit 1; } - # Engine auto-wired to the EPSS file the same sidecar writes (JEF-243) — the + # Engine auto-wired to the EPSS file the same sidecar writes — the # predictive exploitation axis alongside KEV. echo "$render" | grep -q 'PROTECTOR_EPSS_FILE' \ || { echo "engine not auto-wired to EPSS by default"; exit 1; } @@ -202,13 +202,13 @@ jobs: if echo "$render" | grep -qE 'kev-snapshot|advisory-snapshot'; then echo "feed-sync ConfigMaps still rendered (should be an emptyDir now)"; exit 1 fi - # The NVD advisory feed was retired (JEF-242): KEV is the ONLY feed now, so no + # The NVD advisory feed was retired: KEV is the ONLY feed now, so no # advisory env and no advisory.json must render anywhere. if echo "$render" | grep -q 'PROTECTOR_ADVISORY_FILE'; then - echo "advisory env rendered despite the advisory feed being retired (JEF-242)"; exit 1 + echo "advisory env rendered despite the advisory feed being retired"; exit 1 fi if echo "$render" | grep -q 'advisory.json'; then - echo "advisory.json path rendered despite the advisory feed being retired (JEF-242)"; exit 1 + echo "advisory.json path rendered despite the advisory feed being retired"; exit 1 fi echo "ok" - name: Assert feedSync.enabled=false renders no sidecar and no auto-wire @@ -231,9 +231,9 @@ jobs: echo "engine auto-wired advisory despite feedSync.enabled=false"; exit 1 fi echo "ok" - - name: "Assert liveness/readiness probes tolerate a sweep-CPU burst (JEF-560)" + - name: "Assert liveness/readiness probes tolerate a sweep-CPU burst" run: | - # JEF-560: a protector container was crashlooping (Ready never latching, restart + # a protector container was crashlooping (Ready never latching, restart # count climbing, clean exit 0) because the default 1s probe timeout / 3-strike # (30s) budget was too tight for the CPU the per-pass signing/provenance sweep # legitimately bursts under the chart's CPU limit — kubelet SIGTERM'd a healthy, @@ -256,11 +256,11 @@ jobs: # The CPU limit that produced the throttling-induced probe misses also needs # headroom (the RAM-tight request is unchanged; only the burst ceiling moves). echo "$ready" | grep -q 'cpu: 500m' \ - || { echo "cpu limit not raised past the JEF-560 250m ceiling"; exit 1; } + || { echo "cpu limit not raised past the 250m ceiling"; exit 1; } echo "ok" - name: Assert the feed-fetcher sidecar is unprivileged and has NO apiserver access run: | - # JEF-238 egress boundary: the sidecar is the ONLY container with egress. It must + # egress boundary: the sidecar is the ONLY container with egress. It must # run unprivileged (non-root, no privilege escalation, read-only rootfs, all caps # dropped) and — unlike the retired CronJob — make NO apiserver call: no dedicated # ServiceAccount, no Role/RoleBinding for it, no kubectl. It fetches the FULL public @@ -284,12 +284,12 @@ jobs: echo "feed-fetcher invokes kubectl (should make no apiserver call)"; exit 1 fi # It must fetch the KEV feed and the EPSS feed (the two exploitation-intel feeds; the - # advisory feed was retired, JEF-242). + # advisory feed was retired). echo "$render" | grep -q 'known_exploited_vulnerabilities.json' \ || { echo "feed-fetcher does not fetch the KEV feed"; exit 1; } echo "$render" | grep -q 'epss_scores-current.csv.gz' \ - || { echo "feed-fetcher does not fetch the EPSS feed (JEF-243)"; exit 1; } + || { echo "feed-fetcher does not fetch the EPSS feed"; exit 1; } if echo "$render" | grep -q 'PROTECTOR_ADVISORY_FILE'; then - echo "advisory env rendered despite the advisory feed being retired (JEF-242)"; exit 1 + echo "advisory env rendered despite the advisory feed being retired"; exit 1 fi echo "ok" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c4198363..ab12af58 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,7 +12,7 @@ name: e2e (k3d) permissions: contents: read on: - # Gate PRs AND every merge to main (JEF-303): e2e is a REQUIRED check via branch + # Gate PRs AND every merge to main: e2e is a REQUIRED check via branch # protection, so it must run on PRs to block a red merge — not just post-merge (the # old post-merge-only signal let a red e2e land silently). It builds an image and # stands up k3d (minutes) on a GitHub-hosted runner (it needs a local Docker daemon, diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6eb30b2e..043b5fa2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -31,7 +31,7 @@ jobs: # Secret via envFrom, so none of it belongs in this workflow and no credential is # committed here. # - # The redis backend is deliberately NOT set here any more (JEF-564): sccache selects + # The redis backend is deliberately NOT set here any more: sccache selects # ONE backend from its env, so setting both would make which one you get an # implementation detail of sccache's precedence -- the R2 cutover could read as done # while every build still went to redis. If the pod env is missing, sccache degrades @@ -80,13 +80,13 @@ jobs: npm --prefix engine/web ci --ignore-scripts npm --prefix engine/web run build - name: Lint dashboard client (eslint + jsx-a11y) - # JEF-499: the accessibility gate. eslint-plugin-jsx-a11y (recommended, mapped onto Preact + # the accessibility gate. eslint-plugin-jsx-a11y (recommended, mapped onto Preact # JSX) fails the build on an a11y authoring regression in engine/web/src; the axe route-smoke # (the runtime half) runs in the `test` job's `npm run test`. Deps were installed by the # `npm ci --ignore-scripts` above. run: npm --prefix engine/web run lint - name: Typecheck dashboard client (tsc --noEmit) - # JEF-399: eslint's no-unused-vars (JEF-499) catches unused imports; it does not type-check. + # eslint's no-unused-vars catches unused imports; it does not type-check. # `tsc --noEmit` over engine/web/src (tsconfig.json: checkJs, jsx: react-jsx / preact, offline — # no `.d.ts` fetch) fails the build on a real type error (e.g. a JSX prop of the wrong type) # slipping into the bundle. Deps were installed by the `npm ci --ignore-scripts` above. @@ -105,7 +105,7 @@ jobs: runs-on: protector-runners env: # sccache backend comes from the runner pod's `sccache-r2` envFrom -- see the - # note on the first job. The redis backend is deliberately unset (JEF-564). + # note on the first job. The redis backend is deliberately unset. RUSTC_WRAPPER: sccache CARGO_INCREMENTAL: "0" # Authenticate binstall's GitHub REST API calls (release-artifact lookups). The @@ -138,9 +138,9 @@ jobs: npm --prefix engine/web ci --ignore-scripts npm --prefix engine/web run build - name: Dashboard client tests - # The v4 Preact client's offline unit + interaction tests (vitest + jsdom, ADR-0025 / - # JEF-397): the keyed reconcile, the state-preservation (JEF-351) acceptance test, client - # escaping, the honesty empty-states, and the JEF-499 vitest-axe route-smoke (every top-level + # The v4 Preact client's offline unit + interaction tests (vitest + jsdom, ADR-0025): + # the keyed reconcile, the state-preservation acceptance test, client + # escaping, the honesty empty-states, and the vitest-axe route-smoke (every top-level # view + AuthGate + status strip asserted free of serious/critical axe violations — an a11y # regression goes red here). `npm ci` above installed the dev toolchain. run: npm --prefix engine/web run test @@ -183,7 +183,7 @@ jobs: contents: read packages: write id-token: write - attestations: write # JEF-275: write the Sigstore-signed SLSA build-provenance attestation + attestations: write # write the Sigstore-signed SLSA build-provenance attestation steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -236,7 +236,7 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # sccache's R2 backend for the in-image cargo build (JEF-584, ADR-0020). + # sccache's R2 backend for the in-image cargo build (ADR-0020). # `secret-envs` (key=envname) reads these straight out of the RUNNER POD's env, # where the `sccache-r2` Secret is injected via envFrom (cluster repo: # charts/actions/runners/values-protector.yaml) — the repo has no Actions secret @@ -272,7 +272,7 @@ jobs: [[ "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "missing/invalid build digest: '$DIGEST'" >&2; exit 1; } echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} # Sigstore-signed SLSA build-provenance attestation (keyless: Fulcio + Rekor), pushed to ghcr - # as an OCI referrer. This is what protector's own provenance observer VERIFIES (JEF-275): the + # as an OCI referrer. This is what protector's own provenance observer VERIFIES: the # SLSA v1 predicate carries this repo + workflow as builder.id + workflow.repository, and the # keyless Fulcio/Rekor chain is what trips `keyless_verified` → the "verified" posture. The # buildx `provenance: true` above is the UNSIGNED variant embedded in the image index, which diff --git a/Dockerfile b/Dockerfile index 94d73ba5..dae5f389 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,13 @@ # the rust:1-bookworm builder is built on bookworm so the two stay in sync and the # dynamically-linked binary loads on the slim runtime. # Pulled via mirror.gcr.io (Google's Docker Hub pull-through cache) — the homelab -# buildkit's shared IP exhausts Docker Hub's anonymous quota → 429 (JEF-78). +# buildkit's shared IP exhausts Docker Hub's anonymous quota → 429. # Node stage (ADR-0025): build the Preact dashboard bundle from source. The Rust builder # `include_str!`s engine/web/dist/dashboard.js, which is gitignored (built, never # committed) — so it must be produced here and COPYed in before `cargo build`. This # fetches preact+esbuild-wasm from npm exactly as the cargo stages fetch crates from # crates.io; zero-egress is scoped to the RUNNING engine, not the build (ADR-0025). Pulled -# via mirror.gcr.io for the same Docker Hub quota reason as the cargo base (JEF-78). +# via mirror.gcr.io for the same Docker Hub quota reason as the cargo base. # `npm ci --ignore-scripts` kills install hooks; the build uses esbuild-WASM (arch-neutral, # so the same command works on the amd64 and arm64 native builders — no per-arch esbuild # binary to resolve). @@ -34,11 +34,11 @@ RUN apt-get update \ # glibc. (Changing CFLAGS also reruns aws-lc-sys' build script, rebuilding a stale, # toolchain-mismatched object left in the build cache.) ENV CFLAGS=-std=gnu17 -# sccache (JEF-84) is the dep-caching layer now — it shares the rustc object cache with the +# sccache is the dep-caching layer now — it shares the rustc object cache with the # in-cluster Redis (cluster repo charts/sccache), reached via the meshed BuildKit's own identity, # so a workspace dep compiled by ANY repo's image build (or the CI test build) is reused here. # cargo-chef was REMOVED: sccache + cargo-chef's `cook` fight over the shared /app/target dir and -# abort with "Failed to open file for hashing: …/lib*.rmeta" (JEF-389) — a conflict that is +# abort with "Failed to open file for hashing: …/lib*.rmeta" — a conflict that is # backend-independent (it fails on the local fallback too). A single plain `cargo build` compiles # deps then workspace crates in dependency order, so every `--extern` .rmeta exists when sccache # hashes it. sccache is a HARD GATE here — if it can't start against Redis the build FAILS (no @@ -54,7 +54,7 @@ RUN set -eux; ver=0.16.0; \ | tar -xz -C /usr/local/bin --strip-components=1 "sccache-v${ver}-${a}-unknown-linux-musl/sccache" ENV RUSTC_WRAPPER=sccache CARGO_INCREMENTAL=0 # sccache backend = the shared Cloudflare R2 bucket (cluster repo: charts/sccache, -# ADR-0020, JEF-584), replacing the in-cluster Redis this used to hardcode. Config + +# ADR-0020), replacing the in-cluster Redis this used to hardcode. Config + # bucket-scoped token arrive as BuildKit build SECRETS below — never ENV or a build-arg, # both of which persist in `docker history` on every image we push to ghcr. # diff --git a/README.md b/README.md index ad34fd6d..7739b548 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ like Cilium/Calico). `/metrics` exposes the engine's OTLP instruments (no-op unless an OTLP endpoint is configured). Among them, the **runtime-corroboration coverage** gauges mirror the same -per-node liveness classification the dashboard readiness row reads (JEF-308 → -[JEF-422]), so an operator watching only `/metrics` sees the same blind count the UI +per-node liveness classification the dashboard readiness row reads (→ +), so an operator watching only `/metrics` sees the same blind count the UI does. They are **counts only — no per-node label dimension** (node names are attacker-influenceable, so a per-node series would be a cardinality/DoS vector), and they exclude out-of-scope reporters (nodes the agent isn't scheduled on): diff --git a/agent/Dockerfile b/agent/Dockerfile index 991bc457..118cdb19 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -10,7 +10,7 @@ # DaemonSet's securityContext, never baked into the image. # bookworm-based to match the bookworm-slim runtime's glibc 2.36. -# Via mirror.gcr.io (Docker Hub pull-through) to dodge the anonymous 429 (JEF-78). +# Via mirror.gcr.io (Docker Hub pull-through) to dodge the anonymous 429. FROM mirror.gcr.io/library/rust:1-bookworm AS builder WORKDIR /app # cmake: reqwest's rustls provider (aws-lc-rs → aws-lc-sys) is a C build. zstd: unpacks @@ -20,7 +20,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends cmake curl ca-certificates zstd \ && rm -rf /var/lib/apt/lists/* ENV CFLAGS=-std=gnu17 -# sccache (JEF-84) is the dep-caching layer — it shares the rustc object cache with the in-cluster +# sccache is the dep-caching layer — it shares the rustc object cache with the in-cluster # Redis (cluster repo charts/sccache), reached via the meshed BuildKit's own identity, so a # workspace dep compiled by ANY repo's image build (or the CI test build) is reused here. sccache # is a HARD GATE — if it can't start against Redis the build FAILS (no fallback). The build @@ -35,7 +35,7 @@ RUN set -eux; ver=0.16.0; \ ENV RUSTC_WRAPPER=sccache CARGO_INCREMENTAL=0 # Prebuilt bpf-linker (sha256-pinned, per-arch) + nightly/rust-src for the bpf target's # build-std. Prebuilt so the image build stays light (no LLVM source build). -# ARCH-AWARE: with native multi-arch builders (JEF-295/#161) this builder stage runs on +# ARCH-AWARE: with native multi-arch builders (#161) this builder stage runs on # the TARGET arch, so a hardcoded aarch64 bpf-linker Syntax-errors when run on the amd64 # build leg ("bpf-linker: 1: Syntax error"). Pick the musl build by TARGETARCH # (arm64->aarch64, amd64->x86_64), keeping the per-arch SHA-256 pin. @@ -63,7 +63,7 @@ COPY agent/ agent/ COPY behavior/ behavior/ WORKDIR /app/agent # Workspace target is /app/agent/target; the excluded ebpf crate keeps its own. -# sccache backend = the shared Cloudflare R2 bucket (charts/sccache, ADR-0020, JEF-584). +# sccache backend = the shared Cloudflare R2 bucket (charts/sccache, ADR-0020). # Config + bucket-scoped token arrive as build SECRETS, never ENV/build-arg (those persist # in `docker history`). Fail-soft: scripts/start-sccache-docker.sh probes, retries, then # degrades to a local disk cache rather than failing the build. diff --git a/agent/common/src/lib.rs b/agent/common/src/lib.rs index e6c4b723..61895fb6 100644 --- a/agent/common/src/lib.rs +++ b/agent/common/src/lib.rs @@ -20,7 +20,7 @@ pub const KIND_FILE_OPEN: u32 = 2; /// a LibraryLoaded with the basename. Reuses [`FileEvent`] (kind discriminates). pub const KIND_LIBRARY_LOAD: u32 = 3; /// A process was exec'd (fentry on `security_bprm_check`). Carries the exec'd binary's -/// path, read from `linux_binprm->filename`, PLUS the anon-inode kernel fact (JEF-317, +/// path, read from `linux_binprm->filename`, PLUS the anon-inode kernel fact ( /// Route A) read from `bprm->file->f_inode`; userspace emits a `ProcessExec`. Its own /// [`ExecEvent`] body (not [`FileEvent`]) — the runtime signal for "unexpected process /// spawned" (ADR-0014). @@ -31,20 +31,20 @@ pub const KIND_EXEC: u32 = 4; /// emits a [`Behavior::PrivilegeChange`]. pub const KIND_PRIV_CHANGE: u32 = 5; /// A file was written (fentry on `security_file_open`, filtered in-kernel to write-intent -/// open flags — JEF-306). Carries the written file's path (`bpf_d_path`); userspace emits +/// open flags —). Carries the written file's path (`bpf_d_path`); userspace emits /// a `Behavior::FileWrite`. The runtime signal for container drift: drop-and-execute / /// config tampering (ADR-0014). Reuses [`FileEvent`] /// (the `kind` discriminates it from the read/exec/library file events). pub const KIND_FILE_WRITE: u32 = 6; /// A ptrace ATTACH access check (fentry on `security_ptrace_access_check`, filtered -/// in-kernel to `mode & PTRACE_MODE_ATTACH` — JEF-318, Retire-Falco G2). The classic +/// in-kernel to `mode & PTRACE_MODE_ATTACH` — Retire-Falco G2). The classic /// process-injection primitive Falco fires critical on. Carries NO body beyond the shared /// [`EventHeader`]: the occurrence, attributed by the header's pid/cgroup, IS the fact — the /// target `task_struct`'s pid is deliberately NOT read (see the eBPF probe's doc comment for /// why). Userspace emits a `Behavior::PtraceAttach`. pub const KIND_PTRACE_ATTACH: u32 = 7; /// A kernel module load (fentry on `security_kernel_load_data`, filtered in-kernel to -/// `id == LOADING_MODULE` — JEF-318, Retire-Falco G2). Falco fires critical on +/// `id == LOADING_MODULE` — Retire-Falco G2). Falco fires critical on /// `init_module`/`finit_module`; `load_module()` calls this hook on BOTH syscalls before any /// parsing, so one probe covers both. Carries NO body beyond [`EventHeader`], same shape as /// [`KIND_PTRACE_ATTACH`] — the occurrence is the fact. Userspace emits a @@ -69,7 +69,7 @@ pub struct FileEvent { } /// One observed process exec (kind [`KIND_EXEC`]) — the same `header`/`len`/`path` shape -/// as [`FileEvent`], plus the pure-data anon-inode fact (JEF-317, Route A): +/// as [`FileEvent`], plus the pure-data anon-inode fact (Route A): /// [`Self::exe_anon_inode`]. A dedicated struct rather than a [`FileEvent`] field, since /// this fact is exec-specific — the file-open/library-load/file-write probes have no /// `bprm` to read it from, so folding it into the shared `FileEvent` would mean carrying a @@ -87,7 +87,7 @@ pub struct ExecEvent { /// rather than a normal, linked, on-disk file; `0` otherwise. A `u8`, not `bool`: a /// kernel-written byte is not guaranteed a valid Rust `bool` bit pattern, and `no_std` /// eBPF code writing this field directly must not rely on that guarantee. A KERNEL- - /// OBSERVABLE FACT (JEF-113), not a verdict — whether an anon-inode exec is alarming + /// OBSERVABLE FACT, not a verdict — whether an anon-inode exec is alarming /// is engine policy, conservatively scoped (see `engine::observe::exec_class` / /// `engine::reason::proof::corroborate`), NOT decided here. pub exe_anon_inode: u8, @@ -97,7 +97,7 @@ pub struct ExecEvent { /// body, so userspace can read `kind` (and `pid`/`cgroup_id`) before it knows which body /// follows. /// -/// `cgroup_id` (JEF-158) is the kernel cgroup id captured AT EVENT TIME via the stable +/// `cgroup_id` is the kernel cgroup id captured AT EVENT TIME via the stable /// `bpf_get_current_cgroup_id()` helper — the cgroup v2 directory's inode number. /// Userspace resolves pod attribution from it through a `cgroup_id → pod_uid` table built /// from `/sys/fs/cgroup`, which fixes the exited-process race: a short-lived in-container @@ -146,7 +146,7 @@ pub struct PrivEvent { pub new_uid: u32, } -/// In-kernel dedup window for high-frequency repeat events (JEF-65). A connect to the +/// In-kernel dedup window for high-frequency repeat events. A connect to the /// same `(pid, daddr, dport)` seen again within this many nanoseconds is coalesced — /// suppressed at the source so it never costs a ring-buffer slot. 1s is long enough to /// collapse a chatty process hammering one destination (the volume problem) yet short @@ -154,7 +154,7 @@ pub struct PrivEvent { /// once a second — the additive-evidence model needs presence, not every packet. pub const DEDUP_WINDOW_NS: u64 = 1_000_000_000; -/// Max entries in an in-kernel dedup map (JEF-65 connect, JEF-306 file-write). One slot +/// Max entries in an in-kernel dedup map (connect file-write). One slot /// per live dedup key — `(pid, dest)` for connect, `(pid, inode)` for writes; an LRU map /// evicts the coldest when full, so a churn of distinct keys can't exhaust it (eviction /// just means the evicted key re-emits once — safe, never a crash). Sized to cover a busy @@ -181,7 +181,7 @@ impl ConnKey { } } -/// Dedup key for the file-write probe (JEF-306): the `(pid, inode)` tuple. Coalescing on +/// Dedup key for the file-write probe: the `(pid, inode)` tuple. Coalescing on /// the inode collapses the high-frequency case — a process writing the SAME file /// repeatedly (appending a log, rewriting a state file) — at the source, so a suppressed /// write never costs a ring-buffer slot. The inode number (not the path) is the cheap @@ -203,7 +203,7 @@ impl WriteKey { } } -/// Dedup key for the credential-basename read gate (JEF-320 security rework): the +/// Dedup key for the credential-basename read gate (security rework): the /// `(pid, inode)` tuple, same shape as [`WriteKey`] but a distinct type (its own LRU map, /// its own gate) so the two dedup domains can't be mixed up at a call site. Bounds a HIGH /// finding from security review: `try_file_open`'s widening past `is_tmpfs` to a small @@ -228,7 +228,7 @@ impl ReadKey { } /// Whether a repeat event keyed at `last_ns` should be coalesced (suppressed) at `now_ns`, -/// given the dedup `window_ns` (JEF-65). The single source of truth for the dedup +/// given the dedup `window_ns`. The single source of truth for the dedup /// decision, shared verbatim by the kernel probe and the userspace tests so the two can't /// drift. Returns `true` (coalesce — drop it) when the last emit for this key was strictly /// within the window. A non-monotonic clock (`now_ns < last_ns`, which `bpf_ktime_get_ns` @@ -292,7 +292,7 @@ mod tests { #[test] fn write_key_distinguishes_pid_and_inode() { - // The file-write dedup key (JEF-306) collapses repeat writes to the SAME (pid, + // The file-write dedup key collapses repeat writes to the SAME (pid, // inode) — so it must compare equal for the same pair and differ on either field. let base = WriteKey::new(1234, 42); assert_eq!(base, WriteKey::new(1234, 42)); @@ -302,7 +302,7 @@ mod tests { #[test] fn read_key_distinguishes_pid_and_inode() { - // The credential-basename-read dedup key (JEF-320 security rework) mirrors + // The credential-basename-read dedup key (security rework) mirrors // WriteKey's equality shape: same (pid, inode) pair compares equal, either field // differing does not. let base = ReadKey::new(1234, 42); diff --git a/agent/protector-agent-ebpf/src/main.rs b/agent/protector-agent-ebpf/src/main.rs index 4cb16481..731b067b 100644 --- a/agent/protector-agent-ebpf/src/main.rs +++ b/agent/protector-agent-ebpf/src/main.rs @@ -17,7 +17,7 @@ // Kernel struct bindings (struct file/path/…) — minimal, hand-laid so each read field // sits at its running-kernel byte offset. The offset is what the compiler bakes and the -// verifier checks, so it MUST track the kernel (JEF-324). See vmlinux.rs + docs/ebpf- +// verifier checks, so it MUST track the kernel. See vmlinux.rs + docs/ebpf- // testing-on-nodes.md. mod vmlinux; @@ -32,7 +32,7 @@ use aya_ebpf::{ }; // The event layouts + kind discriminators are shared verbatim with the userspace loader // via this one crate, so the kernel↔userspace byte contract can't drift (ADR-0014). The -// dedup key/window/decision (JEF-65) live here too so the kernel probe and the userspace +// dedup key/window/decision live here too so the kernel probe and the userspace // tests share one definition and can't drift. use protector_agent_common::{ should_coalesce, ConnEvent, ConnKey, EventHeader, ExecEvent, FileEvent, PrivEvent, ReadKey, @@ -47,7 +47,7 @@ static EVENTS: RingBuf = RingBuf::with_byte_size(256 * 1024, 0); /// Count of events the kernel had to drop because [`EVENTS`] was full (a /// `reserve` returning `None`). Ring-buffer loss is otherwise silent — this makes -/// it observable so userspace can surface it in the heartbeat (JEF-58). A +/// it observable so userspace can surface it in the heartbeat. A /// `PerCpuArray` with one slot: each CPU bumps its own counter with no atomics or /// contention; userspace sums across CPUs for the cumulative total. Incremented /// only at the two `EVENTS.reserve` failure sites via [`record_drop`]. @@ -64,7 +64,7 @@ fn record_drop() { } /// Build the [`EventHeader`] common to every emitted event: the kind plus the current -/// task's pid and cgroup id, both captured AT EVENT TIME (JEF-158). The cgroup id comes +/// task's pid and cgroup id, both captured AT EVENT TIME. The cgroup id comes /// from the stable `bpf_get_current_cgroup_id()` helper (the cgroup v2 directory inode), /// recorded while the process is still live so userspace can attribute it to a pod even /// after the (often short-lived) process has exited — the exited-process race the @@ -83,10 +83,10 @@ fn make_header(kind: u32) -> EventHeader { } } -/// In-kernel connect dedup map (JEF-65): `(pid, daddr, dport)` → last-emit time (ns). +/// In-kernel connect dedup map: `(pid, daddr, dport)` → last-emit time (ns). /// Coalesces high-frequency *repeats* — a chatty process hammering the same destination — /// at the source, so a suppressed connect never costs a ring-buffer slot (the volume -/// problem JEF-58's drop counter measures). LRU so a churn of distinct destinations can't +/// problem 's drop counter measures). LRU so a churn of distinct destinations can't /// exhaust it: the coldest key is evicted and simply re-emits once. Connect is the /// firehose probe; the other probes are already volume-bounded (in-kernel filtered to rare /// events), so dedup is applied to connect only — the per-(pid, dest) case the ticket names. @@ -94,7 +94,7 @@ fn make_header(kind: u32) -> EventHeader { static CONN_SEEN: LruHashMap = LruHashMap::with_max_entries(DEDUP_MAP_CAP, 0); /// Count of events coalesced (suppressed in-kernel) by a dedup map — connect repeats via -/// [`CONN_SEEN`] (JEF-65) and file-write repeats via [`WRITE_SEEN`] (JEF-306). Same +/// [`CONN_SEEN`] and file-write repeats via [`WRITE_SEEN`]. Same /// per-CPU, one-slot shape as [`DROPS`]: each CPU bumps its own slot, no atomics; userspace /// sums across CPUs and surfaces the cumulative total in the heartbeat, so the volume cut is /// observable rather than invisible. Bumped only in [`record_coalesced`]. @@ -109,7 +109,7 @@ fn record_coalesced() { } } -/// The connect dedup gate (JEF-65). Returns `true` if this connect to `key` should be +/// The connect dedup gate. Returns `true` if this connect to `key` should be /// emitted, `false` if it's a repeat inside [`DEDUP_WINDOW_NS`] and was coalesced (the /// counter is bumped here). On emit, stamps `now` so the next repeat is measured from it. /// LRU insert can't fail meaningfully — if it ever did we fall through to emit (fail open: @@ -132,7 +132,7 @@ fn allow_connect(key: &ConnKey) -> bool { true } -/// In-kernel file-write dedup map (JEF-306): `(pid, inode)` → last-emit time (ns). +/// In-kernel file-write dedup map: `(pid, inode)` → last-emit time (ns). /// File writes are high-frequency — a process appending to a log or rewriting a state file /// hammers the SAME file — so coalescing repeats to the same `(pid, inode)` at the source /// keeps a suppressed write from ever costing a ring-buffer slot (the volume problem the @@ -141,7 +141,7 @@ fn allow_connect(key: &ConnKey) -> bool { #[map] static WRITE_SEEN: LruHashMap = LruHashMap::with_max_entries(DEDUP_MAP_CAP, 0); -/// The file-write dedup gate (JEF-306), mirroring [`allow_connect`]. Returns `true` if this +/// The file-write dedup gate, mirroring [`allow_connect`]. Returns `true` if this /// write to `key` should be emitted, `false` if it's a repeat inside [`DEDUP_WINDOW_NS`] and /// was coalesced (the shared [`COALESCED`] counter is bumped here). On emit, stamps `now` so /// the next repeat is measured from it. Fail open: an insert that never fails falls through @@ -164,7 +164,7 @@ fn allow_write(key: &WriteKey) -> bool { true } -/// In-kernel dedup map for the credential-basename read gate (JEF-320 security rework): +/// In-kernel dedup map for the credential-basename read gate (security rework): /// `(pid, inode)` → last-emit time (ns). Bounds a HIGH finding from security review: the /// `try_file_open` widening past `is_tmpfs` to `SENSITIVE_CREDENTIAL_BASENAMES` had no /// dedup, so a chatty reader of a matched basename (e.g. repeatedly opening `/etc/shadow` @@ -175,7 +175,7 @@ fn allow_write(key: &WriteKey) -> bool { static CREDENTIAL_READ_SEEN: LruHashMap = LruHashMap::with_max_entries(DEDUP_MAP_CAP, 0); -/// The credential-basename-read dedup gate (JEF-320 security rework), mirroring +/// The credential-basename-read dedup gate (security rework), mirroring /// [`allow_write`]. Returns `true` if this read of `key` should be emitted, `false` if /// it's a repeat inside [`DEDUP_WINDOW_NS`] and was coalesced (the shared [`COALESCED`] /// counter is bumped here). On emit, stamps `now` so the next repeat is measured from it. @@ -198,7 +198,7 @@ fn allow_credential_read(key: &ReadKey) -> bool { true } -/// In-kernel dedup map for the ptrace-attach probe (JEF-318): `pid` → last-emit time (ns). +/// In-kernel dedup map for the ptrace-attach probe: `pid` → last-emit time (ns). /// `security_ptrace_access_check` fires on every PTRACE_MODE_ATTACH check — not just a /// `ptrace(PTRACE_ATTACH/PTRACE_SEIZE)` syscall, but also `process_vm_readv`/ /// `process_vm_writev` (a debugger or monitoring tool reading another process's memory), @@ -206,12 +206,12 @@ fn allow_credential_read(key: &ReadKey) -> bool { /// attacking `pid` — no target (see [`try_ptrace_access_check`]'s doc for why the target /// `task_struct` is never read): a repeat attach check from the SAME attacker inside the /// window is the same "this pid is ptrace-attaching things" fact refreshed, not a new one. -/// Mirrors [`CREDENTIAL_READ_SEEN`]'s JEF-320 ring-DoS lesson — an unbounded fentry on a hook +/// Mirrors [`CREDENTIAL_READ_SEEN`]'s ring-DoS lesson — an unbounded fentry on a hook /// with a legitimate high-frequency caller is exactly the shape that flooded the ring there. #[map] static PTRACE_SEEN: LruHashMap = LruHashMap::with_max_entries(DEDUP_MAP_CAP, 0); -/// The ptrace-attach dedup gate (JEF-318), mirroring [`allow_credential_read`]. Returns +/// The ptrace-attach dedup gate, mirroring [`allow_credential_read`]. Returns /// `true` if an attach check from `pid` should be emitted, `false` if it's a repeat inside /// [`DEDUP_WINDOW_NS`] and was coalesced (the shared [`COALESCED`] counter is bumped here). /// Fail open: an insert that never fails falls through to emit, so a bookkeeping error never @@ -289,7 +289,7 @@ fn try_connect(ctx: &ProbeContext) -> Result<(), i64> { let pid = (aya_ebpf::helpers::bpf_get_current_pid_tgid() >> 32) as u32; let dport = u16::from_be(dport); - // JEF-65: coalesce high-frequency repeats in-kernel. A connect to the same + // coalesce high-frequency repeats in-kernel. A connect to the same // (pid, daddr, dport) seen again within DEDUP_WINDOW_NS is suppressed here — it never // reaches the ring buffer — cutting volume at the source rather than draining + dropping // duplicates in userspace. The first sighting (and one per window thereafter) emits. @@ -317,7 +317,7 @@ fn try_connect(ctx: &ProbeContext) -> Result<(), i64> { /// no universal secret marker (see docs/ebpf-testing-on-nodes.md). const TMPFS_MAGIC: u64 = 0x0102_1994; -/// A small, fixed allowlist of on-host credential-file BASENAMES (JEF-320, Retire-Falco +/// A small, fixed allowlist of on-host credential-file BASENAMES (Retire-Falco /// G3) — the cheap in-kernel volume gate that lets `try_file_open` widen past `is_tmpfs` /// for a read that might be the host shadow/gshadow/sudoers file, an SSH private key, or a /// cloud-provider credential file. These live on the container's ordinary rootfs @@ -330,7 +330,7 @@ const TMPFS_MAGIC: u64 = 0x0102_1994; /// shared ring — see its doc comment). /// /// This is NOT the security classification — same division of labor as the existing -/// tmpfs-scoped probe: the agent stays pure data (JEF-113), and the engine +/// tmpfs-scoped probe: the agent stays pure data, and the engine /// (`engine::observe::host_credential_class`) makes the real "is this path a known /// on-host credential path" call from the FULL path `bpf_d_path` returns below. /// @@ -381,7 +381,7 @@ const O_RDONLY: u64 = 0o0; const O_CREAT: u64 = 0o100; const O_TRUNC: u64 = 0o1000; -/// Whether an `open` with these `f_flags` is a **write** (JEF-306): a non-read-only access +/// Whether an `open` with these `f_flags` is a **write**: a non-read-only access /// mode, or a create/truncate. This is the in-kernel filter that keeps the (very high) /// read-open volume off the ring buffer — only write-intent opens become FileWrite events. fn is_write_open(flags: u64) -> bool { @@ -391,7 +391,7 @@ fn is_write_open(flags: u64) -> bool { /// fentry on `security_file_open(struct file *file)` — the secret-read probe (ADR-0014). /// For a tmpfs read, emits a [`FileEvent`] with the container-relative path via /// `bpf_d_path`; the engine maps it to a SecretRead (or drops it). Filtering to tmpfs -/// in-kernel keeps the (very high) file-open volume off the ring buffer. JEF-320 widens +/// in-kernel keeps the (very high) file-open volume off the ring buffer. widens /// this past tmpfs for a small, fixed allowlist of on-host credential-file basenames (see /// [`SENSITIVE_CREDENTIAL_BASENAMES`]), bounded by the [`allow_credential_read`] dedup gate /// (security rework) so a chatty reader of a matched basename can't flood the ring — ON-NODE @@ -414,7 +414,7 @@ fn try_file_open(ctx: &FEntryContext) -> Result<(), i64> { return Ok(()); } if is_sensitive_credential_basename(file) { - // JEF-320 security rework: dedup gate on (pid, inode) — a chatty reader of a + // security rework: dedup gate on (pid, inode) — a chatty reader of a // matched basename (e.g. hammering `/etc/shadow` or a `credentials` file) must not // be able to flood the single shared ring and starve real exec/priv-change/connect // signals. A missing inode still emits (fail open, mirrors `try_file_write`): the @@ -430,7 +430,7 @@ fn try_file_open(ctx: &FEntryContext) -> Result<(), i64> { Ok(()) } -/// fentry on `security_file_open(struct file *file)` — the file-write probe (JEF-306, +/// fentry on `security_file_open(struct file *file)` — the file-write probe ( /// ADR-0014). A SECOND program on the same LSM hook as the secret-read probe (aya loads /// each program independently), filtered IN-KERNEL to write-intent opens so the read /// firehose never reaches the ring. For a write it emits a [`FileEvent`] (kind @@ -463,7 +463,7 @@ fn try_file_write(ctx: &FEntryContext) -> Result<(), i64> { if !is_write_open(flags as u64) { return Ok(()); } - // Coalesce repeat writes to the same (pid, inode) in-kernel (JEF-306). A write whose + // Coalesce repeat writes to the same (pid, inode) in-kernel. A write whose // inode is unreadable still emits (fail open) — the dedup is a volume optimization, not // a correctness gate, so a missing inode must never silently drop a real write. let pid = (aya_ebpf::helpers::bpf_get_current_pid_tgid() >> 32) as u32; @@ -497,7 +497,7 @@ fn try_mmap_file(ctx: &FEntryContext) -> Result<(), i64> { } // NOT emit_file_path: bpf_d_path is rejected by the verifier in security_mmap_file // (security_mmap_file isn't on the kernel's d_path allowlist, unlike - // security_file_open — JEF-68). Userspace only needs the library *name*, which is the + // security_file_open —). Userspace only needs the library *name*, which is the // leaf basename, so read the dentry's d_name directly with bpf_probe_read_kernel. emit_lib_name(file); Ok(()) @@ -508,7 +508,7 @@ fn try_mmap_file(ctx: &FEntryContext) -> Result<(), i64> { /// runs on every credential change (setuid/setresuid/…), so we filter IN-KERNEL to the only /// case worth a signal: a process *gaining* root (`new->uid.val == 0 && old->uid.val != 0`). /// That keeps ring volume tiny and the signal meaningful — a non-root process becoming root. -/// Reads the cred `uid.val` fields with `bpf_probe_read_kernel` (never bpf_d_path — JEF-68). +/// Reads the cred `uid.val` fields with `bpf_probe_read_kernel` (never bpf_d_path —). /// Observe-only; a failed read drops the event, never errors the probe. #[fentry(function = "security_task_fix_setuid")] pub fn fix_setuid(ctx: FEntryContext) -> u32 { @@ -553,19 +553,19 @@ fn try_fix_setuid(ctx: &FEntryContext) -> Result<(), i64> { } /// fentry on `security_bprm_check(struct linux_binprm *bprm)` — the process-exec probe -/// (ADR-0014, JEF-53). This LSM hook fires on every `execve` once the new binary is +/// (ADR-0014). This LSM hook fires on every `execve` once the new binary is /// resolved, so `bprm->filename` is the path the kernel is about to exec. Emits an /// [`ExecEvent`] (kind [`KIND_EXEC`]) carrying that path plus the anon-inode fact -/// (JEF-317, below); userspace turns it into a `ProcessExec`. Observe-only. NOTE: the +/// (below); userspace turns it into a `ProcessExec`. Observe-only. NOTE: the /// attach point is `security_bprm_check` (the exported LSM call, in BTF — like the other /// `security_*` probes); the un-prefixed `bprm_check_security` is NOT a BTF function on -/// 6.8 (verified on-node: JEF-53 deploy). Attached via **fentry, not `lsm/*`**: the fleet +/// 6.8 (verified on-node: deploy). Attached via **fentry, not `lsm/*`**: the fleet /// does not carry `bpf` in its active LSM list (`CONFIG_LSM` omits it, no `lsm=` on the /// kernel cmdline — confirmed on-node over SSH on both arches), so an `lsm/` program would /// never attach here; fentry on the `security_*` function works regardless of the active /// LSM list, which is why every probe in this file uses it. /// -/// JEF-317 (fileless exec / memfd_create parity with Falco), Route A: an EARLIER version +/// (fileless exec / memfd_create parity with Falco), Route A: an EARLIER version /// of this signal classified the exec *path's shape* (`/dev/fd/` etc.) — withdrawn by /// security review, because the kernel synthesizes that identical string for a benign /// `fexecve()` of an on-disk file too, and runc copies itself into a memfd and re-execs on @@ -591,11 +591,11 @@ fn try_bprm_check(ctx: &FEntryContext) -> Result<(), i64> { Ok(()) } -/// Emit the exec'd binary's path (plus the anon-inode fact, JEF-317) as a [`KIND_EXEC`] +/// Emit the exec'd binary's path (plus the anon-inode fact) as a [`KIND_EXEC`] /// [`ExecEvent`]. `bprm->filename` is a kernel `char *` (the resolved exec path), so — /// like the library-load probe — read the string directly with `bpf_probe_read_kernel_str`. /// NOT `bpf_d_path`: `security_bprm_check` isn't on the kernel's d_path allowlist, so the -/// verifier would reject it (JEF-68). +/// verifier would reject it. fn emit_exec_path(bprm: *const vmlinux::linux_binprm, exe_anon_inode: bool) { let mut ev = ExecEvent { header: make_header(KIND_EXEC), @@ -635,7 +635,7 @@ fn emit_exec_path(bprm: *const vmlinux::linux_binprm, exe_anon_inode: bool) { } } -/// Whether the exec'd binary's backing inode is anonymous (JEF-317, Route A): a +/// Whether the exec'd binary's backing inode is anonymous (Route A): a /// memfd/shmem-backed file (`inode->i_sb->s_magic` is the tmpfs magic — `memfd_create` is /// shmem-backed under the hood) OR an unlinked file (`inode->i_nlink == 0` — covers a /// memfd, which is never linked into any directory, AND the separate "delete the binary @@ -645,7 +645,7 @@ fn emit_exec_path(bprm: *const vmlinux::linux_binprm, exe_anon_inode: bool) { /// is about to run, not a TOCTOU-able separate lookup. A failed read = "not anonymous" /// (fail closed on the flag, matching [`is_tmpfs`]/[`inode_ino`]'s existing convention). /// -/// PURE DATA (JEF-113): this reports a kernel fact only. Whether an anon-inode exec is +/// PURE DATA: this reports a kernel fact only. Whether an anon-inode exec is /// alarming — and the runc-memfd-reexec false-positive risk that makes this conservative /// — is engine policy, not decided here. fn exe_is_anon_inode(bprm: *const vmlinux::linux_binprm) -> bool { @@ -675,7 +675,7 @@ fn exe_is_anon_inode(bprm: *const vmlinux::linux_binprm) -> bool { const PTRACE_MODE_ATTACH: u32 = 0x02; /// fentry on `security_ptrace_access_check(struct task_struct *child, unsigned int mode)` — -/// the ptrace-attach probe (JEF-318, Retire-Falco G2). Falco fires critical on a ptrace +/// the ptrace-attach probe (Retire-Falco G2). Falco fires critical on a ptrace /// ATTACH: the classic process-injection primitive (debugger-attach, code injection via /// `PTRACE_POKETEXT`, credential/memory scraping via `process_vm_readv`). This hook fires on /// EVERY ptrace access check, including the read-only `PTRACE_MODE_READ` checks @@ -683,14 +683,14 @@ const PTRACE_MODE_ATTACH: u32 = 0x02; /// `mode & PTRACE_MODE_ATTACH` before touching anything else — an ATTACH request /// specifically, not a read-only check — then further dedups per attacking pid /// ([`allow_ptrace`]) so a legitimate chatty caller (a debugger single-stepping via repeated -/// `process_vm_readv`) can't flood the ring (the JEF-320 ring-DoS lesson). +/// `process_vm_readv`) can't flood the ring (the ring-DoS lesson). /// /// No vmlinux struct read at all: `mode` is passed BY VALUE (a plain `unsigned int` /// register), and the attacking workload is already fully identified by [`make_header`]'s -/// pid/cgroup. **DECISION (JEF-318):** the target `task_struct`'s pid is deliberately NOT +/// pid/cgroup. **DECISION:** the target `task_struct`'s pid is deliberately NOT /// read — `struct task_struct` is enormous and its layout shifts heavily across kernel /// configs/versions (far more volatile than the already-ON-NODE-PENDING `linux_binprm`/ -/// `inode` offsets from JEF-317), so adding that offset here would be a materially bigger +/// `inode` offsets from), so adding that offset here would be a materially bigger /// verifier-rejection risk for a field the corroboration predicate below doesn't need — the /// attacking pid alone is enough to scope the Falco-parity signal to the foothold entry. #[fentry(function = "security_ptrace_access_check")] @@ -717,7 +717,7 @@ fn try_ptrace_access_check(ctx: &FEntryContext) -> Result<(), i64> { /// the enum is a stable, list-ordered generator macro — `LOADING_UNKNOWN`(0), /// `LOADING_FIRMWARE`(1), `LOADING_MODULE`(2), `LOADING_KEXEC_IMAGE`(3), /// `LOADING_KEXEC_INITRAMFS`(4), `LOADING_POLICY`(5), `LOADING_X509_CERTIFICATE`(6), -/// `LOADING_MAX_ID`(7). **ON-NODE BTF VERIFICATION PENDING (JEF-318):** confirm against +/// `LOADING_MAX_ID`(7). **ON-NODE BTF VERIFICATION PENDING:** confirm against /// `bpftool btf dump … format c | grep -A8 'enum kernel_load_data_id'` on BOTH fleet arches /// before this ships past a spike deploy (docs/ebpf-testing-on-nodes.md). Unlike a struct /// offset, a wrong value here is NOT verifier-checked — it's a plain integer compare, so a @@ -726,7 +726,7 @@ fn try_ptrace_access_check(ctx: &FEntryContext) -> Result<(), i64> { const LOADING_MODULE: u32 = 2; /// fentry on `security_kernel_load_data(enum kernel_load_data_id id, bool contents)` — the -/// kernel-module-load probe (JEF-318, Retire-Falco G2). Falco fires critical on +/// kernel-module-load probe (Retire-Falco G2). Falco fires critical on /// `init_module`/`finit_module`. `load_module()` (kernel/module/main.c) calls this hook /// EARLY — before any parsing — on BOTH syscalls: `init_module`'s in-memory buffer AND /// `finit_module`'s fd (which first reaches `security_kernel_read_file(id=READING_MODULE)` @@ -756,7 +756,7 @@ fn try_kernel_load_data(ctx: &FEntryContext) -> Result<(), i64> { } /// Emit a bare [`EventHeader`]-only fact of `kind` — shared by the ptrace-attach and -/// module-load probes (JEF-318), whose entire signal IS the occurrence, attributed by +/// module-load probes, whose entire signal IS the occurrence, attributed by /// [`make_header`]'s pid/cgroup, with no further payload. Unlike every other emitter in this /// file there is no body struct: the ring event for these two kinds IS the header, so /// userspace's `decode` needs no kind-specific byte parse beyond the header it already reads. @@ -781,7 +781,7 @@ fn emit_file_path(file: *const vmlinux::file, kind: u32) { // &file->f_path. bpf_d_path needs the arg to resolve (against kernel BTF, at the baked // offset) to a `struct path`; the verifier walks `file` at `f_path`'s offset and checks // it lands on `path`. So `f_path`'s offset in vmlinux::file MUST match the running kernel - // (JEF-324) — a stale offset lands elsewhere and is rejected ("R1 is of type file …"). + // — a stale offset lands elsewhere and is rejected ("R1 is of type file …"). let path_ptr = unsafe { core::ptr::addr_of!((*file).f_path) }; let n = unsafe { bpf_d_path( @@ -808,7 +808,7 @@ fn emit_file_path(file: *const vmlinux::file, kind: u32) { /// Emit the library *name* (leaf basename) of `file` as a [`KIND_LIBRARY_LOAD`] event. /// The library-load probe can't use `bpf_d_path` (the verifier rejects it in the -/// security_mmap_file hook — not on the kernel's d_path allowlist; JEF-68). Userspace only +/// security_mmap_file hook — not on the kernel's d_path allowlist;). Userspace only /// needs the basename to name the library, which is the leaf dentry's `d_name`, so read it /// directly with bpf_probe_read_kernel(_str) — allowed in any program type. fn emit_lib_name(file: *const vmlinux::file) { @@ -860,7 +860,7 @@ fn emit_lib_name(file: *const vmlinux::file) { } /// Whether `file`'s leaf dentry name is one of [`SENSITIVE_CREDENTIAL_BASENAMES`] -/// (JEF-320) — the cheap volume gate for `try_file_open`'s past-tmpfs widening. Reads the +/// — the cheap volume gate for `try_file_open`'s past-tmpfs widening. Reads the /// dentry's `d_name` directly rather than `bpf_d_path`ing every non-tmpfs open, the same /// allowed-anywhere pattern as [`emit_lib_name`]. A failed read = "not sensitive" (drop, /// never a false allow). @@ -944,7 +944,7 @@ fn is_tmpfs(file: *const vmlinux::file) -> bool { } /// Read `file->f_inode->i_ino` — the inode number, the file-write dedup key's identity -/// (JEF-306). The pointer chase uses bpf_probe_read_kernel (fixed offsets from the node-BTF +/// . The pointer chase uses bpf_probe_read_kernel (fixed offsets from the node-BTF /// vmlinux), the same safe pattern as [`is_tmpfs`]. `None` on any failed read — the caller /// then emits without deduping (fail open), never dropping a real write for a bookkeeping miss. fn inode_ino(file: *const vmlinux::file) -> Option { diff --git a/agent/protector-agent-ebpf/src/vmlinux.rs b/agent/protector-agent-ebpf/src/vmlinux.rs index 1c44db81..bb8b8b7e 100644 --- a/agent/protector-agent-ebpf/src/vmlinux.rs +++ b/agent/protector-agent-ebpf/src/vmlinux.rs @@ -1,4 +1,4 @@ -//! Minimal kernel struct bindings for the eBPF probes (ADR-0014, JEF-324). +//! Minimal kernel struct bindings for the eBPF probes (ADR-0014). //! //! These are NOT the full `aya-tool`-generated `vmlinux.rs` (that was ~60k lines — well //! over the repo's 1,000-line file cap — and, being a static snapshot, silently rots on @@ -25,13 +25,13 @@ //! offset on both arches (the only arm64/amd64 `super_block` divergence is `s_vop` at //! +192, long past `s_magic` at +96), so this single static layout is correct fleet-wide. //! -//! Linux 6.11 reorganized `struct file` (JEF-324): `f_path` moved +168 -> +64, `f_inode` +//! Linux 6.11 reorganized `struct file`: `f_path` moved +168 -> +64, `f_inode` //! -> +32, `f_flags` -> +40. The previous 6.8-generated bindings put `f_path` at +168, //! which on 7.0.0 lands in the `f_wb_err`/`f_ep` region — the verifier rejection that //! degraded the two `bpf_d_path` probes (secret-read `file_open` + `file_write`) to //! loaded=4/6 fleet-wide. Regenerate (re-verify the offsets) on any kernel struct change. //! -//! # `linux_binprm.file` / `inode.i_nlink` — ON-NODE BTF VERIFICATION PENDING (JEF-317) +//! # `linux_binprm.file` / `inode.i_nlink` — ON-NODE BTF VERIFICATION PENDING //! //! Two fields added for the fileless-exec (anon-inode) probe were derived from kernel //! *source* layout, not dumped from live BTF like everything else above: `linux_binprm.file` @@ -39,7 +39,7 @@ //! comment. Both must be confirmed against `bpftool btf dump` on BOTH fleet arches — the //! same process that produced the offsets above — before this probe ships past a spike //! deploy (docs/ebpf-testing-on-nodes.md). A wrong offset here fails the SAME way a wrong -//! `f_path` offset would have (JEF-324): either a verifier rejection (probe degrades, +//! `f_path` offset would have: either a verifier rejection (probe degrades, //! loud in the heartbeat) or, worse, a silently wrong bool if the misread pointer happens //! to still verify — which is why this module keeps every derivation reasoning explicit //! rather than asserting a bare number. @@ -89,11 +89,11 @@ pub struct qstr { } /// `struct inode` — prefix through `i_nlink` (+72). `i_sb` (+40) reaches the superblock -/// (tmpfs magic); `i_ino` (+64) is the file-write dedup key; `i_nlink` (+72, JEF-317) is +/// (tmpfs magic); `i_ino` (+64) is the file-write dedup key; `i_nlink` (+72) is /// the anon-inode discriminator — `0` for an unlinked inode (a memfd, or any file `rm`'d /// while still executing), non-zero for a normal directory-linked file. /// -/// **ON-NODE BTF VERIFICATION PENDING for `i_nlink` (JEF-317):** derived from kernel +/// **ON-NODE BTF VERIFICATION PENDING for `i_nlink`:** derived from kernel /// source, not dumped from live BTF like the fields above it. `i_nlink` is the first field /// of an anonymous union (`union { const unsigned int i_nlink; unsigned int __i_nlink; }`) /// immediately after `i_ino` in `struct inode` — no padding needed since `i_ino` (an @@ -108,7 +108,7 @@ pub struct inode { pub i_sb: *mut super_block, // +40 _pad1: [u8; 16], pub i_ino: u64, // +64 unsigned long - pub i_nlink: u32, // +72 ON-NODE BTF VERIFICATION PENDING (JEF-317, see doc above) + pub i_nlink: u32, // +72 ON-NODE BTF VERIFICATION PENDING (see doc above) } /// `struct super_block` — prefix through `s_magic` (+96), the tmpfs filter's discriminator. @@ -136,15 +136,15 @@ pub struct cred { } /// `struct linux_binprm` — prefix through `filename` (+96), the resolved exec path -/// (`char *`) the process-exec probe emits. `file` (+64, JEF-317) is the ALREADY-OPENED +/// (`char *`) the process-exec probe emits. `file` (+64) is the ALREADY-OPENED /// executable's `struct file*` — by the time `security_bprm_check` fires, `bprm_execve()` /// (fs/exec.c) has already opened it (`do_open_execat`/`bprm->file = …`), before /// `exec_binprm()` → `search_binary_handler()` → `security_bprm_check()` is reached — so /// this read is safe at this hook, no ordering hazard. /// -/// **ON-NODE BTF VERIFICATION PENDING for `file` (JEF-317):** derived from kernel source +/// **ON-NODE BTF VERIFICATION PENDING for `file`:** derived from kernel source /// layout (`struct linux_binprm` in linux/binfmts.h), not dumped from live BTF like -/// `filename` below (already verified on-node, JEF-53). Derivation: `vma`(+0) + +/// `filename` below (already verified on-node). Derivation: `vma`(+0) + /// `vma_pages`(+8) + `mm`(+16) + `p`(+24) + `argmin`(+32) + the four-bit `unsigned int` /// bitfield (+40, padded to +48 for the next pointer's alignment) + `executable`(+48) + /// `interpreter`(+56) + `file`(+64) + `cred`(+72) + `unsafe`(+80) + `per_clear`(+84) + @@ -157,7 +157,7 @@ pub struct cred { #[derive(Copy, Clone)] pub struct linux_binprm { _pad0: [u8; 64], - pub file: *mut file, // +64 ON-NODE BTF VERIFICATION PENDING (JEF-317, see doc above) + pub file: *mut file, // +64 ON-NODE BTF VERIFICATION PENDING (see doc above) _pad1: [u8; 24], pub filename: *const c_char, // +96 } @@ -178,10 +178,10 @@ const _: () = { assert!(offset_of!(qstr, name) == 8); assert!(offset_of!(inode, i_sb) == 40); assert!(offset_of!(inode, i_ino) == 64); - assert!(offset_of!(inode, i_nlink) == 72); // JEF-317, ON-NODE PENDING + assert!(offset_of!(inode, i_nlink) == 72); /, ON-NODE PENDING assert!(offset_of!(super_block, s_magic) == 96); assert!(offset_of!(cred, uid) == 8); assert!(offset_of!(kuid_t, val) == 0); - assert!(offset_of!(linux_binprm, file) == 64); // JEF-317, ON-NODE PENDING + assert!(offset_of!(linux_binprm, file) == 64); /, ON-NODE PENDING assert!(offset_of!(linux_binprm, filename) == 96); }; diff --git a/agent/protector-agent/src/coalesce.rs b/agent/protector-agent/src/coalesce.rs index ff39b1bd..68c3767e 100644 --- a/agent/protector-agent/src/coalesce.rs +++ b/agent/protector-agent/src/coalesce.rs @@ -1,6 +1,6 @@ -//! Agent-side debounce/coalescing of behavioral observations before the POST (JEF-296). +//! Agent-side debounce/coalescing of behavioral observations before the POST. //! -//! Follow-on to JEF-294 (which raised the engine's per-batch cap 256→1024 so batches +//! Follow-on to (which raised the engine's per-batch cap 256→1024 so batches //! stopped truncating). That stopped the *truncation*, but the real cost is VOLUME: the //! eBPF observer emits events as they happen, so the engine sees hundreds of near-identical //! observations per batch — the same workload doing the same coarse thing (repeated cluster @@ -20,7 +20,7 @@ //! //! [`Behavior::is_alert`] observations bypass the buffer entirely and are returned for an //! IMMEDIATE POST. Alerts are the "something alarming, now" corroboration signal that live -//! containment depends on (JEF-284 condition-2 quarantine, JEF-117) — debouncing them would +//! containment depends on (condition-2 quarantine) — debouncing them would //! add window latency to exactly the path that must stay fast. Debouncing is only ever for //! the high-frequency mundane stream (network / exec / file / library / secret reads). //! @@ -98,7 +98,7 @@ impl Coalescer { /// identical near-duplicate and is dropped (the first-seen is kept). The common steady- /// state case returns an empty vec — the observation is buffered for the window flush. pub fn offer(&mut self, obs: RuntimeObservation) -> Vec { - // Alerts bypass the debounce entirely — flush now, never buffer (JEF-296 correctness + // Alerts bypass the debounce entirely — flush now, never buffer (correctness // requirement: live corroboration must not eat the window latency). if obs.behavior.is_alert() { return vec![obs]; diff --git a/agent/protector-agent/src/coalesce/tests.rs b/agent/protector-agent/src/coalesce/tests.rs index dafcabd9..b198609b 100644 --- a/agent/protector-agent/src/coalesce/tests.rs +++ b/agent/protector-agent/src/coalesce/tests.rs @@ -1,4 +1,4 @@ -//! Unit tests for the agent-side debounce/coalescer (JEF-296). +//! Unit tests for the agent-side debounce/coalescer. use super::*; use protector_behavior::{Behavior, SecretReadSource}; diff --git a/agent/protector-agent/src/linkage.rs b/agent/protector-agent/src/linkage.rs index 42992201..f6093b92 100644 --- a/agent/protector-agent/src/linkage.rs +++ b/agent/protector-agent/src/linkage.rs @@ -1,6 +1,6 @@ -//! Per-workload ELF static/dynamic **linkage** classification (JEF-407). +//! Per-workload ELF static/dynamic **linkage** classification. //! -//! The engine has no in-cluster access to a workload's entrypoint bytes, so JEF-404's +//! The engine has no in-cluster access to a workload's entrypoint bytes, so 's //! static-linkage reachability sat dormant — `Image::static_binary` was always `None` in //! prod and a Go / musl-static CVE rendered `not-observed` forever. The node-local agent DOES //! see the running binary (`/proc//exe`), so it is the natural byte source: on an exec it @@ -23,7 +23,7 @@ use protector_behavior::{Behavior, elf::elf_static_linkage}; /// `None` (unknown) — never a wrong answer (the classifier is conservative by construction). pub const ELF_HEAD_CAP: usize = 4096; -/// Classify a pid's entrypoint linkage from its ELF header (JEF-407). +/// Classify a pid's entrypoint linkage from its ELF header. /// /// Returns `Some(true)` for a statically linked binary (no `PT_INTERP`), `Some(false)` for a /// dynamically linked one, and `None` when the linkage is unknown — the exe couldn't be read diff --git a/agent/protector-agent/src/main.rs b/agent/protector-agent/src/main.rs index 22c6f9dc..402b5f58 100644 --- a/agent/protector-agent/src/main.rs +++ b/agent/protector-agent/src/main.rs @@ -7,7 +7,7 @@ //! reversible network cut. mod coalesce; -// The linkage classifier (JEF-407) is called only from the ebpf observer's exec path (plus +// The linkage classifier is called only from the ebpf observer's exec path (plus // its own unit tests), so gate it like `pod` — the default no-eBPF build doesn't carry it as // dead code (the repo treats warnings as errors). #[cfg(any(feature = "ebpf", test))] @@ -28,7 +28,7 @@ use tokio::sync::mpsc; use coalesce::Coalescer; use reporter::Reporter; -/// Shared **probe-attach status** (JEF-308): the observer sets it once its eBPF probes attach, and +/// Shared **probe-attach status**: the observer sets it once its eBPF probes attach, and /// the per-node liveness beacon reads it each window. The default no-eBPF build never sets it (stays /// `0/0`), so the agent honestly reports itself BLIND — signal-flow liveness, not pod-Ready. #[derive(Default)] @@ -62,7 +62,7 @@ fn now_ms() -> Option { .map(|d| d.as_millis() as u64) } -/// Build a per-node liveness beacon (JEF-308) from the node, the probe-attach status, and the +/// Build a per-node liveness beacon from the node, the probe-attach status, and the /// signals emitted this window. Pure over its inputs so it's unit-testable without the runtime. fn build_agent_report(node: &str, probes: (u32, u32), signals: u64) -> AgentReport { AgentReport { @@ -74,17 +74,17 @@ fn build_agent_report(node: &str, probes: (u32, u32), signals: u64) -> AgentRepo } } -/// Max distinct coalesced keys the debounce buffer holds before a forced flush (JEF-296). +/// Max distinct coalesced keys the debounce buffer holds before a forced flush. /// Bounds memory and keeps a flushed batch well under the engine's 1024 per-batch cap, so /// the "behavior batch exceeds the per-batch cap" WARN stays quiet under normal load. const MAX_BATCH: usize = 512; -/// How often the delivered/rejected heartbeat is logged (JEF-240 surfacing). Kept on its +/// How often the delivered/rejected heartbeat is logged (surfacing). Kept on its /// own long cadence — decoupled from the (much shorter) debounce window so shrinking the /// window doesn't spam this operator line. const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30); -/// Default debounce/coalesce window (JEF-296). Conservative within the ticket's 2–5s band: +/// Default debounce/coalesce window. Conservative within the ticket's 2–5s band: /// long enough to collapse high-frequency near-duplicate churn into one compact batch, short /// enough that a mundane signal's freshness lag stays trivial against the engine's 300s /// evidence TTL. Tunable via `PROTECTOR_AGENT_DEBOUNCE_MS`. Alerts never wait for it. @@ -116,7 +116,7 @@ async fn main() -> anyhow::Result<()> { let endpoint = std::env::var("PROTECTOR_AGENT_ENDPOINT") .unwrap_or_else(|_| "http://protector.protector.svc.cluster.local:9999".to_string()); let debounce_window = parse_debounce_window(std::env::var("PROTECTOR_AGENT_DEBOUNCE_MS").ok()); - // The node this agent runs on (JEF-308), from the downward API (`K8S_NODE = spec.nodeName`). + // The node this agent runs on, from the downward API (`K8S_NODE = spec.nodeName`). // Stamped onto every observation and onto the per-node liveness beacon. When unset (a dev run // outside k8s) we can't attribute per node — observations go out node-less and no beacon is // sent (an un-attributable beacon would be dishonest). @@ -134,12 +134,12 @@ async fn main() -> anyhow::Result<()> { "protector-agent starting" ); let mut reporter = Reporter::new(&endpoint)?; - // Probe-attach status the observer updates and the liveness beacon reads (JEF-308). + // Probe-attach status the observer updates and the liveness beacon reads. let probes = Arc::new(ProbeStatus::default()); let (tx, mut rx) = mpsc::channel::(4096); - // Debouncing reporter task (JEF-296): coalesce mundane observations over a short window + // Debouncing reporter task: coalesce mundane observations over a short window // and flush one compact, deduped batch — collapsing the high-frequency near-duplicate // churn (repeated cluster egress, repeated execs) the engine would otherwise wake on and // dedup only after the fact. Alerts bypass the buffer and POST immediately (live @@ -161,7 +161,7 @@ async fn main() -> anyhow::Result<()> { tokio::select! { recv = rx.recv() => match recv { Some(mut obs) => { - // Stamp this agent's node (JEF-308) so the observation is node-attributed. + // Stamp this agent's node so the observation is node-attributed. obs.node = beacon_node.clone(); // `offer` returns anything to POST NOW: an alert (never debounced), // or the drained buffer if this new distinct key hit the max-size cap. @@ -199,7 +199,7 @@ async fn main() -> anyhow::Result<()> { } } _ = heartbeat.tick() => { - // JEF-240: surface cumulative delivered/rejected alongside the interval + // surface cumulative delivered/rejected alongside the interval // count so a wedged ingest (token skew → every batch 401'd) is visible // here, not just in a per-batch WARN. A rising `rejected` against a flat // `delivered` is the agent dropping 100% of signal. @@ -211,7 +211,7 @@ async fn main() -> anyhow::Result<()> { "behavioral observations reported (last {}s)", HEARTBEAT_INTERVAL.as_secs(), ); - // Per-node liveness beacon (JEF-308): sent EVERY window even when quiet + // Per-node liveness beacon: sent EVERY window even when quiet // (reported_since_tick == 0) — a quiet node with probes loaded reads // healthy-quiet, not blind. Skipped only when the node is unknown (an // un-attributable beacon would be dishonest). probes==0/0 ⇒ blind (Ready @@ -222,7 +222,7 @@ async fn main() -> anyhow::Result<()> { beacon_probes.snapshot(), reported_since_tick as u64, ); - // Liveness rides the unified envelope (JEF-336): a quiet node still POSTs + // Liveness rides the unified envelope: a quiet node still POSTs // — empty observations, liveness present — so it reads HEALTHY-quiet, not // blind, instead of the old single-beacon POST the engine 422-rejected. reporter @@ -239,7 +239,7 @@ async fn main() -> anyhow::Result<()> { }); // Collection. Default build is a no-op; `--features ebpf` loads the real probes. The observer - // updates `probes` with how many eBPF probes attached (JEF-308) — the no-op build leaves it at + // updates `probes` with how many eBPF probes attached — the no-op build leaves it at // 0/0, honestly reporting itself blind. #[cfg(not(feature = "ebpf"))] observer::NoopObserver.run(tx, probes).await; @@ -278,7 +278,7 @@ mod tests { #[test] fn agent_report_carries_node_probes_and_window_signals() { - // JEF-308: a healthy window — probes loaded, some signals. + // a healthy window — probes loaded, some signals. let r = build_agent_report("node-a", (6, 6), 12); assert_eq!(r.node, "node-a"); assert_eq!(r.probes_loaded, 6); diff --git a/agent/protector-agent/src/observer.rs b/agent/protector-agent/src/observer.rs index e49aa275..14e2d134 100644 --- a/agent/protector-agent/src/observer.rs +++ b/agent/protector-agent/src/observer.rs @@ -24,7 +24,7 @@ impl NoopObserver { _tx: Sender, probes: std::sync::Arc, ) { - // No collection ⇒ zero probes attached: the liveness beacon (JEF-308) then honestly reports + // No collection ⇒ zero probes attached: the liveness beacon then honestly reports // this node BLIND (probes_loaded == 0), never a false healthy. probes.set(0, 0); tracing::warn!( @@ -37,7 +37,7 @@ impl NoopObserver { } /// Signals-per-second over a heartbeat interval: the count of successfully attributed -/// and forwarded observations divided by the elapsed wall-clock seconds (JEF-101). Pure +/// and forwarded observations divided by the elapsed wall-clock seconds. Pure /// and kernel-free so it's unit-testable in the default build. Guards a zero/sub-tick /// elapsed (returns 0.0 rather than dividing by ~0 and reporting a nonsense spike). /// @@ -95,7 +95,7 @@ mod ebpf { /// How often to read the kernel drop counter and (if it moved) log a heartbeat. /// Drops are silent loss from a full ring; 30s keeps the signal visible without - /// spamming the log (JEF-58). + /// spamming the log. const HEARTBEAT: Duration = Duration::from_secs(30); /// Depth of the drain→attribution hand-off channel. The drain parses ring bytes @@ -113,7 +113,7 @@ mod ebpf { const PID_CACHE_CAP: usize = 8192; /// How often the attribution worker rescans `/sys/fs/cgroup` to refresh the - /// `cgroup_id → pod_uid` table (JEF-158). The agent has no pod watch (no cluster + /// `cgroup_id → pod_uid` table. The agent has no pod watch (no cluster /// credentials, ADR-0014), so a periodic rescan is how it tracks pods coming and going. /// 10s is well under a pod's lifetime: a pod created between scans simply attributes via /// the `/proc` fallback until the next scan, then via the table — never a lost signal. @@ -127,7 +127,7 @@ mod ebpf { } /// A ring event parsed into typed fields but **not yet attributed** to a pod. This is - /// the unit handed across the drain→worker boundary (JEF-64): the cheap `repr(C)` + /// the unit handed across the drain→worker boundary: the cheap `repr(C)` /// decode stays on the drain, the expensive cgroup read happens in the worker. One /// variant per probe — mirrors the `decode` dispatch. enum RawEvent { @@ -149,7 +149,7 @@ mod ebpf { new_uid: u32, }, /// Process exec: the exec'd binary path (e.g. `/usr/bin/bash`), NUL-trimmed, plus - /// the anon-inode kernel fact (JEF-317, Route A) the probe read from + /// the anon-inode kernel fact (Route A) the probe read from /// `bprm->file->f_inode` — memfd/shmem-backed or unlinked, rather than a normal /// on-disk file. Exec { @@ -159,15 +159,15 @@ mod ebpf { }, /// File write: the written file's path (e.g. `/etc/cron.d/x`), NUL-trimmed. The /// eBPF side already filtered to write-intent opens and deduped repeats to the same - /// `(pid, inode)`; this just carries the path through (JEF-306). + /// `(pid, inode)`; this just carries the path through. FileWrite { attr: EventAttr, path: String }, - /// Ptrace ATTACH access check (JEF-318): a process attempted to PTRACE_ATTACH (or + /// Ptrace ATTACH access check: a process attempted to PTRACE_ATTACH (or /// PTRACE_SEIZE / a cross-process memory access) another process — the /// process-injection primitive Falco fires critical on. No payload beyond /// attribution: the attacking pid/cgroup IS the fact (the target pid is deliberately /// not read — see the eBPF probe's doc comment). PtraceAttach { attr: EventAttr }, - /// Kernel module load (JEF-318): `init_module`/`finit_module` reached + /// Kernel module load: `init_module`/`finit_module` reached /// `load_module()`'s `security_kernel_load_data(LOADING_MODULE, …)` call — a /// container loading arbitrary code into the HOST kernel, the module-load parity /// signal Falco fires critical on. No payload beyond attribution — the occurrence is @@ -175,7 +175,7 @@ mod ebpf { ModuleLoad { attr: EventAttr }, } - /// The pair of identities every event carries for attribution (JEF-158): the in-kernel + /// The pair of identities every event carries for attribution: the in-kernel /// `cgroup_id` (the hot path — resolved via the [`CgroupTable`], works after the process /// exits) and the `pid` (the `/proc//cgroup` fallback when the table misses). #[derive(Clone, Copy)] @@ -274,12 +274,12 @@ mod ebpf { // skipped, leaving the others (and the connect kprobe) running. let (fentry_loaded, fentry_total) = Self::attach_fentry(&mut ebpf); loaded += fentry_loaded; - // Publish probe-attach status (JEF-308): the liveness beacon reads it so a Ready agent + // Publish probe-attach status: the liveness beacon reads it so a Ready agent // whose probes failed to attach (loaded == 0) reads BLIND, and a partial load reads // degraded — signal-flow liveness, not pod-Ready. let total = PROBES.len() as u32 + fentry_total; probes.set(loaded, total); - tracing::info!(loaded, total, "eBPF probes attached (JEF-308 liveness)"); + tracing::info!(loaded, total, "eBPF probes attached (liveness)"); tracing::info!("draining events"); let ring = RingBuf::try_from( @@ -287,14 +287,14 @@ mod ebpf { .ok_or_else(|| anyhow::anyhow!("EVENTS map missing"))?, )?; // The kernel's cumulative drop counter (per-CPU, one slot). Taken like - // EVENTS so we own a stable handle for the heartbeat reads (JEF-58). + // EVENTS so we own a stable handle for the heartbeat reads. let drops: PerCpuArray<_, u64> = PerCpuArray::try_from( ebpf.take_map("DROPS") .ok_or_else(|| anyhow::anyhow!("DROPS map missing"))?, )?; // The kernel's cumulative in-kernel-coalesced counter (per-CPU, one slot), // taken like DROPS so the heartbeat can surface how many connect repeats the - // dedup map suppressed at the source (JEF-65). + // dedup map suppressed at the source. let coalesced: PerCpuArray<_, u64> = PerCpuArray::try_from( ebpf.take_map("COALESCED") .ok_or_else(|| anyhow::anyhow!("COALESCED map missing"))?, @@ -306,26 +306,26 @@ mod ebpf { heartbeat.tick().await; let mut last_drops: u64 = 0; - // JEF-64: attribution is OFF the drain path. The drain only parses ring bytes + // attribution is OFF the drain path. The drain only parses ring bytes // into `RawEvent`s (cheap) and hands them to this bounded channel; a separate // worker task does the blocking `/proc//cgroup` read, builds the // `RuntimeObservation`, and forwards it to `tx`. A slow `/proc` can no longer // back the ring up — at worst the channel fills and we drop new raw events // (see `try_send` below), which the additive-evidence model tolerates. // - // JEF-65: in-kernel event aggregation now coalesces high-frequency connect + // in-kernel event aggregation now coalesces high-frequency connect // repeats at the source — a per-(pid, dest) LRU dedup map in the connect probe // suppresses a repeat seen within the dedup window so it never costs a ring slot // (cutting volume before the drain, not draining + dropping duplicates here). The // suppressed count surfaces as `coalesced` in the heartbeat below. let (raw_tx, raw_rx) = mpsc::channel::(ATTRIB_QUEUE); - // Per-node counters shared with the attribution worker (JEF-101). All are - // cumulative; the heartbeat snapshots them to surface the numbers JEF-48's + // Per-node counters shared with the attribution worker. All are + // cumulative; the heartbeat snapshots them to surface the numbers 's // exit criteria need measurable per node: ring-buffer drops, the signal rate, // and attribution quality. `Relaxed` is fine — these are monotonic counters // read for observability, not a synchronization gate. // - // JEF-115: `unresolved` now counts ONLY genuine misses (pid gone / cgroup + // `unresolved` now counts ONLY genuine misses (pid gone / cgroup // unreadable), matching the engine-side ~1.4%. The host-process firehose the // node-wide kprobe sees — readable cgroups that simply aren't pods — is the // EXPECTED case and is counted separately in `host_events`, not as a failure. @@ -358,7 +358,7 @@ mod ebpf { // immediately so draining stays fast: a full queue means // attribution is behind, and we deliberately drop this // raw event rather than block the drain (which would - // re-introduce the very ring-buffer backpressure JEF-64 + // re-introduce the very ring-buffer backpressure // removes). A closed channel means the worker exited // (receiver gone) — shut the drain down too. match raw_tx.try_send(raw) { @@ -388,21 +388,21 @@ mod ebpf { } last_drops = total; - // JEF-101: emit the per-node numbers JEF-48 needs measurable — + // emit the per-node numbers needs measurable — // cumulative ring drops, the signal rate over this interval, and // attribution quality — as a structured stat line (greppable/ // scrapeable per node, no new deps, wire payload unchanged). Unlike // the drop warning above this fires every tick so "zero drops" is // observable as a present-and-zero datapoint. // - // JEF-115: `attribution_unresolved` is now genuine misses only + // `attribution_unresolved` is now genuine misses only // (should be near-zero, matching the engine's ~1.4%); the expected // host-process firehose is reported separately as `host_events` so // it's visible without masquerading as attribution failure. let unresolved_total = unresolved.load(Ordering::Relaxed); let host_total = host_events.load(Ordering::Relaxed); let signals_total = signals.load(Ordering::Relaxed); - // JEF-65: connect repeats coalesced in-kernel (cumulative). A + // connect repeats coalesced in-kernel (cumulative). A // rising `coalesced` against a flat/low `ring_drops` is the dedup // working — volume cut at the source before it can pressure the ring. let coalesced_total = Self::sum_percpu(&coalesced); @@ -432,12 +432,12 @@ mod ebpf { } /// The attribution worker: the slow half of the split, off the drain path - /// (JEF-64). Receives parsed-but-unattributed [`RawEvent`]s, resolves each to a pod + /// . Receives parsed-but-unattributed [`RawEvent`]s, resolves each to a pod /// UID, builds the `RuntimeObservation`, and forwards it to `tx`. Exits when the /// drain drops its sender (`recv` → `None`) or the report receiver is gone /// (`tx.send` errors) — either way a clean shutdown. /// - /// JEF-158: attribution now resolves the event's in-kernel `cgroup_id` against a + /// attribution now resolves the event's in-kernel `cgroup_id` against a /// [`CgroupTable`] built from `/sys/fs/cgroup` FIRST. A table hit needs no `/proc` /// read, so a short-lived in-container exec/shell that has already exited still /// attributes — the exited-process race the post-hoc `/proc//cgroup` read keeps @@ -447,7 +447,7 @@ mod ebpf { /// watch (ADR-0014), so a periodic rescan of the cgroup hierarchy is how it tracks /// pods coming and going. /// - /// JEF-115 (unchanged): three outcomes. A pod is forwarded; a readable non-pod + /// (unchanged): three outcomes. A pod is forwarded; a readable non-pod /// cgroup (the host-process firehose) is dropped and counted as a `host_event` /// (EXPECTED, not a failure); an unreadable cgroup (pid gone) is the only case /// counted as `unresolved` — a genuine miss. @@ -461,16 +461,13 @@ mod ebpf { // The hot-path table (cgroup_id → pod_uid), rescanned from /sys/fs/cgroup on an // interval. Built once up front so the very first events can resolve. let mut table = scan_cgroupfs(cgroup_root()); - tracing::info!( - pods = table.len(), - "cgroup attribution table built (JEF-158)" - ); + tracing::info!(pods = table.len(), "cgroup attribution table built"); let mut rescan = tokio::time::interval(CGROUP_RESCAN); rescan.tick().await; // consume the immediate first tick // Per-pid cache for the FALLBACK `/proc` read only — a table miss from a chatty // host pid shouldn't re-read `/proc` per event. Bounded; cleared wholesale at cap. let mut fallback_cache: HashMap = HashMap::new(); - // Pod UIDs we've already reported entrypoint linkage for (JEF-407). Linkage is a + // Pod UIDs we've already reported entrypoint linkage for. Linkage is a // stable per-image fact, so we classify `/proc//exe` once per pod on its first // exec and never again — one ELF read per pod, not per exec. Bounded like the pid // cache; cleared wholesale at the cap (a re-report on a churned pod is harmless — @@ -496,18 +493,18 @@ mod ebpf { PodAttribution::NotAPod => { // The node-wide kprobe's expected host firehose — dropped (never // fatal). Counted apart from misses so it doesn't masquerade as - // attribution failure (JEF-115). + // attribution failure. host_events.fetch_add(1, Ordering::Relaxed); continue; } PodAttribution::Unreadable => { // pid gone / cgroup unreadable — a genuine miss. This is what - // JEF-48's "low unresolved attribution" measures per node. + // 's "low unresolved attribution" measures per node. unresolved.fetch_add(1, Ordering::Relaxed); continue; } }; - // JEF-407: an exec is our chance to classify the workload's ENTRYPOINT linkage + // an exec is our chance to classify the workload's ENTRYPOINT linkage // — `/proc//exe` is the exec'd binary. Capture the pid before `raw` is // consumed; only an Exec triggers a linkage classification, and only the first // time we see a given pod (linkage is a stable per-image fact). @@ -516,7 +513,7 @@ mod ebpf { attribution: Attribution::by_pod_uid(uid.clone()), source: Some(SOURCE.into()), observed_at_ms: now_ms(), - // The agent's node (JEF-308) is stamped by the flusher in `main` from `K8S_NODE` + // The agent's node is stamped by the flusher in `main` from `K8S_NODE` // — kept in one place, so the ebpf worker leaves it unset here. node: None, behavior: raw.into_behavior(), @@ -527,7 +524,7 @@ mod ebpf { // A signal successfully attributed and forwarded — the rate numerator. signals.fetch_add(1, Ordering::Relaxed); - // Emit the entrypoint's static/dynamic linkage once per pod (JEF-407). Bounds + // Emit the entrypoint's static/dynamic linkage once per pod. Bounds // the ELF read to one-per-pod, and drops an unknown classification (unreadable // exe / non-ELF) rather than guessing — the engine then keeps its prior // `static_binary == None` behavior for that workload. @@ -557,7 +554,7 @@ mod ebpf { } } - /// Resolve one event's [`EventAttr`] to a [`PodAttribution`] (JEF-158): the in-kernel + /// Resolve one event's [`EventAttr`] to a [`PodAttribution`]: the in-kernel /// `cgroup_id` against `table` first (no `/proc` — the exited-process-safe hot path), /// then the `/proc//cgroup` fallback on a miss, memoized in `cache` so a flood /// from one pid doesn't re-read `/proc`. Every fallback outcome (pod, host non-pod, @@ -584,8 +581,8 @@ mod ebpf { } /// Sum a single-slot per-CPU `u64` counter across all CPUs into its cumulative - /// total. Shared by the ring-drop counter (JEF-58) and the in-kernel-coalesced - /// counter (JEF-65) — both are the same one-slot `PerCpuArray` shape. A + /// total. Shared by the ring-drop counter and the in-kernel-coalesced + /// counter — both are the same one-slot `PerCpuArray` shape. A /// per-CPU read failure is treated as 0 for that read (best-effort observability — /// never errors the drain). fn sum_percpu( @@ -601,7 +598,7 @@ mod ebpf { /// (program name in the object, kernel function it hooks). fentry attaches via /// BTF, so it's separate from the kprobe table; the BTF is loaded once. Returns /// `(attached, attempted)` so the caller can publish the probe-attach status the - /// per-node liveness beacon reads (JEF-308) — a partial load reads degraded. + /// per-node liveness beacon reads — a partial load reads degraded. fn attach_fentry(ebpf: &mut Ebpf) -> (u32, u32) { const FENTRY_PROBES: &[(&str, &str)] = &[ ("file_open", "security_file_open"), @@ -655,7 +652,7 @@ mod ebpf { /// drain path: only the `repr(C)` byte parse (no `/proc`, no allocation beyond the /// path string). Returns `None` for a truncated event, an unknown kind, or an /// empty path — all dropped, never fatal. Attribution (the cgroup read) happens - /// later in the worker (JEF-64). + /// later in the worker. fn decode(data: &[u8]) -> Option { if data.len() < std::mem::size_of::() { return None; @@ -711,7 +708,7 @@ mod ebpf { let ev = unsafe { std::ptr::read_unaligned(data.as_ptr().cast::()) }; Self::file_write(&ev) } - // JEF-318: both bodies ARE the header — already parsed above, and its length + // both bodies ARE the header — already parsed above, and its length // already checked at the top of this function — so no further byte parse. KIND_PTRACE_ATTACH => Some(RawEvent::PtraceAttach { attr: EventAttr::from_header(&header), @@ -780,7 +777,7 @@ mod ebpf { /// Parse a process-exec event into a raw Exec. `path` is the exec'd binary path as /// the kernel saw it (`linux_binprm->filename`), NUL-trimmed; the behavior crate - /// coarsens it to the basename for the fingerprint. `exe_anon_inode` (JEF-317, + /// coarsens it to the basename for the fingerprint. `exe_anon_inode` ( /// Route A) carries the probe's `bprm->file->f_inode` fact straight through — a /// non-zero kernel byte is `true`, never inferred from `path`. Drops empty paths. /// Pure (no `/proc`). @@ -803,7 +800,7 @@ mod ebpf { /// as the kernel saw it (`bpf_d_path`), NUL-trimmed; the behavior crate coarsens it /// to the dirname for the fingerprint. The eBPF side already filtered to write-intent /// opens and deduped repeats to the same `(pid, inode)`, so this just carries the - /// path through. Drops empty paths. Pure (no `/proc`). PURE DATA (JEF-306): the + /// path through. Drops empty paths. Pure (no `/proc`). PURE DATA: the /// container-drift / tamper *classification* is engine policy (F3), not done here. fn file_write(ev: &FileEvent) -> Option { let len = (ev.len as usize).min(PATH_CAP); @@ -821,7 +818,7 @@ mod ebpf { } /// Read a pid's cgroup membership text (`/proc//cgroup`). The blocking read kept - /// off the drain path (JEF-64): called only from the attribution worker. `None` if the + /// off the drain path: called only from the attribution worker. `None` if the /// process is gone or unreadable (a host process or an exited pid) — the event is then /// dropped, never fatal. fn read_cgroup(pid: u32) -> Option { @@ -864,7 +861,7 @@ mod rate_tests { #[test] fn zero_signals_is_zero_rate() { // A quiet interval must report 0.0, not absence — present-and-zero is the - // "no drops / no traffic" datapoint JEF-48 needs. + // "no drops / no traffic" datapoint needs. assert_eq!(signal_rate(0, Duration::from_secs(30)), 0.0); } diff --git a/agent/protector-agent/src/observer/ebpf/observer_ebpf_tests.rs b/agent/protector-agent/src/observer/ebpf/observer_ebpf_tests.rs index 08f615bd..694a5815 100644 --- a/agent/protector-agent/src/observer/ebpf/observer_ebpf_tests.rs +++ b/agent/protector-agent/src/observer/ebpf/observer_ebpf_tests.rs @@ -15,7 +15,7 @@ fn attr(pid: u32, cgroup_id: u64) -> EventAttr { #[test] fn resolve_uses_the_table_and_never_reads_proc_on_a_cgroup_id_hit() { - // JEF-158 hot path: a cgroup_id table hit resolves with NO `/proc` read and NO + // hot path: a cgroup_id table hit resolves with NO `/proc` read and NO // fallback-cache entry — which is what lets an already-exited process attribute. let table = crate::pod::build_cgroup_table([(100u64, POD_SLICE.to_string())]); let mut cache = HashMap::new(); @@ -138,7 +138,7 @@ fn decode_priv_change_parses_uids() { } /// Build an [`ExecEvent`] with a NUL-terminated `path` and the given `exe_anon_inode` byte -/// (JEF-317, Route A). +/// (Route A). fn exec_event(kind_pid_cgroup: (u32, u32, u64), bin: &[u8], exe_anon_inode: u8) -> ExecEvent { let (kind, pid, cgroup_id) = kind_pid_cgroup; let mut path = [0u8; PATH_CAP]; @@ -159,7 +159,7 @@ fn exec_event(kind_pid_cgroup: (u32, u32, u64), bin: &[u8], exe_anon_inode: u8) fn decode_exec_parses_path_and_maps_to_process_exec() { // A KIND_EXEC ExecEvent carrying a NUL-terminated exec path must decode to a // RawEvent::Exec, and into_behavior must map it to Behavior::ProcessExec whose - // fingerprint coarsens to the basename (JEF-53). exe_anon_inode == 0 here — the + // fingerprint coarsens to the basename. exe_anon_inode == 0 here — the // ordinary, non-anonymous case. let ev = exec_event((KIND_EXEC, 4321, 999), b"/usr/bin/bash\0", 0); let bytes = unsafe { @@ -205,7 +205,7 @@ fn decode_exec_parses_path_and_maps_to_process_exec() { #[test] fn decode_exec_carries_the_anon_inode_flag_through() { - // A KIND_EXEC ExecEvent with exe_anon_inode == 1 (JEF-317, Route A: the kernel's own + // A KIND_EXEC ExecEvent with exe_anon_inode == 1 (Route A: the kernel's own // f_inode read, not a path-shape guess) must decode and map the flag through verbatim // — never inferred from the path, which here looks like an ordinary on-disk binary. let ev = exec_event((KIND_EXEC, 1, 2), b"/bin/bash\0", 1); @@ -230,7 +230,7 @@ fn decode_exec_carries_the_anon_inode_flag_through() { fn decode_file_write_parses_path_and_maps_to_file_write() { // A KIND_FILE_WRITE FileEvent carrying a NUL-terminated path must decode to a // RawEvent::FileWrite with attribution, and into_behavior must map it to - // Behavior::FileWrite whose fingerprint coarsens to the dirname (JEF-306). + // Behavior::FileWrite whose fingerprint coarsens to the dirname. let mut path = [0u8; PATH_CAP]; let file = b"/etc/cron.d/dropper\0"; path[..file.len()].copy_from_slice(file); @@ -273,7 +273,7 @@ fn decode_file_write_parses_path_and_maps_to_file_write() { #[test] fn decode_ptrace_attach_parses_with_no_body_beyond_the_header() { - // JEF-318: a KIND_PTRACE_ATTACH event IS an EventHeader — no extra bytes, unlike every + // a KIND_PTRACE_ATTACH event IS an EventHeader — no extra bytes, unlike every // other kind's body. Decode must still succeed on exactly `size_of::()` // bytes and attribute + map it to Behavior::PtraceAttach. let header = EventHeader { @@ -301,7 +301,7 @@ fn decode_ptrace_attach_parses_with_no_body_beyond_the_header() { #[test] fn decode_module_load_parses_with_no_body_beyond_the_header() { - // JEF-318: same header-only shape as KIND_PTRACE_ATTACH, distinct kind + behavior. + // same header-only shape as KIND_PTRACE_ATTACH, distinct kind + behavior. let header = EventHeader { kind: KIND_MODULE_LOAD, pid: 555, diff --git a/agent/protector-agent/src/pod.rs b/agent/protector-agent/src/pod.rs index e7e60259..2c38d39a 100644 --- a/agent/protector-agent/src/pod.rs +++ b/agent/protector-agent/src/pod.rs @@ -3,7 +3,7 @@ //! An eBPF event carries a kernel identity, not a pod. There are two ways to recover the //! pod UID, and this module owns both: //! -//! - **In-kernel cgroup id (JEF-158, the hot path).** The probe stamps each event with +//! - **In-kernel cgroup id (the hot path).** The probe stamps each event with //! `bpf_get_current_cgroup_id()` — the cgroup v2 directory inode — captured while the //! process is still live. Userspace keeps a [`CgroupTable`] mapping that id to the pod //! UID, built by scanning `/sys/fs/cgroup` (each kubepods cgroup directory's inode is @@ -19,7 +19,7 @@ //! A cgroup that isn't a pod's (a host process) yields `None` and the event is dropped — //! a missing signal beats a mis-attributed one. -/// The outcome of resolving a pid to a pod (JEF-115). The node-wide kprobe sees the +/// The outcome of resolving a pid to a pod. The node-wide kprobe sees the /// whole host firehose, so most events are *expected* non-pods, not failures. Keeping /// the two apart is what lets the agent's `attribution_unresolved` stat mean a GENUINE /// miss (matching the engine's ~1.4%) rather than the host-process noise floor: @@ -84,7 +84,7 @@ pub fn parse_pod_uid(cgroup_path: &str) -> Option { None } -/// A snapshot of `cgroup_id → pod_uid`, the in-kernel attribution table (JEF-158). +/// A snapshot of `cgroup_id → pod_uid`, the in-kernel attribution table. /// /// The eBPF probe stamps each event with `bpf_get_current_cgroup_id()` (the cgroup v2 /// directory inode). This table maps that id straight to the pod UID, so a hot-path event @@ -123,7 +123,7 @@ impl CgroupTable { } } -/// Build a [`CgroupTable`] from `(cgroup_id, cgroup_path)` pairs (JEF-158). Pure — the +/// Build a [`CgroupTable`] from `(cgroup_id, cgroup_path)` pairs. Pure — the /// filesystem walk is injected so this is unit-testable without a real `/sys/fs/cgroup`. /// Only paths that [`parse_pod_uid`] recognizes as a pod cgroup are kept (host cgroups are /// dropped); a `cgroup_id` of `0` is skipped (it can never match an event, see @@ -145,7 +145,7 @@ pub fn build_cgroup_table(entries: impl IntoIterator) -> C } /// Walk the cgroup v2 hierarchy under `root` (normally `/sys/fs/cgroup`) and build the -/// [`CgroupTable`] (JEF-158). For every directory, the directory's **inode number is the +/// [`CgroupTable`]. For every directory, the directory's **inode number is the /// cgroup id** that `bpf_get_current_cgroup_id()` returns for tasks in it, so we pair each /// directory's inode with its path and let [`build_cgroup_table`] keep the pod ones. /// @@ -191,7 +191,7 @@ pub fn scan_cgroupfs(root: &std::path::Path) -> CgroupTable { } /// Resolve an event's attribution from its in-kernel `cgroup_id` first, falling back to -/// the per-event `/proc//cgroup` read only on a table miss (JEF-158). This is the +/// the per-event `/proc//cgroup` read only on a table miss. This is the /// single decision point that keeps the hot path off `/proc`: /// /// - A table hit is a [`Pod`](PodAttribution::Pod) — resolved with no `/proc` read, so it @@ -268,7 +268,7 @@ mod tests { assert_eq!(classify_cgroup(None), PodAttribution::Unreadable); } - // ---- JEF-158: cgroup_id → pod_uid table (build, lookup, scan, resolve+fallback) ---- + // ---- cgroup_id → pod_uid table (build, lookup, scan, resolve+fallback) ---- const POD_SLICE: &str = "/sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/\ kubepods-besteffort-pod3f5e1a2b_4c6d_7e8f_9a0b_1c2d3e4f5a6b.slice"; diff --git a/agent/protector-agent/src/reporter.rs b/agent/protector-agent/src/reporter.rs index e8946f44..04bb5167 100644 --- a/agent/protector-agent/src/reporter.rs +++ b/agent/protector-agent/src/reporter.rs @@ -1,10 +1,10 @@ //! The reporter: batches a window's observations and (when this node has one) its per-node //! liveness beacon into ONE [`RuntimeReport`] envelope and POSTs it to the engine's unified -//! behavioral ingest (`/behavior`, ADR-0014 / JEF-336). In-cluster, mesh-protected hop; the agent +//! behavioral ingest (`/behavior`, ADR-0014). In-cluster, mesh-protected hop; the agent //! never sends behavioral data anywhere else (the data is a map of the cluster — it stays //! in-cluster, per VISION's local-first conviction). //! -//! One endpoint, one envelope (JEF-336): liveness always travels with the report, so a quiet node +//! One endpoint, one envelope: liveness always travels with the report, so a quiet node //! still POSTs (empty observations, liveness present) and the engine reads it HEALTHY-quiet rather //! than blind. This replaced a separate `/agent-liveness` beacon POST that shipped a single //! `AgentReport` the engine's array-typed handler 422-rejected — the "no agents connected" bug. @@ -14,7 +14,7 @@ //! is the shared secret the engine also reads; authentication (this header) is //! complementary to the cluster's Linkerd mesh authorization. //! -//! ## Self-healing token rotation (JEF-240) +//! ## Self-healing token rotation //! //! The token is read once at startup, but the kubelet updates the mounted secret file //! in place when the Secret rolls. If the engine and agent read `protector-ingest-auth` @@ -40,13 +40,13 @@ const RERESOLVE_AFTER_401S: u32 = 3; /// wedged ingest is loud once (and on a slow cadence) rather than a WARN every 30s. const ERROR_EVERY_N_REJECTIONS: u64 = 20; -/// Resolves the ingest bearer from the environment — the seam JEF-240 re-invokes to pick +/// Resolves the ingest bearer from the environment — the seam re-invokes to pick /// up a rotated secret file. Boxed so tests can inject a deterministic, mutating source /// (a stale-then-fresh token) without touching the filesystem or sleeping. type TokenSource = Box Option + Send>; /// POSTs per-window [`RuntimeReport`] envelopes (observations + optional per-node liveness beacon, -/// JEF-336) to `{base}/behavior`. +/// ) to `{base}/behavior`. pub struct Reporter { client: reqwest::Client, url: String, @@ -54,7 +54,7 @@ pub struct Reporter { /// `Authorization` header (the engine then runs the ingest unauthenticated, which /// it warns about); set it once the Secret has rolled out. token: Option, - /// Re-resolves the token on sustained 401s (JEF-240). Defaults to reading + /// Re-resolves the token on sustained 401s. Defaults to reading /// `PROTECTOR_INGEST_TOKEN_FILE` / `PROTECTOR_INGEST_TOKEN`. token_source: TokenSource, /// Consecutive 401s since the last accepted (2xx) batch. Drives both re-resolution @@ -102,7 +102,7 @@ impl Reporter { /// `base` is the engine's runtime-ingest URL (e.g. /// `http://protector.protector.svc.cluster.local:9999`). The ingest token is read /// once from the environment (file before env); on sustained 401s it is re-read from - /// the same source (JEF-240). + /// the same source. pub fn new(base: &str) -> anyhow::Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(10)) @@ -110,7 +110,7 @@ impl Reporter { Ok(Self::with_source(client, base, Box::new(ingest_token))) } - /// Construct over an explicit token source — the JEF-240 seam. `source` is resolved + /// Construct over an explicit token source — the seam. `source` is resolved /// once now (the startup read) and re-invoked on sustained 401s. Used by the crate's /// tests to inject a stale-then-fresh token deterministically. fn with_source(client: reqwest::Client, base: &str, mut source: TokenSource) -> Self { @@ -144,7 +144,7 @@ impl Reporter { req } - /// Cumulative (delivered, rejected) tallies for the periodic heartbeat (JEF-240). + /// Cumulative (delivered, rejected) tallies for the periodic heartbeat. /// `delivered` counts accepted observations; `rejected` counts rejected batches. pub fn counters(&self) -> (u64, u64) { (self.delivered_total, self.rejected_total) @@ -167,7 +167,7 @@ impl Reporter { tracing::info!( consecutive_401s = self.consecutive_401s, "ingest token re-resolved after sustained 401s — retrying with the \ - current secret (JEF-240 self-heal)" + current secret (self-heal)" ); self.token = fresh; } else { @@ -193,7 +193,7 @@ impl Reporter { } } - /// Send one per-window [`RuntimeReport`] envelope (observations + optional liveness, JEF-336); + /// Send one per-window [`RuntimeReport`] envelope (observations + optional liveness); /// returns how many observations were accepted (0 on failure, or when the envelope carries /// neither observations nor liveness). An envelope with empty observations but a liveness beacon /// IS sent — that is the quiet-node path that keeps a silent node reading HEALTHY-quiet, not @@ -201,7 +201,7 @@ impl Reporter { /// a lost report costs a little freshness, never correctness, and must never wedge the agent. /// The caller rolls the count into an interval heartbeat; per-send detail stays at debug. /// - /// On a run of 401s the token is re-resolved (JEF-240) so a secret rotation self-heals + /// On a run of 401s the token is re-resolved so a secret rotation self-heals /// without a pod restart; the run-length resets on the first 2xx. pub async fn send(&mut self, report: &RuntimeReport) -> usize { if report.observations.is_empty() && report.liveness.is_none() { @@ -301,7 +301,7 @@ mod tests { assert_eq!(auth, "Bearer s3cr3t"); } - /// JEF-336: the unified envelope — including a quiet-node liveness-only report — POSTs to the + /// the unified envelope — including a quiet-node liveness-only report — POSTs to the /// single `{base}/behavior` route carrying the same bearer (no separate `/agent-liveness`). #[test] fn liveness_rides_the_behavior_envelope_with_bearer() { @@ -344,7 +344,7 @@ mod tests { assert_eq!(req.url().as_str(), "http://engine.svc:9999/behavior"); } - /// JEF-240: a token source backed by a shared cell the test flips, so the re-resolve + /// a token source backed by a shared cell the test flips, so the re-resolve /// seam is exercised deterministically with no real sleeps or filesystem. fn rotating_source(cell: std::sync::Arc>>) -> TokenSource { Box::new(move || cell.lock().unwrap().clone()) @@ -427,7 +427,7 @@ mod tests { assert_eq!(reporter.consecutive_401s, 1); } - /// Counters track delivered vs rejected for the heartbeat (JEF-240 surfacing). + /// Counters track delivered vs rejected for the heartbeat (surfacing). #[test] fn counters_tally_rejections() { let mut reporter = reporter_with(Some("t")); diff --git a/behavior/src/elf.rs b/behavior/src/elf.rs index ce1c4ebc..d6c450d5 100644 --- a/behavior/src/elf.rs +++ b/behavior/src/elf.rs @@ -1,5 +1,5 @@ -//! Minimal ELF static-linkage classification (JEF-404), shared by the engine and the -//! first-party agent (JEF-407). +//! Minimal ELF static-linkage classification, shared by the engine and the +//! first-party agent. //! //! Reachability is proven by correlating a CVE's package against runtime //! [`Behavior::LibraryLoaded`](crate::Behavior::LibraryLoaded) events: a `.so` the kernel @@ -20,7 +20,7 @@ //! It is byte-only and pure: give it the leading bytes of a binary, get back whether it is //! statically linked. That keeps it fully unit-testable with tiny synthetic fixtures and //! keeps *where the bytes come from* a separate plumbing concern (the engine had none in -//! prod until JEF-407 wired the agent as the byte source). +//! prod until wired the agent as the byte source). /// The four-byte ELF magic (`0x7f 'E' 'L' 'F'`) every ELF file starts with. const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F']; @@ -39,7 +39,7 @@ const ELFDATA2MSB: u8 = 2; /// valid executable) means static linkage. const PT_INTERP: u32 = 3; -/// Classify a binary's ELF header as statically vs dynamically linked (JEF-404). +/// Classify a binary's ELF header as statically vs dynamically linked. /// /// Returns: /// - `Some(true)` — a valid ELF with **no** `PT_INTERP` program header: statically linked. @@ -110,7 +110,7 @@ pub fn elf_static_linkage(bytes: &[u8]) -> Option { /// Read a little/big-endian `u16` at `off`, or `None` if `off + 2` overflows or runs past /// the end. `checked_add` keeps a crafted near-`usize::MAX` offset from overflow-panicking -/// in a debug build (JEF-407 hardening) — the `bytes.get` bound then handles the truncation. +/// in a debug build (hardening) — the `bytes.get` bound then handles the truncation. fn read_u16(bytes: &[u8], off: usize, le: bool) -> Option { let end = off.checked_add(2)?; let b: [u8; 2] = bytes.get(off..end)?.try_into().ok()?; diff --git a/behavior/src/elf/tests.rs b/behavior/src/elf/tests.rs index f32bbbe7..e06d34d5 100644 --- a/behavior/src/elf/tests.rs +++ b/behavior/src/elf/tests.rs @@ -1,4 +1,4 @@ -//! Unit tests for the ELF static-linkage classifier (JEF-404). Fixtures are built as the +//! Unit tests for the ELF static-linkage classifier. Fixtures are built as the //! smallest representative ELF byte layouts — a 64-bit little-endian header plus a program //! header table — so a `PT_INTERP` entry (dynamic) and its absence (static) classify //! differently WITHOUT shipping a real multi-megabyte binary. The classifier reads only the @@ -84,7 +84,7 @@ fn dynamic_elf_carries_an_interp_program_header() { #[test] fn static_vs_dynamic_classify_differently() { - // The core JEF-404 distinction: the same shape with vs without PT_INTERP must differ. + // The core distinction: the same shape with vs without PT_INTERP must differ. let stat = elf64_le(&[PT_LOAD]); let dynm = elf64_le(&[PT_LOAD, PT_INTERP_T]); assert_ne!(elf_static_linkage(&stat), elf_static_linkage(&dynm)); @@ -155,7 +155,7 @@ fn out_of_range_program_header_table_is_unknown() { #[test] fn crafted_max_offset_returns_none_without_overflow_panicking() { - // JEF-407 hardening: a crafted `e_phoff` near u64::MAX must NOT overflow-panic when the + // hardening: a crafted `e_phoff` near u64::MAX must NOT overflow-panic when the // parser forms `phoff + i*phentsize` and the per-read `off + N` slice — it must return an // honest `None`. Without the `checked_add` in the read helpers this panics on a debug // build (`attempt to add with overflow`); with it, it's a clean unknown. diff --git a/behavior/src/lib.rs b/behavior/src/lib.rs index a525cc95..5820958a 100644 --- a/behavior/src/lib.rs +++ b/behavior/src/lib.rs @@ -29,9 +29,9 @@ pub enum Behavior { /// A read of a secret. `source` distinguishes *how* it was read: a mounted-file read /// (the eBPF agent's on-disk path), a Kubernetes API GET/LIST/WATCH via the /// workload's ServiceAccount RBAC (observed engine-side from the apiserver audit log, - /// JEF-269), or a well-known ON-HOST credential path — the host shadow file, an SSH + /// ), or a well-known ON-HOST credential path — the host shadow file, an SSH /// private-key dir, a cloud-credential file (observed engine-side from the path alone, - /// JEF-320) — three genuinely different runtime facts that all reach credential + /// ) — three genuinely different runtime facts that all reach credential /// material. Older sensors omit `source`, which defaults to /// [`SecretReadSource::Mounted`] (the only kind eBPF originally saw), preserving the /// pre-existing wire shape. @@ -54,15 +54,15 @@ pub enum Behavior { /// agent's privilege-change probe, fentry on `security_task_fix_setuid`). Model /// evidence, not blanket corroboration: /// legitimate workloads sometimes escalate (init/entrypoint), so wiring this to - /// corroborate a specific attack is JEF-49's job. + /// corroborate a specific attack is 's job. PrivilegeChange { from_uid: u32, to_uid: u32 }, /// A process was exec'd in the workload — the runtime signal for "unexpected process /// spawned" (ADR-0014). `path` is the exec'd binary's path as the kernel saw it /// (`linux_binprm->filename`). PURE DATA: whether a `path` is a shell / package manager - /// is engine classification (`observe::exec_class`, JEF-113), not a property of this + /// is engine classification (`observe::exec_class`), not a property of this /// shared wire type. /// - /// `exe_anon_inode` (JEF-317, Route A) is a SEPARATE kernel-observed fact, not derived + /// `exe_anon_inode` (Route A) is a SEPARATE kernel-observed fact, not derived /// from `path`: whether the exec'd binary's backing inode is anonymous — memfd/shmem- /// backed, or unlinked (`i_nlink == 0`) — rather than a normal, linked, on-disk file. /// This is the Falco-parity signal ("memfd_create + execve of an anonymous fd") a path @@ -70,7 +70,7 @@ pub enum Behavior { /// `bprm->filename` for a benign `fexecve()` of an on-disk file as it does for a real /// memfd payload, so an earlier version of this signal that classified the *path shape* /// was withdrawn (a security review caught it forging corroboration on routine - /// behavior — see JEF-317). The exec probe now reads `bprm->file->f_inode` directly + /// behavior — see). The exec probe now reads `bprm->file->f_inode` directly /// instead. Defaulted `false` (an older sensor, or a sensor without inode access, omits /// it) — never inferred, so an unset flag reads as "not anonymous", never guessed /// `true`. A raw kernel fact, not a verdict: whether it's alarming is engine policy @@ -85,14 +85,14 @@ pub enum Behavior { /// (a new file created then run) and config tampering (an existing file overwritten). /// The eBPF agent's file-write probe (fentry on `security_file_open` filtered to /// write-intent open flags, ADR-0014). `path` is the - /// written file's path as the kernel saw it (`bpf_d_path`). PURE DATA (JEF-306): whether + /// written file's path as the kernel saw it (`bpf_d_path`). PURE DATA: whether /// the path is *sensitive* — the container-drift / tamper judgement — is engine - /// corroboration policy (JEF-306 F3), not a property of this shared wire type. The agent + /// corroboration policy (F3), not a property of this shared wire type. The agent /// emits the path; the engine classifies. Model evidence only today. FileWrite { path: String }, - /// The workload's entrypoint binary's **static/dynamic linkage** (JEF-407) — read by + /// The workload's entrypoint binary's **static/dynamic linkage** — read by /// the node-local agent from the executable's ELF header (`/proc//exe`, no - /// `PT_INTERP` ⇒ statically linked). This is the byte source that ACTIVATES JEF-404's + /// `PT_INTERP` ⇒ statically linked). This is the byte source that ACTIVATES 's /// static-linkage reachability in prod: the engine has no in-cluster access to the /// entrypoint bytes, so without this signal `Image::static_binary` stays `None` and a /// Go / musl-static CVE renders `not-observed` forever. `static_linkage == true` ⇒ a @@ -103,9 +103,9 @@ pub enum Behavior { /// corroborates ([`Self::is_alert`] is false) and is CONTEXT only. Reported over the /// SAME behavioral channel (ADR-0014), so no new egress (the zero-egress invariant /// holds — the agent already sees `/proc//exe`). PURE DATA: the agent classifies - /// the bytes; the *reachability* consequence is engine policy (JEF-404). + /// the bytes; the *reachability* consequence is engine policy. ImageLinkage { static_linkage: bool }, - /// A ptrace ATTACH access check (JEF-318, Retire-Falco G2): the eBPF agent's + /// A ptrace ATTACH access check (Retire-Falco G2): the eBPF agent's /// `security_ptrace_access_check` probe, filtered in-kernel to `mode & /// PTRACE_MODE_ATTACH` so the read-only `PTRACE_MODE_READ` checks `/proc//…` makes /// constantly never reach the wire. The classic process-injection primitive Falco fires @@ -114,17 +114,17 @@ pub enum Behavior { /// occurrence fact — the attacking workload is already carried by /// [`RuntimeObservation::attribution`], and the target process's pid is deliberately not /// read by the agent (a `struct task_struct` offset read judged too fragile for this - /// signal — see the agent's probe doc). PURE DATA (JEF-113): whether an attach on this + /// signal — see the agent's probe doc). PURE DATA: whether an attach on this /// entry is alarming is engine policy (`engine::reason::proof::corroborate`), /// conservatively foothold-scoped, not decided here. PtraceAttach, - /// A kernel module load (JEF-318, Retire-Falco G2): the eBPF agent's + /// A kernel module load (Retire-Falco G2): the eBPF agent's /// `security_kernel_load_data` probe, filtered in-kernel to `id == LOADING_MODULE` so /// firmware/kexec/policy/x509 loads on the SAME hook never reach the wire. Covers BOTH /// `init_module` and `finit_module` — `load_module()` reaches this hook on either path. /// The module-load parity signal Falco fires critical on: a container loading arbitrary /// code into the HOST kernel. No fields — the occurrence, attributed by - /// [`RuntimeObservation::attribution`], is the whole fact. PURE DATA (JEF-113): engine + /// [`RuntimeObservation::attribution`], is the whole fact. PURE DATA: engine /// policy decides whether it's alarming, conservatively foothold-scoped, not this crate. ModuleLoad, } @@ -132,7 +132,7 @@ pub enum Behavior { /// How a [`Behavior::SecretRead`] was observed — a type distinction, not a string /// convention. The wire type stays cluster-agnostic (ADR-0003): a sensor names only the /// *kind* of read it saw; the engine, not the agent, resolves the ServiceAccount→edge -/// attribution for an API read (JEF-269). +/// attribution for an API read. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SecretReadSource { @@ -147,8 +147,8 @@ pub enum SecretReadSource { Api, /// The path read is a well-known ON-HOST sensitive credential path — the host /// password/shadow file, a per-user SSH private-key directory, or a cloud-provider - /// credential file — outside any k8s Secret mount (JEF-320, Retire-Falco G3). The - /// eBPF agent still only emits a path (pure data, JEF-113); the engine classifies it + /// credential file — outside any k8s Secret mount (Retire-Falco G3). The + /// eBPF agent still only emits a path (pure data); the engine classifies it /// (`engine::observe::host_credential_class`), same division of labor as `Mounted`. HostPath, } @@ -163,7 +163,7 @@ impl SecretReadSource { /// Whether `b` is `false` — a named predicate for `#[serde(skip_serializing_if)]` (no /// built-in one exists for `bool`). Used to omit a `false` anon-inode-exec flag from the -/// wire (JEF-317), keeping the common (non-anonymous) exec's JSON byte-identical to before +/// wire, keeping the common (non-anonymous) exec's JSON byte-identical to before /// this field existed. fn is_false(b: &bool) -> bool { !b @@ -174,7 +174,7 @@ fn is_false(b: &bool) -> bool { /// path to a stable, low-cardinality cache token. /// /// Note: exec *classification* (is this a shell / package manager?) is engine policy, not -/// part of this wire type — it lives in `engine::observe::exec_class` (JEF-113), keyed on +/// part of this wire type — it lives in `engine::observe::exec_class`, keyed on /// this same basename token, so a list change rebuilds only the engine, never the agent. fn basename(path: &str) -> &str { path.rsplit('/').next().unwrap_or(path) @@ -207,7 +207,7 @@ impl Behavior { /// A stable, **low-cardinality** label naming this behavior's variant — one of a /// fixed, small set (`alert`/`connection`/`secret-read`/`library-load`/`file-read`/ /// `priv-change`/`exec`). Used as a metric label for behavioral-signal counters - /// (JEF-100): it must never carry per-instance payload (a peer, a path, a secret + /// : it must never carry per-instance payload (a peer, a path, a secret /// name), which would explode metric cardinality — only the variant name. Distinct /// from [`Self::summary`] (human prose) and [`Self::fingerprint_key`] (cache key). pub fn variant_label(&self) -> &'static str { @@ -229,7 +229,7 @@ impl Behavior { /// A one-line, human summary for the adjudication prompt. For a /// [`Behavior::ProcessExec`] this is the bare `executed {path}` — *classification* of a /// notable exec (shell / package manager in container) is engine policy - /// (`engine::observe::exec_class`, JEF-113), not a property of this shared wire type, so + /// (`engine::observe::exec_class`), not a property of this shared wire type, so /// the engine annotates the path when it builds the prompt/output line rather than /// this crate baking a rule list into the contract. pub fn summary(&self) -> String { @@ -251,7 +251,7 @@ impl Behavior { Behavior::PrivilegeChange { from_uid, to_uid } => { format!("privilege change uid {from_uid} -> {to_uid}") } - // The exec'd path, plus the raw `exe_anon_inode` kernel fact when set (JEF-317) + // The exec'd path, plus the raw `exe_anon_inode` kernel fact when set // — unlike the shell/package-manager CLASSIFICATION (a curated list, engine // policy in `engine::observe::exec_class`), this is a single kernel-computed // boolean the agent already resolved, so it rides the bare summary like @@ -269,7 +269,7 @@ impl Behavior { } } // Just the written path. Whether the write is *sensitive* (container drift / - // config tampering) is engine corroboration policy (JEF-306 F3), not a property + // config tampering) is engine corroboration policy (F3), not a property // of this shared wire type — the agent emits the path, the engine classifies. Behavior::FileWrite { path } => format!("wrote file {path}"), // A structural linkage fact, not an action. Named so the prompt/dashboard read @@ -283,7 +283,7 @@ impl Behavior { } } // No fields to render — the occurrence, attributed by the observation's - // workload, is the whole fact (JEF-318). + // workload, is the whole fact. Behavior::PtraceAttach => "ptrace attach (process injection primitive)".to_string(), Behavior::ModuleLoad => "loaded a kernel module".to_string(), } @@ -324,7 +324,7 @@ impl Behavior { // Coarsen to the basename so repeated execs of the same binary from different // absolute paths collapse to one stable key (mirrors how LibraryLoaded keys on // the lib name, not the full path) — keeps exec churn from busting the cache. - // `exe_anon_inode` is kept in the key (JEF-317): it is a genuinely different + // `exe_anon_inode` is kept in the key: it is a genuinely different // security-relevant fact about the SAME binary name (an on-disk `bash` vs. an // anonymous-inode exec that happens to report itself as "bash"), so folding it // in must not silently collapse the two into one cache entry. @@ -346,7 +346,7 @@ impl Behavior { // bool verbatim — the two states are genuinely distinct facts, and it's // low-cardinality by construction (exactly two values). Behavior::ImageLinkage { static_linkage } => format!("linkage:{static_linkage}"), - // No varying fields, so a fixed token is already maximally coarse (JEF-318) — + // No varying fields, so a fixed token is already maximally coarse — // mirrors how a fieldless fact would key regardless of source. Behavior::PtraceAttach => "ptrace-attach".to_string(), Behavior::ModuleLoad => "module-load".to_string(), @@ -355,7 +355,7 @@ impl Behavior { } /// How a sensor **attributed** an observation to a workload — a type distinction, not an -/// empty-string convention (JEF-59). A sensor either knows the pod's cgroup UID (the +/// empty-string convention. A sensor either knows the pod's cgroup UID (the /// first-party eBPF agent, which stays node-local and can't resolve names itself) or it /// already has the namespace/name (a sensor that reads k8s metadata). The engine resolves /// [`Self::ByPodUid`] → namespace/pod via its own pod watch (ADR-0014); the agent needs no @@ -433,7 +433,7 @@ pub struct RuntimeObservation { /// batch interval + a judging pass). Defaulted → adapter uses now(). #[serde(default, skip_serializing_if = "Option::is_none")] pub observed_at_ms: Option, - /// The Kubernetes NODE the sensor observed this on (JEF-308) — the eBPF agent reports its + /// The Kubernetes NODE the sensor observed this on — the eBPF agent reports its /// own node (from the downward API, `spec.nodeName`), so the engine can reason about /// runtime-corroboration coverage PER NODE ("blind on node X"), not just fleet-aggregate. /// Defaulted (older agents, or a node-agnostic sensor, omit it) — an absent node is @@ -444,7 +444,7 @@ pub struct RuntimeObservation { pub behavior: Behavior, } -/// A per-node **agent-liveness beacon** (JEF-308): the eBPF agent's own self-report, one per +/// A per-node **agent-liveness beacon**: the eBPF agent's own self-report, one per /// report window, distinct from a workload [`RuntimeObservation`]. It is what makes /// runtime-corroboration coverage honestly derivable per node: liveness is **signal-flow**, not /// pod-Ready — a Ready agent whose eBPF probes failed to attach is still BLIND (a Ready-but-blind @@ -467,7 +467,7 @@ pub struct AgentReport { /// build with no collection (the default no-eBPF image), which is also honestly blind. pub probes_total: u32, /// Signals the agent emitted this window. `0` is HEALTHY-quiet when probes are loaded — a - /// quiet node is not a down sensor (the JEF-308 quiet≠blind invariant). + /// quiet node is not a down sensor (the quiet≠blind invariant). pub signals_emitted: u64, /// When the window closed, as Unix epoch millis. Defaulted → the engine stamps ingest time. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -490,10 +490,10 @@ impl AgentReport { } } -/// A per-window **runtime report** (JEF-336): the single envelope every sensor POSTs to the +/// A per-window **runtime report**: the single envelope every sensor POSTs to the /// engine's unified runtime ingest (`/behavior`). It carries the window's normalized /// [`RuntimeObservation`]s AND — for a sensor that has one — its per-node liveness -/// [`AgentReport`], so liveness ALWAYS travels with the report. That is what keeps the JEF-308 +/// [`AgentReport`], so liveness ALWAYS travels with the report. That is what keeps the /// "quiet ≠ blind" guarantee honest: a node that saw nothing still POSTs an envelope with empty /// `observations` and its `liveness` present, so the engine records it HEALTHY-quiet instead of /// reading it blind for want of a beacon. @@ -507,7 +507,7 @@ pub struct RuntimeReport { /// The normalized observations seen this window — possibly empty (a quiet node still reports). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub observations: Vec, - /// This sensor's per-node liveness beacon (JEF-308), when it has one. Absent for a + /// This sensor's per-node liveness beacon, when it has one. Absent for a /// node-agnostic third-party sensor with no agent-specific liveness to report. #[serde(default, skip_serializing_if = "Option::is_none")] pub liveness: Option, diff --git a/behavior/src/tests.rs b/behavior/src/tests.rs index b7449332..f560080b 100644 --- a/behavior/src/tests.rs +++ b/behavior/src/tests.rs @@ -1,5 +1,5 @@ //! Unit tests for the behavioral wire contract. Moved out of `lib.rs`'s -//! `#[cfg(test)] mod tests` block into its own file (JEF-320) per the repo's 1,000-line +//! `#[cfg(test)] mod tests` block into its own file per the repo's 1,000-line //! file cap — `lib.rs` was approaching it. `use super::*` resolves to `lib.rs`, exactly //! as the inline `mod tests` block did. No test content changed by the move. @@ -99,7 +99,7 @@ fn secret_read_source_distinguishes_mounted_from_api() { #[test] fn secret_read_source_distinguishes_host_path_from_mounted_and_api() { - // An on-host credential read (JEF-320) serializes its source explicitly, just like + // An on-host credential read serializes its source explicitly, just like // `Api`, and round-trips — it is a genuinely distinct runtime fact from a k8s // Secret-mount read even though both reach the same `Behavior::SecretRead` shape. let host = Behavior::SecretRead { @@ -163,7 +163,7 @@ fn process_exec_fingerprint_coarsens_to_basename() { assert_eq!(a.fingerprint_key(), "exec:bash"); assert_eq!(a.fingerprint_key(), b.fingerprint_key()); // The wire type's summary is the bare path; *classification* of a notable exec - // (shell / package manager) is engine policy (engine::observe::exec_class, JEF-113), + // (shell / package manager) is engine policy (engine::observe::exec_class), // so it's not annotated here. assert_eq!(a.summary(), "executed /usr/bin/bash"); } @@ -171,7 +171,7 @@ fn process_exec_fingerprint_coarsens_to_basename() { #[test] fn process_exec_summary_is_the_bare_path() { // The shared wire type emits only the path — engine policy decides if it's notable - // (a shell / package manager) and annotates the prompt/output line (JEF-113). + // (a shell / package manager) and annotates the prompt/output line. let shell = Behavior::ProcessExec { path: "/bin/bash".into(), exe_anon_inode: false, @@ -189,7 +189,7 @@ fn process_exec_summary_is_the_bare_path() { #[test] fn exe_anon_inode_is_a_raw_fact_distinct_from_path_shape_classification() { - // JEF-317 (Route A): `exe_anon_inode` is a kernel-observed inode fact, independent + // (Route A): `exe_anon_inode` is a kernel-observed inode fact, independent // of the path — a `/bin/bash`-looking exec can still be anon-inode-backed (the // path is whatever `bprm->filename` resolved to; the flag is a separate read). let anon = Behavior::ProcessExec { @@ -219,7 +219,7 @@ fn exe_anon_inode_is_a_raw_fact_distinct_from_path_shape_classification() { #[test] fn exe_anon_inode_serializes_only_when_true() { - // JEF-317: the common (non-anonymous) exec omits the field entirely, keeping the + // the common (non-anonymous) exec omits the field entirely, keeping the // JSON byte-identical to before this field existed (mirrors SecretReadSource's // `Mounted`-is-omitted convention). A `true` flag serializes explicitly and both // round-trip; an older sensor's JSON with the field absent defaults to `false`. @@ -254,11 +254,11 @@ fn exe_anon_inode_serializes_only_when_true() { #[test] fn ptrace_attach_and_module_load_are_fieldless_facts() { - // JEF-318: both new variants carry no fields at all — the occurrence, attributed by + // both new variants carry no fields at all — the occurrence, attributed by // RuntimeObservation::attribution, IS the whole fact. Serde round-trips to a bare // `{"kind": "..."}`, summary/fingerprint are fixed strings, and neither is a wire-type // Alert (only Behavior::Alert corroborates from this crate's own view — the foothold - // scoping is engine policy, JEF-113). + // scoping is engine policy). let ptrace = Behavior::PtraceAttach; let v = serde_json::to_value(&ptrace).unwrap(); assert_eq!(v, serde_json::json!({"kind": "ptrace_attach"})); @@ -288,7 +288,7 @@ fn ptrace_attach_and_module_load_are_fieldless_facts() { #[test] fn ptrace_attach_observation_round_trips_over_the_wire() { // The full RuntimeObservation the agent POSTs for a ptrace attach — attributed by pod - // UID (the eBPF agent's path), source + node stamped — round-trips (JEF-318). + // UID (the eBPF agent's path), source + node stamped — round-trips. let obs = RuntimeObservation { attribution: Attribution::by_pod_uid("uid"), source: Some("protector-agent".into()), @@ -364,7 +364,7 @@ fn variant_label_is_a_stable_low_cardinality_token() { fn file_write_fingerprint_coarsens_to_the_dirname() { // Per-file write churn within a directory must collapse to one stable key so a // burst of writes (drop-and-execute, a config dir rewritten file-by-file) doesn't - // bust the verdict cache — the write signal is high-frequency (JEF-306). + // bust the verdict cache — the write signal is high-frequency. let a = Behavior::FileWrite { path: "/etc/cron.d/dropper".into(), }; @@ -393,7 +393,7 @@ fn file_write_fingerprint_coarsens_to_the_dirname() { #[test] fn file_write_summary_is_the_bare_path_and_never_corroborates() { // The shared wire type emits only the path — whether the write is *sensitive* - // (container drift / config tampering) is engine corroboration policy (JEF-306 F3), + // (container drift / config tampering) is engine corroboration policy (F3), // so it's pure data here and, like other mundane behaviors, never an alert. let w = Behavior::FileWrite { path: "/etc/ssh/sshd_config".into(), @@ -404,7 +404,7 @@ fn file_write_summary_is_the_bare_path_and_never_corroborates() { #[test] fn file_write_serializes_to_the_kind_tagged_contract() { - // Pure-data wire shape: `{"kind":"file_write","path":"..."}`, round-trips (JEF-306). + // Pure-data wire shape: `{"kind":"file_write","path":"..."}`, round-trips. let w = Behavior::FileWrite { path: "/etc/cron.d/x".into(), }; @@ -418,7 +418,7 @@ fn file_write_serializes_to_the_kind_tagged_contract() { #[test] fn observation_carries_the_node_and_omits_it_when_absent() { - // JEF-308: the agent stamps its node so coverage is derivable PER NODE. When present it + // the agent stamps its node so coverage is derivable PER NODE. When present it // rides the wire; when absent (a node-agnostic sensor, older agents) it is omitted — never guessed. let with_node = RuntimeObservation { attribution: Attribution::by_pod_uid("uid"), @@ -514,7 +514,7 @@ fn agent_report_observed_at_ms_is_omitted_when_absent() { #[test] fn runtime_report_round_trips_with_observations_and_liveness() { - // JEF-336: the unified envelope carries the window's observations AND the per-node + // the unified envelope carries the window's observations AND the per-node // liveness beacon in one shape, and round-trips byte-for-byte. let report = RuntimeReport { observations: vec![RuntimeObservation { @@ -589,7 +589,7 @@ fn runtime_report_omits_empty_observations_and_absent_liveness() { #[test] fn image_linkage_serializes_to_the_kind_tagged_contract_and_round_trips() { - // JEF-407: the linkage signal rides the same `{"kind": "...", ...}` behavioral wire. + // the linkage signal rides the same `{"kind": "...", ...}` behavioral wire. // A static-linkage report and a dynamic one both round-trip byte-for-byte. let stat = Behavior::ImageLinkage { static_linkage: true, diff --git a/charts/protector/README.md b/charts/protector/README.md index d388b394..d217def0 100644 --- a/charts/protector/README.md +++ b/charts/protector/README.md @@ -178,7 +178,7 @@ feed and the FIRST.org EPSS feed into a shared `emptyDir`, then re-fetches on automatically, no further configuration. The engine degrades gracefully if either file is missing or empty (the first-boot race before the sidecar's first fetch). -**Two feeds, not the retired advisory feed.** The NVD advisory feed was retired (JEF-242): +**Two feeds, not the retired advisory feed.** The NVD advisory feed was retired: it was redundant with Trivy's CVE metadata (Trivy already supplies `title`, `severity`, `fixedVersion`, and the CVSS `score` per vulnerability), the only net-new field (`cwe[]`) was one trivy-operator omits anyway, and the NVD "recent" feed had a poor hit-rate against the @@ -194,8 +194,8 @@ retained — no untrusted free-text from the feed reaches the model prompt. **Why a sidecar, not a ConfigMap?** Raw CISA KEV JSON is ~1.5 MiB — over Kubernetes' 1 MiB ConfigMap limit (the retired CronJob path had to lossily strip it to CVE IDs); the EPSS feed is similarly large. An `emptyDir` has no size limit, so the sidecar fetches and -the engine reads both feeds in **full**. This supersedes the JEF-228 CronJob and the -cancelled JEF-110 engine-fetch (see ADR-0015). +the engine reads both feeds in **full**. This supersedes the CronJob and the +cancelled engine-fetch (see ADR-0015). Override the cadence with `feedSync.interval` (e.g. `12h`), the sources with `feedSync.kevUrl` / `feedSync.epssUrl`, and the curl image with `feedSync.image.*`. @@ -291,7 +291,7 @@ Requires the `protector-agent` image and probes load-tested on your kernel (see | `feedSync.epssUrl` | FIRST.org EPSS scores CSV (gzipped) | EPSS source (gzipped CSV, gunzipped in place). See feeds section. | | `feedSync.interval` | `"12h"` | Re-fetch interval for the sidecar (a `sleep` arg, e.g. `6h`, `30m`). | | `webhook.enforcedFailurePolicy` | `Fail` | The fail-closed enforcing webhook's policy (its scope is derived from `enforceScope`). | -| `resources` | 10m/64Mi → 500m/256Mi | RAM-tight, arm64-friendly; the CPU limit has headroom for signing-sweep bursts (JEF-560). | +| `resources` | 10m/64Mi → 500m/256Mi | RAM-tight, arm64-friendly; the CPU limit has headroom for signing-sweep bursts. | See [`values.yaml`](values.yaml) for the fully commented set. diff --git a/charts/protector/templates/_helpers.tpl b/charts/protector/templates/_helpers.tpl index 286c90a7..020a90eb 100644 --- a/charts/protector/templates/_helpers.tpl +++ b/charts/protector/templates/_helpers.tpl @@ -67,12 +67,12 @@ app.kubernetes.io/part-of: {{ include "protector.name" . }} {{/* Mount path of the shared emptyDir the feed-fetcher sidecar writes and the engine reads -(JEF-238). The native-sidecar initContainer fetches the FULL public CISA KEV catalogue +. The native-sidecar initContainer fetches the FULL public CISA KEV catalogue into a file on a shared emptyDir under this dir; the engine container mounts the SAME emptyDir read-only and points PROTECTOR_KEV_FILE at the file. The engine itself stays zero-egress (ADR-0015): only the co-located sidecar egresses, and only to one public, read-only feed — it never reads or transmits any cluster data. KEV is the only feed (the -NVD advisory feed was retired in JEF-242). +NVD advisory feed was retired). */}} {{- define "protector.feedsDir" -}} {{- "/var/lib/protector/feeds" -}} diff --git a/charts/protector/templates/agent-daemonset.yaml b/charts/protector/templates/agent-daemonset.yaml index 88c60e67..46fa268e 100644 --- a/charts/protector/templates/agent-daemonset.yaml +++ b/charts/protector/templates/agent-daemonset.yaml @@ -84,9 +84,9 @@ spec: {{- toYaml .Values.agent.capabilities | nindent 16 }} {{- end }} env: - # The node this agent runs on (JEF-308), via the downward API. The agent stamps it + # The node this agent runs on, via the downward API. The agent stamps it # onto every observation AND onto the per-node liveness beacon it folds into the same - # /behavior report envelope (JEF-336), so + # /behavior report envelope, so # the engine can report runtime-corroboration coverage PER NODE ("blind on node X") # rather than fleet-aggregate — and a Ready-but-blind agent (probes failed to load) # still reads blind. Signal-flow liveness, not pod-Ready. diff --git a/charts/protector/templates/clusterrole.yaml b/charts/protector/templates/clusterrole.yaml index 8a310e8f..8d3e89c0 100644 --- a/charts/protector/templates/clusterrole.yaml +++ b/charts/protector/templates/clusterrole.yaml @@ -33,9 +33,9 @@ rules: verbs: ["get", "list", "watch"] # trivy-operator reports — the in-cluster, zero-egress scanner signal (ADR-0003). # Read-only; all harmless if the CRDs are absent — the engine degrades to no data for - # whichever report kind is missing. vulnerabilityreports is the CVE source (JEF-242); + # whichever report kind is missing. vulnerabilityreports is the CVE source; # exposedsecretreports / configauditreports / rbacassessmentreports are the - # breach/exposure/misconfiguration signals (JEF-244). + # breach/exposure/misconfiguration signals. - apiGroups: ["aquasecurity.github.io"] resources: - "vulnerabilityreports" @@ -59,7 +59,7 @@ rules: # `enforceScope` (ActuationScope::in_scope). When the scope is purely NAMESPACE-based # (no pod labels) the create/delete/patch grant is rendered instead as a per-namespace # Role + RoleBinding in each enforced namespace (see role.yaml) — least privilege, RBAC - # matching the actuation reach — and is NOT granted cluster-wide here (JEF-277). + # matching the actuation reach — and is NOT granted cluster-wide here. # # The cluster-wide NetworkPolicy write grant below is kept ONLY when the scope includes # POD LABELS. A label-matched Pod can live in ANY namespace diff --git a/charts/protector/templates/deployment.yaml b/charts/protector/templates/deployment.yaml index cb4e1e28..a51cf1a3 100644 --- a/charts/protector/templates/deployment.yaml +++ b/charts/protector/templates/deployment.yaml @@ -13,18 +13,18 @@ {{- fail (printf "enforceRung must be 'edge-cut' or 'quarantine', got %q" .Values.enforceRung) }} {{- end }} {{- /* -Feed-fetcher sidecar wiring (JEF-238, JEF-243). When feedSync is ON (the default), a native +Feed-fetcher sidecar wiring. When feedSync is ON (the default), a native sidecar (an initContainer with restartPolicy: Always) fetches TWO public read-only feeds into a shared emptyDir, and the engine reads those files from the SAME volume — no ConfigMap, no CronJob (those hit the 1 MiB ConfigMap limit; raw CISA KEV is ~1.5 MiB): * the FULL public CISA KEV catalogue (kev.json) — known-exploited-NOW intel; and * the FIRST.org EPSS scores (epss.csv, fetched gzipped and gunzipped in place) — the - PREDICTIVE exploitation probability per CVE (JEF-243). + PREDICTIVE exploitation probability per CVE. The emptyDir has no size limit, so both feeds are fetched in FULL. The engine itself stays zero-egress (ADR-0015): only the sidecar egresses, to public read-only feeds. With feedSync OFF, no sidecar and no shared volume render, and the feed env is dropped — the air-gapped / manual-mount path (mount your own kev.json / epss.csv at the same paths if you -want enrichment offline). The NVD advisory feed was retired (JEF-242, redundant with Trivy's +want enrichment offline). The NVD advisory feed was retired (redundant with Trivy's CVE metadata); KEV + EPSS are the two distinct exploitation axes alongside Trivy's CVSS. */}} {{- $feeds := and .Values.engine.enabled .Values.feedSync.enabled }} @@ -68,7 +68,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- if $feeds }} - # --- feed-fetcher sidecar (JEF-238) ------------------------------------------------- + # --- feed-fetcher sidecar ------------------------------------------------- # A NATIVE SIDECAR: an initContainer with restartPolicy: Always (k8s 1.29+ / the # cluster's k3s 1.36). Because it is a native sidecar it (a) starts and is given a # head start BEFORE the engine container, so the feed files usually exist by the time @@ -77,7 +77,7 @@ spec: # EGRESS BOUNDARY. This is the ONLY container the chart gives network egress to. It # GETs ONE public, READ-ONLY feed URL (feedSync.kevUrl) and writes it to the shared # `feeds` emptyDir. It makes NO apiserver call (unlike the - # retired JEF-228 CronJob — no RBAC, no ServiceAccount grant) and never reads or + # retired CronJob — no RBAC, no ServiceAccount grant) and never reads or # transmits any cluster data. The engine stays ZERO-EGRESS (ADR-0015): it only reads # the resulting files. A curl-only image is used because bitnami/kubectl was pulled # from Docker Hub (cluster fork) and no apiserver client is needed here anymore. @@ -125,9 +125,9 @@ spec: # retries next tick. # # Two feeds, both fetched in FULL (no ConfigMap size limit): KEV (plain JSON, - # known-exploited-NOW) and EPSS (gzipped CSV, the PREDICTIVE per-CVE probability — - # JEF-243). NO `jq` / NO transform / NO extra image — `gunzip` is busybox-builtin in - # the curl image. The NVD advisory feed was retired (JEF-242, redundant with Trivy's + # known-exploited-NOW) and EPSS (gzipped CSV, the PREDICTIVE per-CVE probability). + # NO `jq` / NO transform / NO extra image — `gunzip` is busybox-builtin in + # the curl image. The NVD advisory feed was retired (redundant with Trivy's # CVE metadata). The engine never fetches anything itself (zero egress, ADR-0015); # it only reads these files. - | @@ -222,7 +222,7 @@ spec: - name: PROTECTOR_TLS_KEY value: /etc/protector/tls/tls.key # sigstore TUF cache — must be writable under readOnlyRootFilesystem, so it points - # into the dedicated `tuf-cache` emptyDir below. Kept OFF /tmp (JEF-377): the engine + # into the dedicated `tuf-cache` emptyDir below. Kept OFF /tmp: the engine # pins $TMPDIR to this dir at startup so the tough TUF client's atomic temp writes # (latest_known_time.json + refreshed metadata) land here — a stable, attributable, # protector-owned path — instead of a `/tmp/.tmp/` dir that both churns the @@ -316,7 +316,7 @@ spec: value: {{ .Values.engine.dashboard.clusterLabel | quote }} {{- with .Values.engine.dashboard.oidc }} {{- if .issuer }} - # App-level OIDC verification (ADR-0030 / JEF-487) — fail-closed when the issuer is set. + # App-level OIDC verification (ADR-0030) — fail-closed when the issuer is set. # Namespaced PROTECTOR_DASHBOARD_OIDC_* so it never collides with the cosign # signature-verification PROTECTOR_OIDC_ISSUER (a different, cert-identity concern). # Absent issuer = edge-trust-only (the engine logs a loud startup warning, §6). @@ -327,7 +327,7 @@ spec: - name: PROTECTOR_DASHBOARD_OIDC_TIER_CLAIM value: {{ .tierClaim | quote }} {{- if .tierGrants }} - # Identity→tier grants (JEF-501): resolves the forensic/raw ceiling from the VERIFIED + # Identity→tier grants: resolves the forensic/raw ceiling from the VERIFIED # sub/email when the IdP mints no `tier` claim (e.g. Cloudflare Access over GitHub). # An explicit `tier` claim, when present, still takes precedence over a grant. - name: PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS @@ -337,14 +337,14 @@ spec: {{- end }} {{- end }} {{- if $feeds }} - # KEV catalogue file the feed-fetcher sidecar writes into the shared emptyDir - # (JEF-238). FULL CISA KEV JSON — KevCatalog parses the canonical + # KEV catalogue file the feed-fetcher sidecar writes into the shared emptyDir. + # FULL CISA KEV JSON — KevCatalog parses the canonical # {"vulnerabilities":[…]} shape. The engine never fetches KEV itself (zero # egress); it only reads this file, and degrades to "nothing known-exploited" # if it is missing/empty (the first-boot race before the sidecar's first fetch). - name: PROTECTOR_KEV_FILE value: {{ printf "%s/kev.json" $feedsDir | quote }} - # EPSS scores file the same sidecar writes into the shared emptyDir (JEF-243). + # EPSS scores file the same sidecar writes into the shared emptyDir. # The FIRST.org CSV (gunzipped in place) — EpssStore parses `cve,epss,percentile` # rows. The engine never fetches EPSS itself (zero egress); it only reads this # file, and degrades to "no exploit-prediction evidence" if it is missing/empty @@ -367,11 +367,11 @@ spec: value: {{ $.Values.engine.notify.verbose | quote }} {{- end }} {{- if .Values.engine.journal.enabled }} - # Persistent decision journal (JEF-141) — survives restarts; the path is on + # Persistent decision journal — survives restarts; the path is on # the journal PVC mounted below. Unset = in-memory only (graceful fallback). - name: PROTECTOR_ENGINE_JOURNAL_PATH value: {{ .Values.engine.journal.path | quote }} - # Durable forensic/raw MCP disclosure audit (JEF-490, ADR-0031 §4) — a DISTINCT + # Durable forensic/raw MCP disclosure audit (ADR-0031 §4) — a DISTINCT # append-only file on the SAME journal PVC (never overloaded onto the decision # journal). The "Access" tab reads it; durable-on-PVC so an empty log honestly reads # "nobody pulled" rather than "resets on restart". Unset = in-memory only (graceful). @@ -385,7 +385,7 @@ spec: readOnly: true - name: tmp mountPath: /tmp - # Writable sigstore TUF cache, kept OFF /tmp (JEF-377) so the tough client's atomic + # Writable sigstore TUF cache, kept OFF /tmp so the tough client's atomic # temp writes are attributable to protector, not IOC-shaped /tmp/.tmp/ paths. - name: tuf-cache mountPath: /var/lib/protector/tuf @@ -399,7 +399,7 @@ spec: mountPath: {{ dir .Values.engine.journal.path | quote }} {{- end }} {{- if $feeds }} - # The shared feeds emptyDir the sidecar writes (JEF-238, JEF-243), mounted + # The shared feeds emptyDir the sidecar writes, mounted # READ-ONLY in the engine — the engine only reads kev.json / epss.csv, never writes. - name: feeds mountPath: {{ $feedsDir | quote }} @@ -417,8 +417,8 @@ spec: - name: tmp emptyDir: {} # Dedicated writable scratch for the sigstore TUF cache, deliberately separate from the - # /tmp emptyDir so protector's own TUF metadata writes never look like a /tmp drop IOC - # (JEF-377). Ephemeral per pod — tough deletes its datastore after each load, and the + # /tmp emptyDir so protector's own TUF metadata writes never look like a /tmp drop IOC. + # Ephemeral per pod — tough deletes its datastore after each load, and the # trusted_root.json checkout is cheap to re-fetch. - name: tuf-cache emptyDir: {} @@ -438,7 +438,7 @@ spec: {{- end }} {{- if $feeds }} # Shared scratch volume between the feed-fetcher sidecar (writer) and the engine - # (reader) — JEF-238, JEF-243. emptyDir has no 1 MiB ConfigMap limit, so the FULL CISA + # (reader). emptyDir has no 1 MiB ConfigMap limit, so the FULL CISA # KEV JSON (~1.5 MiB) and the FIRST.org EPSS CSV both fit. Lives for the pod's lifetime; # the sidecar re-fetches on its interval and on restart. - name: feeds diff --git a/charts/protector/templates/journal-pvc.yaml b/charts/protector/templates/journal-pvc.yaml index 67142cc0..50612313 100644 --- a/charts/protector/templates/journal-pvc.yaml +++ b/charts/protector/templates/journal-pvc.yaml @@ -1,5 +1,5 @@ {{- if and .Values.engine.enabled .Values.engine.journal.enabled }} -# Persistent decision journal (JEF-141): survives engine restarts/reschedules so the +# Persistent decision journal: survives engine restarts/reschedules so the # engine's output state repopulates immediately and the operator keeps a durable record of # what protector decided. ReadWriteOnce — the engine is a singleton (replicaCount 1), and the # Deployment uses the Recreate strategy so a rollout never contends for the volume. diff --git a/charts/protector/templates/role.yaml b/charts/protector/templates/role.yaml index 592e2062..a53ab831 100644 --- a/charts/protector/templates/role.yaml +++ b/charts/protector/templates/role.yaml @@ -3,7 +3,7 @@ {{- $nsScoped := gt (len .Values.enforceScope.namespaces) 0 }} {{- $labelScoped := gt (len (keys .Values.enforceScope.labels)) 0 }} {{- /* - JEF-277 — least-privilege NetworkPolicy write grant. + Least-privilege NetworkPolicy write grant. NetworkPolicy is a NAMESPACED resource, and under `mode: enforce` the engine only writes its reversible cuts inside `enforceScope` (ActuationScope::in_scope: every cut @@ -31,7 +31,7 @@ metadata: rules: # The network actuator creates/deletes its own (managed-by: protector) additive, # reversible NetworkPolicies to quarantine/sever a corroborated attack path in this - # enforced namespace (ADR-0010). Scoped to exactly this namespace (JEF-277). + # enforced namespace (ADR-0010). Scoped to exactly this namespace. - apiGroups: ["networking.k8s.io"] resources: ["networkpolicies"] verbs: ["create", "delete", "patch"] diff --git a/charts/protector/values.yaml b/charts/protector/values.yaml index 2717a31e..910348c6 100644 --- a/charts/protector/values.yaml +++ b/charts/protector/values.yaml @@ -23,12 +23,12 @@ # EGRESS POSTURE — read this. The ENGINE itself stays ZERO-EGRESS (ADR-0015): the # security graph and evidence never leave the cluster, and it makes no breach-notify # call, no OTLP export, and no live feed fetch (it only reads mounted files). -# As of JEF-238/JEF-243 the chart ships ONE component with network egress, ON BY DEFAULT: the +# The chart ships ONE component with network egress, ON BY DEFAULT: the # feed-fetcher SIDECAR on the engine pod (feedSync.enabled: true). It is the SOLE # component the chart grants egress to. It makes outbound GETs to PUBLIC, READ-ONLY feeds # (the CISA KEV catalogue and the FIRST.org EPSS scores) and writes the results into a shared # emptyDir the engine reads. It NEVER reads cluster state and NEVER transmits any cluster data -# outward — only the plain feed downloads leave. (This supersedes the JEF-228 feed-sync CronJob: +# outward — only the plain feed downloads leave. (This supersedes the feed-sync CronJob: # raw CISA KEV is ~1.5 MiB, over the 1 MiB ConfigMap limit; an emptyDir holds the FULL data.) # # * ENGINE ZERO-EGRESS preserved engine never calls out (feeds are read from a volume) @@ -133,7 +133,7 @@ service: # RAM-tight, arm64-friendly: a webhook that mostly idles with occasional signature # verification (network I/O to the registry + Rekor) plus the async engine loop. -# The 250m limit (JEF-560) proved too tight: a per-pass signing/provenance sweep across a +# The 250m limit proved too tight: a per-pass signing/provenance sweep across a # fleet's worth of images does a burst of TLS handshakes + JSON/crypto work on the SAME cgroup # the probe HTTP server shares, and the CFS quota can throttle the whole container — including # the probe response — long enough to look dead. 500m gives that burst headroom without @@ -148,7 +148,7 @@ resources: # Probes hit the HTTPS server; kubelet doesn't verify the cert for probes. `/healthz` and # `/readyz` are both served from process start (no engine-sweep dependency), so a probe -# failure here means the process is either genuinely wedged or — the JEF-560 incident — was +# failure here means the process is either genuinely wedged or — the incident — was # merely CPU-throttled long enough to miss the default 1s timeout / 3-strike (30s) budget # during a legitimate sweep burst. Widened timeout + failure budget so a busy-but-alive engine # isn't mistaken for a dead one and SIGTERM'd into a restart loop; liveness (which restarts the @@ -335,7 +335,7 @@ engine: port: 8080 # The cluster label shown in the dashboard's status strip. clusterLabel: cluster - # App-level OIDC verification (ADR-0030 / JEF-487) — closes the port-forward hole. Protector + # App-level OIDC verification (ADR-0030) — closes the port-forward hole. Protector # is a RESOURCE SERVER: it verifies the JWT the edge (or any conformant OIDC IdP) already # issues; it runs no login flow of its own. This is DISTINCT from `signature.oidcIssuer` (the # cosign keyless CERT-IDENTITY issuer) — a different concern, its own env namespace @@ -356,10 +356,10 @@ engine: # Cloudflare Access application AUD tag. audience: "" # The claim path the operator's authorization tier is read from (governs how much of the - # already-read-only view an identity sees; JEF-489). Default `tier`; supports a flat namespaced + # already-read-only view an identity sees). Default `tier`; supports a flat namespaced # key or a dotted path (e.g. `authz.tier`). tierClaim: "tier" - # Operator identity→tier grants (JEF-501): resolves the forensic/raw ceiling from the + # Operator identity→tier grants: resolves the forensic/raw ceiling from the # VERIFIED token identity (`sub`/`email`) when the IdP mints no `tier` claim at all — e.g. # Cloudflare Access relaying GitHub, which emits neither. Format `tier=id1,id2;tier=id3`, # e.g. `raw=alice@example.com;forensic=bob@example.com`. An explicit `tier` claim (when the @@ -371,12 +371,12 @@ engine: tierGrants: "" # Two exploitation-intel feeds drive the engine's exploit intel + model reasoning # (ADR-0015): the CISA KEV catalogue (actively-exploited CVEs, known NOW) and the FIRST.org - # EPSS scores (the PREDICTIVE per-CVE exploitation probability — JEF-243). The engine NEVER + # EPSS scores (the PREDICTIVE per-CVE exploitation probability). The engine NEVER # fetches either over the network (zero egress) — it only READS files from a shared # emptyDir. By default the feed-fetcher SIDECAR (feedSync below, ON by default) fetches both # in FULL into that volume and the engine auto-reads them at /var/lib/protector/feeds/kev.json # (PROTECTOR_KEV_FILE) and /var/lib/protector/feeds/epss.csv (PROTECTOR_EPSS_FILE), both set - # automatically. No further config. (The NVD advisory feed was retired in JEF-242 — redundant + # automatically. No further config. (The NVD advisory feed was retired — redundant # with Trivy; KEV + EPSS are the two distinct exploitation axes alongside Trivy's CVSS.) # # AIR-GAPPED: set feedSync.enabled=false (no sidecar, no egress) and, to keep enrichment @@ -399,7 +399,7 @@ engine: notify: url: "" verbose: false - # Decision journal (JEF-141): an append-only, size-rotated record of every breach + # Decision journal: an append-only, size-rotated record of every breach # decision + mitigation apply/revert, on a PERSISTENT volume so it survives a restart. # Stays in-cluster (zero egress). Disable to fall back to in-memory only (degrades # gracefully — the engine never crashes on a missing path). @@ -408,7 +408,7 @@ engine: # File the engine appends to; it rotates to .1 in the same dir (~2 MiB total). # The dir is the PVC mount; must be writable (fsGroup 65532 owns it). path: /var/lib/protector/journal/decisions.jsonl - # Durable forensic/raw MCP disclosure audit (JEF-490, ADR-0031 §4): a DISTINCT append-only + # Durable forensic/raw MCP disclosure audit (ADR-0031 §4): a DISTINCT append-only # file on the SAME journal PVC/mount (never overloaded onto the decision journal above). It # backs the dashboard's "Access" tab; durable-on-PVC so an empty log honestly reads "nobody # pulled" rather than "resets on restart". Rotates to .1 like the decision journal. @@ -471,21 +471,21 @@ agent: tolerations: - operator: Exists -# --- feed-fetcher sidecar (JEF-238, JEF-243) --- +# --- feed-fetcher sidecar --- # ON BY DEFAULT (feedSync.enabled: true). A co-located feed-fetcher SIDECAR (a native # sidecar = an initContainer with restartPolicy: Always) on the engine pod fetches the # FULL public CISA KEV catalogue AND the FIRST.org EPSS scores into a shared emptyDir the # engine reads — so both exploitation-intel feeds are fresh out of the box with no operator # syncing them by hand and no further config. # -# TWO FEEDS, TWO EXPLOITATION AXES. The NVD advisory feed was retired (JEF-242): it was +# TWO FEEDS, TWO EXPLOITATION AXES. The NVD advisory feed was retired: it was # redundant with Trivy's CVE metadata (title ~ summary, fixedVersion ~ fix_ref) and had a # poor hit-rate against the base-image CVEs Trivy finds. What stays are the two SIGNALS Trivy # does NOT supply: KEV's `exploited_in_wild` (known-exploited NOW) and EPSS's per-CVE # probability (PREDICTIVE — likely-to-be-exploited next). Together with Trivy's CVSS (static # severity) they are the three distinct axes the breach model weighs (ADR-0016). # -# WHY A SIDECAR, NOT A CONFIGMAP (supersedes JEF-228). Raw CISA KEV JSON is ~1.5 MiB, +# WHY A SIDECAR, NOT A CONFIGMAP (supersedes the retired CronJob). Raw CISA KEV JSON is ~1.5 MiB, # over the 1 MiB ConfigMap limit (the CronJob path had to lossily strip it to CVE-IDs); the # EPSS feed is similarly large. An emptyDir has NO size limit, so the sidecar fetches both in # FULL (gunzipping EPSS in place) and the engine reads the complete files. @@ -509,7 +509,7 @@ feedSync: # size limit): # * kevUrl — the canonical CISA KEV catalogue (plain JSON, known-exploited NOW); and # * epssUrl — the FIRST.org EPSS scores (gzipped CSV, the PREDICTIVE per-CVE exploitation - # probability — JEF-243). The sidecar gunzips it in place (busybox gunzip). + # probability). The sidecar gunzips it in place (busybox gunzip). kevUrl: "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" epssUrl: "https://epss.cyentia.com/epss_scores-current.csv.gz" # Re-fetch interval (a `sleep` arg: e.g. "12h", "6h", "30m"). KEV changes at most daily and diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md index abfd1c81..13f20250 100644 --- a/docs/STYLEGUIDE.md +++ b/docs/STYLEGUIDE.md @@ -111,8 +111,8 @@ Two weights only (400/600). Emphasis via weight + ink value, not a third weight. | Reversion log | `--posture-cleared` toned (a self-revert is the system working) | | Signing inventory | `--sign-*` chip (glyph + word); `invalid` → `--posture-breach` keyline (loud), `not signed` calm; ref/signer single-line ellipsis (never `break-all`), full value in the `
` panel + `title=`; "if enforced" → `--cov-present` would-admit / `--posture-breach` would-block | | Empty states | `--ink-2`; posture-coloured only when honestly earned (model judging) | -| Auth-mode pill (JEF-489) | server-derived `auth-mode`: `OIDC` calm (`--mode-enforce` word, no glyph) vs `EDGE-ONLY` ⚠ warn (`--mode-shadow` ink + `--posture-awaiting-tint`, same warn register as the SHADOW pill) — word **+** glyph, never colour alone; a missing mode falls to the loud `EDGE-ONLY` | -| Auth interstitial `AuthGate` (JEF-489) | reuses `empty` / `empty-head` / `empty-sub muted`; `--posture-uncertain` head (attention, never breach-red, never green); `role="alert"`, focus to the heading (`tabindex=-1`) on transition; **401** → "your session expired" + a full-page `` re-auth link (never a fetch — CSP `connect-src 'self'`); **403** → "no access to this dashboard", NO re-auth control; mutually exclusive with the connection banner | +| Auth-mode pill | server-derived `auth-mode`: `OIDC` calm (`--mode-enforce` word, no glyph) vs `EDGE-ONLY` ⚠ warn (`--mode-shadow` ink + `--posture-awaiting-tint`, same warn register as the SHADOW pill) — word **+** glyph, never colour alone; a missing mode falls to the loud `EDGE-ONLY` | +| Auth interstitial `AuthGate` | reuses `empty` / `empty-head` / `empty-sub muted`; `--posture-uncertain` head (attention, never breach-red, never green); `role="alert"`, focus to the heading (`tabindex=-1`) on transition; **401** → "your session expired" + a full-page `` re-auth link (never a fetch — CSP `connect-src 'self'`); **403** → "no access to this dashboard", NO re-auth control; mutually exclusive with the connection banner | ## Accessibility gate (test-enforced) 1. **Contrast:** body/status text ≥ **4.5:1** on its surface; chips/rails/glyphs ≥ **3:1**. diff --git a/docs/adr/0001-async-mitigation-engine.md b/docs/adr/0001-async-mitigation-engine.md index fde7a2bd..5f1bccdf 100644 --- a/docs/adr/0001-async-mitigation-engine.md +++ b/docs/adr/0001-async-mitigation-engine.md @@ -1,9 +1,9 @@ # 0001. Async mitigation engine: propose / prove / respond, local-first -- Status: Accepted — narrowed by the JEF-363 amendment below (the model-backed +- Status: Accepted — narrowed by the amendment below (the model-backed *hypothesis / propose* half is removed; the engine runs purely on the deterministic proof enumerator). -- Date: 2026-06-11 (amended 2026-07-06: model-propose stage removed, JEF-363) +- Date: 2026-06-11 (amended 2026-07-06: model-propose stage removed) ## Context @@ -139,7 +139,7 @@ Harder / accepted downsides: privileged path, not by a proof of exploitation. This is a deliberate, named bound on the claim. -## Amendment (JEF-363): the model-backed *propose* half is removed — deterministic enumeration is exhaustive at this cluster's scale +## Amendment: the model-backed *propose* half is removed — deterministic enumeration is exhaustive at this cluster's scale The original decision above framed the engine as **propose → prove → respond**, with a model-backed *hypothesis engine* (Decision step 2) generating candidate chains that a @@ -175,13 +175,13 @@ layer: re-add an edge grade (a `proof`/`hypothesis` tag on `Edge`) and restore t `is_proof_grade` filter in the proof walk. Until then, carrying that machinery unused was dead cost, not insurance. -**Amendment (JEF-365): the `Grade::Hypothesis` seam is removed too.** The reversibility +**Amendment: the `Grade::Hypothesis` seam is removed too.** The reversibility note above originally kept the `Grade` enum and the proof-walk filter in place as a standing seam. Once the model-propose stage was gone, nothing constructed a hypothesis-grade edge — every edge in the graph is a deterministic observation by construction — so `Grade`, `Edge.grade`, `Edge::is_proof_grade`, and the proof-walk filter arms were all vestigial (a type guard against an edge that can no longer exist). -JEF-365 deletes them. This is safe precisely because nothing constructs a hypothesis-grade + deletes them. This is safe precisely because nothing constructs a hypothesis-grade edge; the proof walk now traverses all edges, which is behaviourally identical (every edge was already proof-grade). The seam remains reintroducible exactly as described above — re-add the grade and the filter if scale ever bites — but it is cheaper to reintroduce a small enum diff --git a/docs/adr/0003-capability-ports.md b/docs/adr/0003-capability-ports.md index 035df548..f9ca3f8d 100644 --- a/docs/adr/0003-capability-ports.md +++ b/docs/adr/0003-capability-ports.md @@ -79,7 +79,7 @@ swap-ins are why the port exists. return an opinion — it returns a checkable answer from a real graph/RBAC/feed query, eligible to move privilege. "Only deterministic proof moves privilege" thereby becomes enforceable at the boundary instead of by convention. (Originally - this was a **proof-grade / hypothesis-grade** tag on each edge; JEF-365 removed the + this was a **proof-grade / hypothesis-grade** tag on each edge; removed the tag — see the amendment below — because every edge is now a deterministic observation by construction, so the tag never had a second value to hold.) @@ -109,7 +109,7 @@ Easier: - Testable in isolation: fake adapters per port exercise the proof and response loops with no real stack. - Cross-source corroboration is first-class, enforced at the port boundary rather - than assumed. (The proof-grade/hypothesis-grade split was removed in JEF-365; see + than assumed. (The proof-grade/hypothesis-grade split was removed later; see the amendment below.) - The core's real value — graph, proof, response — is no longer hidden behind product names. @@ -129,19 +129,19 @@ Harder / accepted downsides: core; plugins only add *evidence*. Holding that line is a permanent review responsibility, not a one-time decision. -## Amendment (JEF-365): the edge-grade tag is removed +## Amendment: the edge-grade tag is removed The port contract above described each edge as carrying a **proof-grade / hypothesis-grade** tag (`Grade`), with the action layer accepting only proof-grade -links. After ADR-0001's JEF-363 amendment removed the model-propose stage, no adapter +links. After ADR-0001's amendment removed the model-propose stage, no adapter or graph builder ever constructed a hypothesis-grade edge — every port in this ADR is deterministic, so every edge it emits is a deterministic observation. The tag became a type-level guard against an edge that can no longer exist. -JEF-365 removes `Grade`, `Edge.grade`, and `Edge::is_proof_grade`. The edge contract is + removes `Grade`, `Edge.grade`, and `Edge::is_proof_grade`. The edge contract is now simpler and unconditional: **every edge is a deterministic observation by construction, eligible to move privilege.** "Only deterministic proof moves privilege" holds structurally — the graph contains nothing else. If an untrusted or heuristic provider is ever admitted (the case the tag anticipated), the seam is reintroducible by -re-adding the grade and restoring the proof-walk filter (see ADR-0001's JEF-365 +re-adding the grade and restoring the proof-walk filter (see ADR-0001's amendment); removing it now is safe because nothing constructs a hypothesis-grade edge. diff --git a/docs/adr/0004-graph-representation.md b/docs/adr/0004-graph-representation.md index dcb6e755..e8ac4004 100644 --- a/docs/adr/0004-graph-representation.md +++ b/docs/adr/0004-graph-representation.md @@ -9,7 +9,7 @@ infrastructure and decided it tracks *observed* state fed by watch streams. [ADR-0003](0003-capability-ports.md) fixed the graph **vocabulary** (typed nodes and edges, each edge carrying provenance; originally also a proof-grade/hypothesis-grade -tag, removed in JEF-365 — see that ADR's amendment) as the stable contract adapters map +tag, later removed — see that ADR's amendment) as the stable contract adapters map into. What neither settled is the concrete question: **what do we store the graph in, and does it persist?** @@ -47,7 +47,7 @@ itself.** Specifically: edge carries its **provenance** (which adapter asserted it) and is a **deterministic observation by construction** — no hypothesis-grade edges exist, so any edge is eligible to move privilege. (The original design tagged each edge proof-grade vs - hypothesis-grade; JEF-365 removed the tag once nothing constructed a hypothesis-grade + hypothesis-grade; removed the tag once nothing constructed a hypothesis-grade edge — see ADR-0003's amendment.) - Reachability and privilege as **explicit predicate-filtered walks**, not a query language. The counterfactual cut enumerates edges on a proven path; we do not diff --git a/docs/adr/0009-asymmetric-action-bar.md b/docs/adr/0009-asymmetric-action-bar.md index 5001a3a8..4d4b61ce 100644 --- a/docs/adr/0009-asymmetric-action-bar.md +++ b/docs/adr/0009-asymmetric-action-bar.md @@ -3,7 +3,7 @@ - Status: Accepted - Date: 2026-06-12 -> **Amendment (JEF-305, 2026-07-04):** this ADR describes the live-corroboration signal +> **Amendment (2026-07-04):** this ADR describes the live-corroboration signal > as "a live Falco signal" because Falco was the only sensor when it was written. That > `corroborated-now` predicate is now **tool-agnostic and per-objective** > ([ADR-0014](0014-behavioral-telemetry-ebpf.md)): any sensor (Falco, Tetragon, the @@ -84,7 +84,7 @@ Harder / accepted downsides: starting point; this is the considered refinement once the signals' asymmetry and the adjudicator were both in place. -## Amendment (JEF-547, 2026-07-27): the adjudicator becomes the cut *selector* ([ADR-0032](0032-model-is-incident-responder.md)) +## Amendment (2026-07-27): the adjudicator becomes the cut *selector* ([ADR-0032](0032-model-is-incident-responder.md)) The `corroborated ∧ adjudicated` auto-gate decided here is **unchanged and reaffirmed** — and ADR-0032 *restores* it where the code had drifted: the downstream `QuarantineWorkload` had @@ -97,5 +97,5 @@ deterministically-selected action; under ADR-0032 the model becomes the **select cut (from a deterministically-enumerated menu). The asymmetry (live evidence acts, latent exposure proposes) is unchanged, but "acts" now means *the model's cut decision*, and auto-action is confined to internet-facing adjudicated paths — an internal-only actively- -exploited pod is **propose-only** (per ADR-0032, superseding the JEF-284 "internal pods +exploited pod is **propose-only** (per ADR-0032, superseding the "internal pods included" stance in [ADR-0022](0022-quarantine-the-entry-is-the-default-containment.md)). diff --git a/docs/adr/0014-behavioral-telemetry-ebpf.md b/docs/adr/0014-behavioral-telemetry-ebpf.md index afaa0ccc..22f1cb32 100644 --- a/docs/adr/0014-behavioral-telemetry-ebpf.md +++ b/docs/adr/0014-behavioral-telemetry-ebpf.md @@ -93,10 +93,10 @@ telemetry without requiring any third-party sensor. corroborates only the objective class whose ATT&CK *tactic* it evidences: internet egress → EXFILTRATION (T1041), secret read → CREDENTIAL_ACCESS (T1552), vuln-library load → the INITIAL_ACCESS / EXPLOIT_PUBLIC_FACING foothold (T1190, matched against the - entry's foothold tactic per JEF-77 as well as the objective's). An *alerting* signal + entry's foothold tactic as well as the objective's). An *alerting* signal (`Behavior::Alert`) still corroborates **any** chain — "an attack is happening now" regardless of which objective — and a *notable* exec (interactive shell / package - manager, JEF-55/JEF-117) corroborates broadly the same way, as the agent-side + manager) corroborates broadly the same way, as the agent-side equivalent of Falco's shell/pkg-mgr criticals. A *bare* `ProcessExec` and mundane in-cluster connections remain model-evidence only, so the predicate never becomes the "everything corroborates everything" blanket. This is entirely **shadow-gated**: the @@ -170,7 +170,7 @@ Shadow-first, mirroring the engine's posture ([ADR-0001](0001-async-mitigation-e `mode: enforce` within `enforceScope` (ADR-0021), never by the mere presence of a signal. -## Addendum — retiring Falco: the corroboration-parity bar (JEF-305, 2026-07-04) +## Addendum — retiring Falco: the corroboration-parity bar (2026-07-04) Falco 0.44.1 crash-loops on the cluster's `7.0.0-1014-raspi` arm64 kernel (a libsinsp ABI mismatch against the syscall tracepoints it parses), leaving live corroboration down @@ -208,7 +208,7 @@ no behavior changes with it.** 4. **"Alarming-now → blanket corroboration" is an engine-side classifier policy.** The decision that an *alerting* signal corroborates any chain (and that a notable exec does - the same) is **classification policy that lives engine-side**, following the JEF-113 + the same) is **classification policy that lives engine-side**, following the pattern: the wire behavior type stays pure data, and the "is this alarming now?" judgement is made in the engine (as `observe::exec_class` already does for shell/pkg-mgr execs). A new sensor does not encode the blanket-corroboration policy on @@ -219,7 +219,7 @@ None of these four touch the honesty, zero-egress, or shadow-by-default framing: agent stays observe-only, the graph and evidence stay in-cluster, and corroboration only ever promotes a cut behind the existing reversible, self-reverting, `enforce`-gated bar. -## Addendum — Falco adapter retired; the first-party agent is the sole deployed corroboration source (JEF-312, 2026-07-05) +## Addendum — Falco adapter retired; the first-party agent is the sole deployed corroboration source (2026-07-05) The retirement is complete. Falco is dead on the cluster's `7.0.0` arm64 kernel (crash-loops, zero events forwarded), so there is no live Falco left to compare the agent diff --git a/docs/adr/0015-advisory-evidence-egress.md b/docs/adr/0015-advisory-evidence-egress.md index 2fb02181..cf165d7e 100644 --- a/docs/adr/0015-advisory-evidence-egress.md +++ b/docs/adr/0015-advisory-evidence-egress.md @@ -1,7 +1,7 @@ # 0015. Advisory evidence is mounted-snapshot-only (zero egress); injection-safe by construction -- Status: Superseded in part by the JEF-242 amendment below — the advisory feed is RETIRED; only the KEV feed remains. The zero-egress + injection-safety rules this ADR established still govern every mounted feed (KEV today). -- Date: 2026-06-22 (amended 2026-06-28: advisory feed retired, JEF-242) +- Status: Superseded in part by the amendment below — the advisory feed is RETIRED; only the KEV feed remains. The zero-egress + injection-safety rules this ADR established still govern every mounted feed (KEV today). +- Date: 2026-06-22 (amended 2026-06-28: advisory feed retired) - Relates to: [0013](0013-proof-winnows-model-decides.md) (the model is promote-capable, so its inputs are a security boundary), [0014](0014-behavioral-telemetry-ebpf.md) (same "in-cluster, no egress of cluster data" posture), [0016](0016-severity-vs-urgency.md) (KEV's `exploited_in_wild` is a distinct exploitation signal kept by this amendment), [0020](0020-signature-continuity.md) (the ADR-0020 amendment below carves out Rekor transparency-log reads as a sanctioned outbound lane for signature continuity) ## Context @@ -10,7 +10,7 @@ The model is the analyst that decides exploitability on a proven foothold ([ADR-0013](0013-proof-winnows-model-decides.md)). It would judge better with **advisory evidence** for a CVE — a CWE class, whether a fix exists, and a short summary — so it can reason "a fix exists but the workload is still on the vulnerable -version" vs "no fix at all" (the JEF-52 payoff). The question is *where that evidence +version" vs "no fix at all" (the payoff). The question is *where that evidence comes from* and *how it reaches the prompt safely*, given two hard constraints: 1. **Egress.** The platform's posture is in-cluster, local-first: the cluster graph @@ -22,7 +22,7 @@ comes from* and *how it reaches the prompt safely*, given two hard constraints: feeds is **promote-capable** (ADR-0013) — a successful prompt injection here could drive an auto-cut. `sanitize`/`fence` (strip fence/structure chars, wrap as data) is adequate for short structured tokens but weak for long free prose and impossible - for patch diffs (JEF-106). + for patch diffs. We need the evidence without the egress and without handing the promote-capable model an injection surface. @@ -38,7 +38,7 @@ rendered prompt is **byte-identical to today** — the feature is invisible unti snapshot is mounted. This is the same "no egress of cluster data" rule ADR-0014 holds for telemetry, applied to enrichment. -### 2. Opt-in live OSV fetch is DEFERRED, not built (JEF-110) +### 2. Opt-in live OSV fetch is DEFERRED, not built A future opt-in live fetch was considered and is explicitly **deferred**. It is not a default and is out of scope for this work; nothing in the codebase reaches the network @@ -48,11 +48,11 @@ own decision — never the default posture this ADR sets. ### 3. Fix-diffs are out of scope for the local model Patch text / fix diffs are **not** surfaced to the local promote-capable model. They -are unbounded free text that `sanitize` cannot make safe (JEF-106), and they buy +are unbounded free text that `sanitize` cannot make safe, and they buy little for the exploitability call. If diffs are ever used, it is in a human or frontier-model lane with a different trust model — never the local auto-promote path. -### 4. Structural extraction + hard caps for injection safety (JEF-106 folded in) +### 4. Structural extraction + hard caps for injection safety (folded in) Advisory text reaches the model as **structured, length-capped, fenced data**: @@ -63,7 +63,7 @@ Advisory text reaches the model as **structured, length-capped, fenced data**: the stored summary, the `fix_ref`, each CWE string, and the CWE count, so an oversized snapshot entry can never enter the system) and again at the prompt boundary (independent per-field caps in `cve_evidence` for the title, summary, and `fix_ref`), so the bound - holds regardless of how the advisory arrived — including a future live-OSV lane (JEF-110) + holds regardless of how the advisory arrived — including a future live-OSV lane that would bypass the parse-time cap. - **A per-entry AGGREGATE budget bounds the whole prompt.** Per-field caps bound any one field, but a CVE-heavy image (hundreds of CVEs, each at its per-field cap) could still @@ -84,12 +84,12 @@ Advisory text reaches the model as **structured, length-capped, fenced data**: The verdict cache keys on `entry_fingerprint`, which is the budget guard against re-judging on every watch event (ADR-0013; one CPU-only model call is dear on a Pi — -JEF-63). The advisory contributes only its **stable** fields — summary, CWE, fix +). The advisory contributes only its **stable** fields — summary, CWE, fix reference — and **no timestamps**. So a freshly-synced snapshot busts the cache **once** (the entry is re-judged with the new evidence) and is then stable across passes; it does not thrash per pass. -## Amendment (JEF-238): a co-located feed-fetcher sidecar is the approved live-enrichment mechanism +## Amendment: a co-located feed-fetcher sidecar is the approved live-enrichment mechanism The core rule above is unchanged: **the engine (and the security graph) make no outbound advisory/KEV call and never transmit cluster data — they only READ mounted files.** What @@ -114,10 +114,10 @@ It is sanctioned as the single approved live-enrichment lane: - **Full data, no ConfigMap limit.** An `emptyDir` has no size cap, so the **full** CISA KEV JSON (~1.5 MiB) and advisory data are fetched and read in full. -**Supersedes.** This replaces the **JEF-228** feed-sync CronJob+ConfigMap path: raw CISA +**Supersedes.** This replaces the **** feed-sync CronJob+ConfigMap path: raw CISA KEV (~1.5 MiB) exceeds Kubernetes' 1 MiB ConfigMap limit (forcing a lossy CVE-IDs-only extraction) and advisory data does not fit at all. It also definitively closes the -cancelled **JEF-110** engine-fetch option (§2): the engine never fetches; only the +cancelled **** engine-fetch option (§2): the engine never fetches; only the co-located, no-cluster-access sidecar does. The advisory file the sidecar fetches must already be in the `AdvisoryStore` CVE-keyed shape (§4) — a transform from a raw OSV/GHSA bulk feed is a documented follow-up, not part of this lane today. @@ -127,7 +127,7 @@ sidecar entirely (nothing in the chart egresses); an operator can still mount th snapshot files into the engine for fully-offline enrichment — the §1 mounted-snapshot posture, verbatim. -## Amendment (JEF-242): the advisory feed is RETIRED — the engine leans on Trivy for CVE metadata +## Amendment: the advisory feed is RETIRED — the engine leans on Trivy for CVE metadata The advisory enrichment lane this ADR established is **removed**. The engine no longer consumes any advisory feed: `AdvisoryStore`, the `Advisory` type, the `Vulnerability.advisory` @@ -135,7 +135,7 @@ field, the `PROTECTOR_ADVISORY_FILE` wiring, the advisory branches in the prompt (`cve_evidence`), and the chart's `feedSync.advisoryUrl` + the sidecar's advisory fetch all go away. **The only feed egress is now the CISA KEV catalogue.** -**Why it was added then retired (kept honest).** The advisory lane was built (JEF-52/JEF-103, +**Why it was added then retired (kept honest).** The advisory lane was built ( this ADR) to give the model a CWE class, a fix reference, and a short summary so it could reason "a fix exists but the workload is still on the vulnerable version". In practice that evidence proved **redundant with Trivy** (the Vulnerability port, ADR-0003), which already @@ -144,7 +144,7 @@ advisory `summary` ≈ Trivy's `title`, the advisory `fix_ref` ≈ Trivy's `fixe only net-new advisory field was `cwe[]`, which trivy-operator omits anyway, so it was empty in practice. The default NVD "recent" feed (~8.5 MiB) also had a poor hit-rate against the old base-image CVEs Trivy actually finds. The cost (a second feed, a gzipped 8.5 MiB -download every cycle, the whole JEF-106 injection surface of untrusted advisory free-text) +download every cycle, the whole injection surface of untrusted advisory free-text) no longer bought anything Trivy didn't already give. **What replaces it.** Trivy's per-vulnerability CVSS **`score`** (a float, e.g. `9.8`; often @@ -157,7 +157,7 @@ Trivy; it loses only the redundant advisory adjunct. signal, `exploit_intel.rs`) is a **distinct** exploitation signal that drives the breach model (ADR-0016) — it is NOT advisory enrichment and is explicitly kept. The KEV mounted-feed lane, its zero-egress posture, and its feed-fetcher sidecar are exactly as this ADR and the -JEF-238 amendment describe them. + amendment describe them. **What still holds from this ADR.** Every rule above about *mounted feeds* — zero engine egress (the engine only READS files; only the co-located, no-cluster-access sidecar @@ -166,7 +166,7 @@ discipline (only stable fields ride the verdict cache; the CVSS `score` is one s field) — governs the KEV feed unchanged. The injection-safety machinery is now smaller because the only untrusted free-text reaching the promote-capable model is Trivy's `title`, which is still capped → sanitized → fenced and charged to the per-entry aggregate budget -(JEF-106). The advisory-only caps (`summary`/`fix_ref`/CWE) are removed with the field they +. The advisory-only caps (`summary`/`fix_ref`/CWE) are removed with the field they guarded; the caps that also guard `title` stay. ## Amendment (ADR-0020): Rekor transparency-log reads are a sanctioned outbound lane for signature continuity @@ -223,10 +223,10 @@ Harder / accepted: - **Live OSV/NVD fetch as the default.** Rejected: outbound calls keyed on the cluster's own CVEs leak the cluster's vulnerability profile to a third party, against - the in-cluster posture. Deferred to an opt-in lane (JEF-110), not built here. + the in-cluster posture. Deferred to an opt-in lane, not built here. - **Surface the raw advisory description / patch diff verbatim.** Rejected: unbounded - untrusted free text into a promote-capable model is exactly the JEF-106 injection + untrusted free text into a promote-capable model is exactly the injection surface `sanitize` cannot close. Structural extraction + hard caps instead. - **Put advisory timestamps in the fingerprint.** Rejected: volatile fields would - thrash the verdict cache every pass and starve the slow CPU model (the JEF-63 + thrash the verdict cache every pass and starve the slow CPU model (the budget). Stable fields only. diff --git a/docs/adr/0016-severity-vs-urgency.md b/docs/adr/0016-severity-vs-urgency.md index d5642ec0..626b870d 100644 --- a/docs/adr/0016-severity-vs-urgency.md +++ b/docs/adr/0016-severity-vs-urgency.md @@ -78,5 +78,5 @@ the live proof+enrichment, not a fixed timer. - **Enrichment coverage is load-bearing.** The decision is only as good as (2): CVE scan, static reachability (M2), and behavioral telemetry (the first-party eBPF agent, or any sensor via the tool-agnostic behavioral port). Gaps weaken the - model's input. Prompt-injection hardening (JEF-106) matters precisely because the model + model's input. Prompt-injection hardening matters precisely because the model decides on (2)'s evidence and acts on it. diff --git a/docs/adr/0017-isolation-persists-on-the-breach-condition.md b/docs/adr/0017-isolation-persists-on-the-breach-condition.md index e6a32ae4..0ea914eb 100644 --- a/docs/adr/0017-isolation-persists-on-the-breach-condition.md +++ b/docs/adr/0017-isolation-persists-on-the-breach-condition.md @@ -30,7 +30,7 @@ That digest exists: `entry_fingerprint` in `engine/src/engine/reason/adjudicate. [ADR-0015](0015-advisory-evidence-egress.md) §5 made it the **stable evidence digest** the verdict cache keys on — it hashes the entry's exploited/critical CVEs (with their stable advisory fields — CWE, fix reference, capped summary — no timestamps), its COARSE -runtime-behavior keys, and its reachable-objective set with reach tags ([JEF-79]). It is +runtime-behavior keys, and its reachable-objective set with reach tags. It is deliberately built to change **once** when the evidence that would change the model's call changes, and stay stable across passes otherwise. That is precisely the property a breach-condition revert key needs: it moves when enrichment (2) meaningfully changes and @@ -55,7 +55,7 @@ runtime behaviors + objectives + reach tags that constituted the concerning sign the breach-condition key the mitigation ledger holds alongside the cut. Reusing the existing digest, rather than adding a parallel store, keeps one source of truth for "what evidence is this verdict standing on" and inherits ADR-0015's stability guarantee for -free (it busts once on real change, not per pass — the [JEF-63] budget). +free (it busts once on real change, not per pass — the budget). ### 2. Conjunction semantics @@ -96,7 +96,7 @@ doesn't thrash or flap the cut on mundane churn in between. This refines ADR-0016 §3 (which named the two clearing conditions but not the key or the detection mechanism) and ADR-0009 (the self-reverting action — the revert condition is the breach condition, tied to the live proof+enrichment, not a fixed timer). The -implementation of the revert is [JEF-134]'s revert portion; this ADR is the decision +implementation of the revert is 's revert portion; this ADR is the decision record only. ## Consequences diff --git a/docs/adr/0018-operator-configured-redacted-breach-notifier.md b/docs/adr/0018-operator-configured-redacted-breach-notifier.md index 604e6299..527d3abf 100644 --- a/docs/adr/0018-operator-configured-redacted-breach-notifier.md +++ b/docs/adr/0018-operator-configured-redacted-breach-notifier.md @@ -7,7 +7,7 @@ ## Context Surfacing today is **pull-only**: a breach decision is recorded in the findings -snapshot and the judgement record, and the durable journal ([JEF-141](journal.rs)) +snapshot and the judgement record, and the durable journal () replays it after a restart — but a solo operator never *learns* protector decided a breach unless they are watching the engine's output. The motivating pain is exactly that gap (the post-restart blind-window memo): the decision is made, recorded, and @@ -127,11 +127,11 @@ Harder / accepted: - **An unbounded reqwest client.** Rejected: a hung sink would stall the single engine loop — the exact failure `model.rs` bounds against. Reuse the timeout-only client. -## Extension — runtime-coverage collapse (JEF-427) +## Extension — runtime-coverage collapse The breach notice fires only on breach *decisions*. But when protector's OWN runtime sensors go dark, a blind engine makes no breach decisions — so the one moment the -operator most needs a push, the notifier stays silent. JEF-427 adds a second, narrow +operator most needs a push, the notifier stays silent. adds a second, narrow event on this same sanctioned path: an edge-triggered **runtime-coverage** notice. - **Same posture, same plumbing.** Off unless `PROTECTOR_ENGINE_NOTIFY_URL` is set @@ -139,8 +139,8 @@ event on this same sanctioned path: an edge-triggered **runtime-coverage** notic client; a failure is logged once and dropped. It never touches a verdict, an actuation, or the journal. - **Edge-triggered, not per-pass.** Fires exactly once when a was-covering fleet goes - fully dark past the JEF-421 stall debounce (`runtime_coverage_degraded`), and once - when it recovers (`runtime_coverage_restored`) — reusing JEF-421's hysteresis so a + fully dark past the stall debounce (`runtime_coverage_degraded`), and once + when it recovers (`runtime_coverage_restored`) — reusing 's hysteresis so a routine DaemonSet roll never strobes it. - **Counts-only, redacted by construction.** The payload carries the event tag, the feed label (`Runtime`, our own constant), and COUNTS — N of M sensor nodes blind — diff --git a/docs/adr/0019-dashboard-v3-presentation-architecture.md b/docs/adr/0019-dashboard-v3-presentation-architecture.md index 3a9ec28e..6da4d624 100644 --- a/docs/adr/0019-dashboard-v3-presentation-architecture.md +++ b/docs/adr/0019-dashboard-v3-presentation-architecture.md @@ -14,11 +14,11 @@ > **`view_model`/props half is RETAINED and elevated** to the serde-serialized JSON contract. > > **What SURVIVES unchanged:** **§3 (the information architecture)** and **§4 (the honesty -> model — three orthogonal axes)**, plus the **JEF-281 amendment** (finding detail shows all +> model — three orthogonal axes)**, plus the ** amendment** (finding detail shows all > proven paths). These were the product; ADR-0025 preserves them on the new stack. The content > below is retained in full for that reason — see ADR-0025 for what is lost vs kept. > -> **Cutover COMPLETED (JEF-398).** The supersession above is now realized in the tree: the maud +> **Cutover COMPLETED.** The supersession above is now realized in the tree: the maud > **body** renderers, the `/fragment` route, and the per-tab Preact flag are **deleted** — the > engine is Preact-only. What remains server-rendered from this ADR is the honest first-paint > shell only: the persistent **status strip** and the **tab nav** (`components/status_strip.rs`, @@ -121,7 +121,7 @@ while `model_judging == true`.** When the model is warming or not answering, exp HTML-escaped at render (maud auto-escape). (Render test.) 7. No source file exceeds 1,000 lines. (`file_size_guard`.) -## Amendment (JEF-281) — the finding detail shows ALL proven paths, not one +## Amendment — the finding detail shows ALL proven paths, not one The v3 rewrite retired the old Mermaid graph (deliberately: it drew an unreadable "wall of arrows", pulled a third-party client graph library over a CDN — a zero-egress violation — and fed diff --git a/docs/adr/0020-signature-continuity.md b/docs/adr/0020-signature-continuity.md index cb91ba40..986f7de2 100644 --- a/docs/adr/0020-signature-continuity.md +++ b/docs/adr/0020-signature-continuity.md @@ -159,7 +159,7 @@ What becomes harder / the downsides we accept: and can over-trust a repo that legitimately serves a mix; the staged rollout starts at repo granularity and revisits if observation shows it is too coarse. -## Addenda (JEF-263 — durable TOFU baseline implementation) +## Addenda (— durable TOFU baseline implementation) These ratify the two implementation decisions the durable baseline (Decision §2) required but did not pin. Both preserve the invariant that the *established* signed @@ -184,7 +184,7 @@ history — the security-bearing state — is the thing that must never be silen state (`first_seen_ms` is already persisted) and is monotonic — once established, a later observation never un-establishes. A digest-count or distinct-day refinement remains a future option; `established` + `first_seen` are exposed so the render - (JEF-262) and drift (JEF-264) work can weigh the distinction as they choose. + and drift work can weigh the distinction as they choose. Follow-up to monitor (not a blocker): per-pass full compaction shares the single decision journal with breach/admission lines, so it raises write volume and @@ -192,9 +192,9 @@ accelerates rotation of those other line kinds. Bounded by `DEFAULT_MAX_REPOS` a acceptable at current scale; revisit change-only or a segmented journal if a large cluster shows rotation pressure on breach/admission history. -## Addenda (JEF-280 — drift is baseline-relative; downgrade is a first-class regression) +## Addenda (— drift is baseline-relative; downgrade is a first-class regression) -The honest-posture split (JEF-276) added two *calm* signing postures — `SignedKeyBased` +The honest-posture split added two *calm* signing postures — `SignedKeyBased` (a real key-based cosign signature: verified Rekor bundle, no Fulcio identity) and `UnverifiableHere` (a signature present but unverifiable against our trust root, a Rekor/TUF variance) — so a legitimately key-based repo (e.g. cert-manager) stops @@ -208,7 +208,7 @@ audit mode). `unsigned→NotSigned` and `keyless→new-identity` were already ca key-based / unverifiable *downgrade* was not. These addenda ratify the fix. They change **drift classification only** — the per-image -posture and the trust/admit semantics (JEF-276) are untouched: the calm postures still +posture and the trust/admit semantics are untouched: the calm postures still confer no trusted identity and still `would_admit() == false`. 1. **Drift is baseline-RELATIVE, ranked.** Each signing posture has a trust-strength @@ -223,11 +223,11 @@ confer no trusted identity and still `would_admit() == false`. 2. **Signing downgrade is a first-class regression class.** An established **keyless** baseline now serving a lesser-but-calm posture (`SignedKeyBased` / `UnverifiableHere`) fires a new `SigningDowngrade` regression — the registry-substitution signal. It rides - JEF-264's admission-finding path (audit-only, shadow; ADR-0016) and feeds the honesty + 's admission-finding path (audit-only, shadow; ADR-0016) and feeds the honesty model exactly as other regressions do: an **established** baseline → breach/non-green; a **cold / freshly-learned** one → uncertain/non-green (never silent). A repo that was **always** key-based (no keyless baseline was ever learned) serving key-based stays - `Continuous` — the JEF-276 false-alarm fix is preserved, because there is no stronger + `Continuous` — the false-alarm fix is preserved, because there is no stronger baseline rank to drop from. 3. **TUF-staleness is surfaced, never silent.** `UnverifiableHere` is caused by a @@ -241,7 +241,7 @@ confer no trusted identity and still `would_admit() == false`. small floor) are deliberately simple; parsing the TUF expiry and tracking a historical unverifiable-rate delta are future refinements. -## Addenda (JEF-275 — provenance is a second continuity axis) +## Addenda (— provenance is a second continuity axis) A cosign signature proves *who* signed an image; SLSA **build provenance** proves *how it was built* — the source repository and the builder/workflow (a GitHub Actions OIDC @@ -274,17 +274,17 @@ new egress path. untouched. A repo with no signing baseline therefore has a **cold** provenance axis — its provenance drift is a weak lead, never a silent miss. -3. **Provenance change is a drift class on JEF-264's audit channel.** A verified +3. **Provenance change is a drift class on 's audit channel.** A verified provenance whose source or builder is **not** in an established repo's learned set fires a **provenance-change** finding — the "built by an unexpected workflow / from an unexpected source" signal — distinct in reason from a signing regression (a repo can carry both). It rides the same admission-finding path (audit-only, shadow; ADR-0016): an **established** baseline → strong signal; a **cold** one → a weak lead, never silent - — exactly the baseline-relative semantics JEF-280 established. Absent / unverifiable / + — exactly the baseline-relative semantics established. Absent / unverifiable / checking never fire a change. 4. **Degrades cleanly.** An image with no provenance (today's norm) simply reads `Absent` — - calm, never an alarm. *(Superseded by the JEF-410 addendum below: the sweep is now + calm, never an alarm. *(Superseded by the addendum below: the sweep is now default-on, not opt-in — it was never a new egress destination, so gating it behind a flag was detection proliferation, not an egress control.)* @@ -300,15 +300,15 @@ honest degradation. Closing the gap (compose sigstore's lower-level DSSE + Fulci primitives, or an upgraded `sigstore` release) is a follow-up that does not change this addendum's contract. -## Addenda (JEF-297 — the rendered "if enforced" is CONTINUITY, not keyless-identity) +## Addenda (— the rendered "if enforced" is CONTINUITY, not keyless-identity) The signing inventory's **"if enforced"** column is a counterfactual: *what would a signature gate do to this image?* Its first implementation read that column off the raw posture — `would_admit ⇔ keyless-Fulcio Signed`, every other posture would-block. That is the **pre-ADR-0020 single-identity gate**, and it directly contradicts the continuity -thesis of this ADR and the honest-posture split (JEF-276): the *entire* key-based-signed +thesis of this ADR and the honest-posture split: the *entire* key-based-signed homegrown fleet (and cert-manager) rendered **would-block**, even though such a repo is -perfectly calm and continuous. The JEF-280 addendum's aside that the calm postures "still +perfectly calm and continuous. The addendum's aside that the calm postures "still `would_admit() == false`" was itself this bug — it conflated the *inventory trust semantic* ("this posture confers no trusted keyless identity", which is true) with the *enforcement counterfactual* ("a continuity gate would reject this image", which is @@ -318,9 +318,9 @@ This addendum corrects the render. It changes **presentation only** — no obser drift classification, no enforcement, no egress changes. 1. **would-admit is the negation of a REGRESSION, not a posture test.** The counterfactual - a signature-continuity gate (JEF-265) applies is: *block on a genuine regression from + a signature-continuity gate applies is: *block on a genuine regression from the repo's established baseline; admit everything continuous.* So the column is derived - from the baseline-relative drift verdict (JEF-264/280), NOT the raw posture: + from the baseline-relative drift verdict, NOT the raw posture: * **would-admit** — any calm posture with no regression: keyless-verified `Signed`, consistent `SignedKeyBased` / `UnverifiableHere` (no keyless baseline to drop from), and `NotSigned` where the repo was never signed (TOFU). This is `block == regression` @@ -330,14 +330,14 @@ drift classification, no enforcement, no egress changes. genuinely `InvalidSignature` (the reserved loud channel — a broken signature is never admissible independent of any baseline). * **uncertain** — a regression against a **cold / freshly-learned** baseline: a weak - lead (JEF-280 cold=uncertain), non-green but never a hard block. This keeps the + lead (cold=uncertain), non-green but never a hard block. This keeps the cold-baseline honesty invariant on the enforcement column too. 2. **Single source of truth = the recorded drift verdict.** The render reads the SAME `SigningRegression/` rows the sweep already recorded (one per regressing image), keyed per image — it never re-classifies against the baseline, so the "if enforced" column and the recorded regression a gate enforces are the same fact. The old per-posture - `SigningPosture::would_admit()` is retired as the render input (superseding the JEF-280 + `SigningPosture::would_admit()` is retired as the render input (superseding the addendum's `would_admit() == false` note); the inventory trust semantic it expressed (`Signed` is the only *keyless-verified* posture) is unchanged and still drives the posture chip. @@ -347,7 +347,7 @@ drift classification, no enforcement, no egress changes. the repo's baseline cold. Cold-baseline regressions read *uncertain* (non-green), never silent and never a fabricated green admit. -## Addenda (JEF-265 — Stage 3 ENFORCE: deny on regression in enforced scope; "exception accepted") +## Addenda (— Stage 3 ENFORCE: deny on regression in enforced scope; "exception accepted") Stages 1–2 (and the addenda above) observe, learn, and *surface* signing drift — audit-only, the shadow invariant (ADR-0016). This addendum ratifies Stage 3: the FIRST code that makes protector @@ -359,11 +359,11 @@ scope only; observation, learning, drift classification, and the render are unch ADR-0021 — namespace or Pod label; `mode: enforce`), a signing **regression** against a repo's **established** baseline is a `Deny`; out of scope it is an `Audit` (recorded, still admitted). The block predicate is the DOMAIN verdict `SigningDrift::would_block` — the exact semantic - JEF-297's presentation `SigningEnforcement::WouldBlock` projects, so the inventory's "would block" + 's presentation `SigningEnforcement::WouldBlock` projects, so the inventory's "would block" column and what admission actually blocks are the same fact. The audit-everywhere default is unchanged: with no `enforceScope`, **nothing is denied** — byte-identical shadow. -2. **Cold-start never denies.** A freshly-learned / not-yet-`established` baseline (JEF-263's 24h +2. **Cold-start never denies.** A freshly-learned / not-yet-`established` baseline ('s 24h maturation) is the weakest evidence (TOFU). `would_block` returns `false` for a cold-baseline regression → admit (audit). Only an established-baseline regression, or a genuinely-`InvalidSignature` posture (the loud channel, inadmissible independent of any baseline so it can't be dodged by keeping @@ -404,9 +404,9 @@ scope only; observation, learning, drift classification, and the render are unch pin regexp, an unavailable observer, or an empty/poisoned baseline snapshot all degrade in the safe direction: MORE enforced or NOT denying — never a silent widen of what is admitted. -## Addenda (JEF-410 — build provenance is default-ON: retire `PROTECTOR_PROVENANCE_ENABLE`) +## Addenda (— build provenance is default-ON: retire `PROTECTOR_PROVENANCE_ENABLE`) -The JEF-275 addendum shipped the provenance sweep opt-in behind `PROTECTOR_PROVENANCE_ENABLE`, +The addendum shipped the provenance sweep opt-in behind `PROTECTOR_PROVENANCE_ENABLE`, "mirroring the Rekor lane." That mirroring was a mistake: the Rekor lane's flag (`PROTECTOR_REKOR_ENABLE`) is a genuine **egress** gate — Rekor is a separate outbound destination from the registry the cluster already pulls from, and ADR-0015's zero-egress default diff --git a/docs/adr/0022-quarantine-the-entry-is-the-default-containment.md b/docs/adr/0022-quarantine-the-entry-is-the-default-containment.md index a34928fe..b753caee 100644 --- a/docs/adr/0022-quarantine-the-entry-is-the-default-containment.md +++ b/docs/adr/0022-quarantine-the-entry-is-the-default-containment.md @@ -1,6 +1,6 @@ # 0022. Quarantine the internet-facing entry is the default containment; the surgical edge-cut is the refinement -- Status: Accepted; the **JEF-284 amendment's decision procedure is superseded in part by [ADR-0032](0032-model-is-incident-responder.md)** (the model now decides the cut). The containment vocabulary, additive/reversible shapes, and the precedence ladder survive as the menu ordering / proposal fallback. +- Status: Accepted; the ** amendment's decision procedure is superseded in part by [ADR-0032](0032-model-is-incident-responder.md)** (the model now decides the cut). The containment vocabulary, additive/reversible shapes, and the precedence ladder survive as the menu ordering / proposal fallback. - Date: 2026-07-03 ## Context @@ -111,7 +111,7 @@ entry-quarantine default, not only an edge-cut) and **entry** by default, not the cut edge's source, and is no longer gated on a network edge existing). -## Amendment (JEF-284): quarantine any *compromised* pod on the chain — reached ≠ exploited +## Amendment: quarantine any *compromised* pod on the chain — reached ≠ exploited The entry quarantine above contains the *front door*. But a breach chain has more than a front door: a popped app two hops in, or an internal pod with hands-on-keyboard @@ -127,7 +127,7 @@ internet-facing entry" to **any qualifying pod on a proven chain**, via a new actually running on it (the `compromisable` predicate — the same bar the proof walk's compromise gate and `entry_foothold` already use). Reachability alone is not enough. 2. **Actively exploited** — the pod has direct live on-pod runtime evidence - (`Behavior::is_alert` / a hands-on-keyboard `notable_exec`, JEF-117) — exploitation + (`Behavior::is_alert` / a hands-on-keyboard `notable_exec`) — exploitation *now* — **regardless of network position**, internal pods included. **The hard guard: never quarantine a merely-reached objective.** A pod that is only a @@ -141,8 +141,8 @@ compromises). The **entry itself stays governed entirely by the precedence above**: it is excluded from condition 1, and its condition-2 quarantine is added only when the primary containment did not already contain it with an additive-live control (a surgical -edge-cut or the entry quarantine) — so JEF-279's behavior and the "prefer the narrower -surgical cut" invariant are preserved byte-for-byte. +edge-cut or the entry quarantine) — so this ADR's existing default-containment behavior and +the "prefer the narrower surgical cut" invariant are preserved byte-for-byte. `QuarantineWorkload` reuses the ADR-0010 `render_isolation` shape driven from the qualifying pod's labels (a self-reference `cut` link, pod-only signature so a pod that @@ -160,12 +160,12 @@ dashboard disposition names the WHY — `quarantine — remotely exploitable` / `quarantine — actively exploited` — distinct from the entry-foothold `quarantine entry (default-deny)`; all are fixed internal strings (no untrusted text). -## Amendment (JEF-547, 2026-07-27): the model decides the cut — the JEF-284 procedure is superseded by [ADR-0032](0032-model-is-incident-responder.md) +## Amendment (2026-07-27): the model decides the cut — the procedure is superseded by [ADR-0032](0032-model-is-incident-responder.md) -The JEF-284 amendment above made the **per-pod deterministic bar the auto-action trigger** +The amendment above made the **per-pod deterministic bar the auto-action trigger** (remotely-exploitable on reachability + CVE presence, or actively-exploited on a live signal, *"regardless of network position, internal pods included"*), with the model never consulted. -A clean fact-check (JEF-322) confirmed this contradicts the product thesis (ADR-0013/0029): +A clean fact-check confirmed this contradicts the product thesis (ADR-0013/0029): the coarsest action had the weakest bar, and CVE *presence* auto-cut downstream pods. **ADR-0032 supersedes that decision procedure.** The model — as incident responder — decides diff --git a/docs/adr/0023-delta-aware-adjudication.md b/docs/adr/0023-delta-aware-adjudication.md index 3ff835c4..63151aaf 100644 --- a/docs/adr/0023-delta-aware-adjudication.md +++ b/docs/adr/0023-delta-aware-adjudication.md @@ -18,7 +18,7 @@ the last time we judged this entry**. The per-entry verdict cache (`VerdictStore decisive verdict on the SHA-256 of that whole prompt and re-judges whenever the hash changes. -A 77-minute production capture (JEF-387 harness, 70 re-judges across 5 entries) showed: +A 77-minute production capture (harness, 70 re-judges across 5 entries) showed: - **100% of re-judges are prompt-churn**, 0% Uncertain-retry. - **~85% are genuinely-new fingerprints** the cache has never seen — dominated by the @@ -26,7 +26,7 @@ A 77-minute production capture (JEF-387 harness, 70 re-judges across 5 entries) churns per-pod registration secrets every few seconds; each new replica adds a new reachable `secret/…` objective). - Only ~15% is exact-state ping-pong (a known peer aging in/out of the runtime window), - recoverable by a multi-slot cache (JEF-390). + recoverable by a multi-slot cache. The key realization: **the churn is correct.** A newly-reachable object *is* new attack surface and *should* be evaluated. The waste is not the re-judge — it is that on every @@ -35,7 +35,7 @@ the only thing different is one new object of a kind it has already judged fifty Meanwhile the engine **already computes the delta** it would need: `graph::delta` emits added/removed edges each pass, `first_seen` stamps when each node first appeared, and -`prev_posture` (JEF-201) diffs posture pass-over-pass. All of it feeds the dashboard's Δ +`prev_posture` diffs posture pass-over-pass. All of it feeds the dashboard's Δ column — **none of it reaches the adjudication prompt.** ## Decision @@ -69,9 +69,9 @@ the *change* as the explicit *question*. 4. **Verdict/cache semantics.** A decisive verdict is now "valid for entry `E` as of baseline `B`", and stays valid until an additive delta arrives. This supersedes the - whole-prompt-fingerprint gate for the re-judge decision (the fingerprint LRU of JEF-390 + whole-prompt-fingerprint gate for the re-judge decision (the fingerprint LRU of remains as a second-level guard for exact-state returns and as the cache key within a - baseline). Uncertain verdicts are still never cached; JEF-234 backoff still gates the + baseline). Uncertain verdicts are still never cached; backoff still gates the retry of failed decisions. ## Correctness guard (non-negotiable) @@ -99,11 +99,11 @@ the delta is a rejection of this ADR. blast radius — worst case the model sees the new object in the full set but without the "NEW" flag, i.e. today's behavior. - **Interacts with:** ADR-0013 (adjudication — this refines *what* the model is asked), - ADR-0001 (deterministic proof is the source of the delta), JEF-390 (LRU cache, second - level), JEF-234 (Uncertain backoff, unchanged). The de-escalation of a verdict whose - surface vanished is the reversion path (ADR-0009/JEF-141), not a re-judge. + ADR-0001 (deterministic proof is the source of the delta) (LRU cache, second + level) (Uncertain backoff, unchanged). The de-escalation of a verdict whose + surface vanished is the reversion path (ADR-0009), not a re-judge. -## Open questions (to resolve before implementation, JEF-391) +## Open questions (to resolve before implementation) - Exact projection of the graph delta into prompt lines (which node/edge kinds count as an "addition" worth flagging; how to summarize a burst of same-kind additions without hiding diff --git a/docs/adr/0024-no-redundant-by-construction-predicates.md b/docs/adr/0024-no-redundant-by-construction-predicates.md index 500c4d1a..611afe4c 100644 --- a/docs/adr/0024-no-redundant-by-construction-predicates.md +++ b/docs/adr/0024-no-redundant-by-construction-predicates.md @@ -6,20 +6,20 @@ ## Context -JEF-319 (retire-Falco G4) proposed two entry-scoped corroboration shapes on + (retire-Falco G4) proposed two entry-scoped corroboration shapes on `corroborated_for`: **cross-tenant lateral** and **reverse-shell**. The corroboration predicate is not cosmetic — flipping `corroborated` can gate a quarantine (ADR-0009 / ADR-0011) — so what it admits is load-bearing and what it *cannot* admit is dead weight. The reverse-shell shape (`notable exec → outbound egress within 60s`) was -**redundant-by-construction**: the existing blanket notable-exec arm (JEF-117) already +**redundant-by-construction**: the existing blanket notable-exec arm already returns `true` for ANY objective whenever a notable exec is present. A shape that fires only when a notable exec is present is therefore strictly narrower than a condition that already holds — it could not independently change the `corroborated_for` boolean. It was proposed as documented, unit-tested-in-isolation code kept "for when the blanket exec arm is later narrowed." -This is exactly the shape the Fable audit (JEF-363/364/367…) was called to excise: a +This is exactly the shape the Fable audit was called to excise: a tidy, well-tested, in-code-documented construct whose output was already determined by another arm, which survived review *because* it was tidy and tested. Redundant-by- construction code that "works" is still a defect (Hickey: incidental complexity; @@ -34,13 +34,13 @@ current predicate.** A shape whose value is contingent on a *future* narrowing o arm lands **with** that narrowing — so it arrives load-bearing, with a test that can actually fail — not ahead of it on the promise of future need. -Concretely for JEF-319: +Concretely for - **Cross-tenant lateral is merged.** A bare in-cluster `NetworkConnection` does not blanket-corroborate, so `is_cross_tenant` is the only thing that can flip `corroborated_for` for that shape; it is genuinely load-bearing and tested end-to-end through `corroborated_for` (positive; same-ns negative; non-foothold negative). - **Reverse-shell is stripped**, along with its isolated predicate tests. A follow-up - ticket tracks implementing it **when** the blanket notable-exec arm (JEF-117) is + ticket tracks implementing it **when** the blanket notable-exec arm is narrowed as part of retiring Falco; at that point the exec+egress-timing correlation becomes the load-bearing reverse-shell signal and lands with a test that can fail. diff --git a/docs/adr/0025-dashboard-v4-preact-client-render.md b/docs/adr/0025-dashboard-v4-preact-client-render.md index 6dade97a..8f436421 100644 --- a/docs/adr/0025-dashboard-v4-preact-client-render.md +++ b/docs/adr/0025-dashboard-v4-preact-client-render.md @@ -1,7 +1,7 @@ # 0025. Dashboard v4: a bundled Preact client reconciling from same-origin JSON, superseding maud server-render -- Status: Accepted — **cutover COMPLETE** (JEF-398): rolled out per-tab behind a flag (JEF-397 / - JEF-400), then the maud render half + the flag were deleted; the engine is Preact-only. +- Status: Accepted — **cutover COMPLETE**: rolled out per-tab behind a flag, + then the maud render half + the flag were deleted; the engine is Preact-only. - Date: 2026-07-11 - Supersedes (in part): [0019](0019-dashboard-v3-presentation-architecture.md) — its presentation-*mechanism* decisions (§1 server-rendered maud, §2 `page.rs`/`/fragment` @@ -96,7 +96,7 @@ ADR-0019's **presentation-mechanism** decisions are **Superseded by ADR-0025**: ADR-0019's **§3 information architecture** (primary Findings + secondary tabs + one persistent status strip; urgency-not-severity sort) and **§4 honesty axes** (breach-vs-safe, decided-vs-awaiting, covered-vs-blind; green honest only while `model_judging`) **SURVIVE -unchanged** — they are the product this rewrite preserves. The JEF-281 amendment +unchanged** — they are the product this rewrite preserves. The amendment (finding detail shows *all* proven paths, not one) survives as a data/IA requirement: the JSON carries every proven path and the client renders them as keyed, collapsible staircases. @@ -154,13 +154,13 @@ Harder / accepted: tokens) plus the `dangerouslySetInnerHTML` ban and the JSON-props honesty tests — the same discipline ADR-0019 §4 enforced, relocated to the props boundary. -## Cutover status (JEF-398 — COMPLETE) +## Cutover status (— COMPLETE) -The migration ran in five parts. JEF-395 stood up the read-only `/api/*.json` snapshots from the -serde view-model and relocated the honesty guards to the JSON-props boundary; JEF-396 built the -bundle from source (gitignored) and added the source/bundle guards; JEF-397 and JEF-400 ported all +The migration ran in five parts. stood up the read-only `/api/*.json` snapshots from the +serde view-model and relocated the honesty guards to the JSON-props boundary; built the +bundle from source (gitignored) and added the source/bundle guards; and ported all five views to Preact behind a per-tab flag (`PROTECTOR_DASHBOARD_PREACT_TABS`), rolling out live in -prod. **JEF-398 completed the cutover:** with the honesty invariants proven on the new stack, it +prod. ** completed the cutover:** with the honesty invariants proven on the new stack, it **deleted the maud render half** (the `components/*_view.rs` / `finding_*` / `evidence.rs` body renderers, the `/fragment` route, and the fragment composition in `page.rs`/`mod.rs`) and **removed the per-tab flag** (`preact_flags.rs`, the `PROTECTOR_DASHBOARD_PREACT_TABS` env read, and diff --git a/docs/adr/0026-adjudication-judge-qwen3-1.7b.md b/docs/adr/0026-adjudication-judge-qwen3-1.7b.md index c84cff02..9d2a10dd 100644 --- a/docs/adr/0026-adjudication-judge-qwen3-1.7b.md +++ b/docs/adr/0026-adjudication-judge-qwen3-1.7b.md @@ -4,7 +4,7 @@ - Date: 2026-07-11 - Refines: [0013](0013-proof-winnows-model-decides.md) (the model makes the exploitability call), [0023](0023-delta-aware-adjudication.md) (delta-aware prompt) -- Relates: JEF-405 (the prompt this bakeoff was run against), JEF-402 (the +- Relates: (the prompt this bakeoff was run against) (the exposed-secret / reachable-secret distinction one of the cases exercises) ## Context @@ -31,15 +31,15 @@ repeatedly been the deciding factor: image" field as exploitation evidence. That is one of the three evidence types ADR-0013 requires the model to recognize. -`scripts/judge_bakeoff.py` benches candidate judges on the JEF-405-fixed prompt (the same +`scripts/judge_bakeoff.py` benches candidate judges on the -fixed prompt (the same `build_judgment_prompt` the engine runs) across cluster-representative cases: the three exploitation-evidence types that MUST be `exploitable`, and the refute cases (broad RBAC, cross-tenant network paths, not-observed CVEs, reachable-but-not-exposed secrets — the -JEF-402 false breach — the ArgoCD cluster-admin false positive) that MUST be `refuted`. + false breach — the ArgoCD cluster-admin false positive) that MUST be `refuted`. ## Bakeoff result -Dev box, temperature 0, current JEF-405 prompt, single-shot per case: +Dev box, temperature 0, current prompt, single-shot per case: | model | score | notes | |---|---|---| diff --git a/docs/adr/0027-dashboard-root-only-shell-client-strip.md b/docs/adr/0027-dashboard-root-only-shell-client-strip.md index 8964c0e2..e57e0b11 100644 --- a/docs/adr/0027-dashboard-root-only-shell-client-strip.md +++ b/docs/adr/0027-dashboard-root-only-shell-client-strip.md @@ -13,7 +13,7 @@ Under ADR-0025 the engine went Preact-only for every view *body*, but kept TWO parts server-rendered in maud — the status strip and the tab nav — so the honest calm-when-blind banner would paint before -any JS ran. That split had two concrete costs that surfaced in production (JEF-408): +any JS ran. That split had two concrete costs that surfaced in production: 1. **A dead recurring poll masqueraded as a working one.** `poll.js` called its injected interval as `(ms, fn)`, but the default was native `setInterval` (`(fn, ms)`), so `setInterval(POLL_MS, tick)` diff --git a/docs/adr/0028-dashboard-client-local-state-simplification.md b/docs/adr/0028-dashboard-client-local-state-simplification.md index 35e9c4f4..6881fc40 100644 --- a/docs/adr/0028-dashboard-client-local-state-simplification.md +++ b/docs/adr/0028-dashboard-client-local-state-simplification.md @@ -36,10 +36,10 @@ no Context, no signals, no new dependency. last-good snapshot), `strip` (global posture — its OWN state, decoupled from `data`), `status` (`first-load` | `live` | `stale`), and `lastGoodAt`. The store (`store.js`) is deleted. The status transitions are small updaters: a snapshot goes live + resets the freshness clock + persists the - strip (keeping the last if a snapshot omits it — JEF-410); stale never fires before the first + strip (keeping the last if a snapshot omits it); stale never fires before the first snapshot; a tab swap nulls `data` but never touches `strip`. - **The poll is decoupled to callbacks** (`poll.js` takes `{ tab, onSnapshot, onStale, liveRegion, - … }`), so it feeds `App`'s `useState` updaters directly with no store dependency. **The JEF-408 + … }`), so it feeds `App`'s `useState` updaters directly with no store dependency. **The fix is retained verbatim**: the default interval is `(ms, fn) => setInterval(fn, ms)` (a function-first handler, never a number coerced to a string and eval'd), the synchronous first `tick()`, the stale-on-failure paths, and the mid-selection defer guard all stand. The `App` poll diff --git a/docs/adr/0029-adjudication-verdict-is-authoritative.md b/docs/adr/0029-adjudication-verdict-is-authoritative.md index 0806dbaa..4032c0f9 100644 --- a/docs/adr/0029-adjudication-verdict-is-authoritative.md +++ b/docs/adr/0029-adjudication-verdict-is-authoritative.md @@ -71,14 +71,14 @@ guard, not the former; and it adds no new guards. entries are **accepted as a known tail cost**, mitigated only by choosing a better judge model, not by overriding or starving the model. - No "evidence guard" and no objective-list cap will be added; proposals to add them are closed by - pointing here. (JEF-414 is cancelled against this decision.) + pointing here. (A prior proposal along those lines is cancelled against this decision.) - The bakeoff remains the sanctioned lever: it stays synced to the live `build_judgment_prompt` and carries the real full-scale entries as fixtures, so model choice is evaluated against what prod actually sends. - If tail flips ever become frequent enough to matter operationally, the response is a model change (evaluated via the bakeoff) — a bounded, reversible knob — never a deterministic gate on the verdict. -## Amendment (2026-07-19): tag-grounding is grounding, not a verdict gate (JEF-451) +## Amendment (2026-07-19): tag-grounding is grounding, not a verdict gate The tail flip recurred on protector's own pod, and a full audit (fable architect, 2026-07-19; `scratchpad/false-positive-audit.md`) isolated its dominant shape: the model cites a **real** CVE id @@ -113,7 +113,7 @@ restructuring is being planned (split the CVE field by tag; rename the `[reachab is the deterministic backstop for the *grounding* failure the prompt fixes shrink but cannot guarantee, not a substitute for them. -## Amendment (2026-07-20): the judge sees only reachable CVEs — not-evidence, not capping (JEF-453) +## Amendment (2026-07-20): the judge sees only reachable CVEs — not-evidence, not capping The prompt now shows the judge ONLY `[reachability: loaded-at-runtime]` CVEs — the sole CVE category that is exploitation evidence. `not-observed`, `present-static-binary`, and `unknown`-reachability @@ -131,7 +131,7 @@ judge non-evidence," not "cap the evidence" — the objective list is still show Why it matters: the audit's root cause R1 for the recurring false `exploitable` was that `loaded-at-runtime` is the most-primed phrase in the prompt while the judge is shown not-observed CVEs — a non-evidence target to fabricate the tag onto. Removing that target is the source-level fix -the guard (JEF-451) backstops. Measured on the deployed qwen3:1.7b, A/B old-vs-new: the temp-0.8 +the guard backstops. Measured on the deployed qwen3:1.7b, A/B old-vs-new: the temp-0.8 boundary-mass on the protector flip prompt collapses **15% → 0%** with no false negatives (log4j / live-signal / exposed-secret all still flag). Bakeoff-validated per the discipline; the bakeoff SYS + fixtures are resynced and gain a `--temp` boundary-mass A/B mode. diff --git a/docs/adr/0030-app-level-oidc-verification-supersedes-edge-trust.md b/docs/adr/0030-app-level-oidc-verification-supersedes-edge-trust.md index e939fb46..68bb30da 100644 --- a/docs/adr/0030-app-level-oidc-verification-supersedes-edge-trust.md +++ b/docs/adr/0030-app-level-oidc-verification-supersedes-edge-trust.md @@ -121,7 +121,7 @@ we do not build a bespoke ID-JAG path. ADR-0016 establishes that the **deterministic layer proves and enriches; the model decides breach; presentation is a view, never a decision gate** — and the repo carries that "presentation is a view, never a gate" principle from ADR-0016 throughout (the dashboard -module's own doc-comment, ADR-0020 §JEF-265.4, ADR-0025's "Reaffirms 0016"). Nothing here +module's own doc-comment, ADR-0020 §.4, ADR-0025's "Reaffirms 0016"). Nothing here touches that: - Authentication gates **who may look at the view.** It is upstream of, and orthogonal to, @@ -145,7 +145,7 @@ endpoint it points at. That is an **outbound call.** Protector's posture is zero ADR-0020 hold. It resolves cleanly under **the exact test ADR-0015 established** for the CISA KEV catalogue -fetch (§Context.1, §JEF-238 amendment): the rejected lane there was a **per-CVE** OSV/NVD +fetch (§Context.1, § amendment): the rejected lane there was a **per-CVE** OSV/NVD lookup, because it is *keyed on the cluster's own data* (its CVE profile) and leaks that profile to a third party on every pass. The **sanctioned** lane was the KEV catalogue GET, because it is **the same request for every relying party** and carries **no cluster-specific diff --git a/docs/adr/0031-read-only-mcp-server-tiered-redaction.md b/docs/adr/0031-read-only-mcp-server-tiered-redaction.md index 9d83b2a0..dc77fa94 100644 --- a/docs/adr/0031-read-only-mcp-server-tiered-redaction.md +++ b/docs/adr/0031-read-only-mcp-server-tiered-redaction.md @@ -2,7 +2,7 @@ - Status: Proposed - Date: 2026-07-22 -- Relates to: [0014](0014-behavioral-telemetry-ebpf.md)/[0015](0015-advisory-evidence-egress.md) (the in-cluster, zero-egress posture this carves a second, bounded exception to), [0018](0018-operator-configured-redacted-breach-notifier.md) (the direct lineage — operator-owned, redacted-by-default, one sanctioned egress; this ADR generalizes that carve-out from *push* to *pull*), [0016](0016-severity-vs-urgency.md) (presentation is a **view, never a gate**, and the engine is **shadow-first** — a read surface cannot become an actuation surface), [0020](0020-signature-continuity.md) (the signing inventory the `signing_inventory` tool exposes), [0025](0025-dashboard-v4-preact-client-render.md) (the read-only, same-origin JSON snapshot this reuses as the tools' data source). **Depends on ADR-0030** (the OIDC token verifier), referenced by number — its file lands on the sibling branch (JEF-483). +- Relates to: [0014](0014-behavioral-telemetry-ebpf.md)/[0015](0015-advisory-evidence-egress.md) (the in-cluster, zero-egress posture this carves a second, bounded exception to), [0018](0018-operator-configured-redacted-breach-notifier.md) (the direct lineage — operator-owned, redacted-by-default, one sanctioned egress; this ADR generalizes that carve-out from *push* to *pull*), [0016](0016-severity-vs-urgency.md) (presentation is a **view, never a gate**, and the engine is **shadow-first** — a read surface cannot become an actuation surface), [0020](0020-signature-continuity.md) (the signing inventory the `signing_inventory` tool exposes), [0025](0025-dashboard-v4-preact-client-render.md) (the read-only, same-origin JSON snapshot this reuses as the tools' data source). **Depends on ADR-0030** (the OIDC token verifier), referenced by number — its file lands on the sibling branch. ## Context @@ -55,7 +55,7 @@ already computes: - **`explain_verdict`** — the *why* behind one entry's verdict (the adjudication reasoning, at the depth the tier permits). - **`get_coverage`** — runtime-coverage / freshness: is protector blind on a node, and - how stale is what it last saw (the [JEF-421](0018-operator-configured-redacted-breach-notifier.md)/JEF-427 signal, read-side). + how stale is what it last saw (the signal, read-side). - **`signing_inventory`** — the [ADR-0020](0020-signature-continuity.md) signing posture: which images are signed, by whom, and where continuity regressed. @@ -127,7 +127,7 @@ The tiers split cleanly along the egress boundary: cluster-specific remains** — no name, no CVE, no path, no topology, only verdicts, counts, technique IDs, and coverage/freshness. It is the same "no untrusted cluster string to leak" property [ADR-0018](0018-operator-configured-redacted-breach-notifier.md)'s - redacted default and its JEF-427 counts-only extension rely on. This tier is **on by + redacted default and its counts-only extension rely on. This tier is **on by default** and needs no per-tier opt-in. - **`forensic` and `raw` are genuine cluster-data egress.** A CVE id, a path, a judgement prompt, a secret name — these are cluster facts. Emitting them is exactly diff --git a/docs/adr/0032-model-is-incident-responder.md b/docs/adr/0032-model-is-incident-responder.md index 2b24eea8..cb96daae 100644 --- a/docs/adr/0032-model-is-incident-responder.md +++ b/docs/adr/0032-model-is-incident-responder.md @@ -16,7 +16,7 @@ decides." The code honors it for the entry lane and violates it downstream in th ways: (1) the adjudication prompt is entry-scoped — a popped pod two hops in is invisible to the judge; (2) `RemotelyExploitable` (reachability + CVE *presence*) and `ActivelyExploited` (a deterministic live signal) both auto-fire via `is_live_corroborated`'s unconditional -`true` for `QuarantineWorkload` — the model is never consulted (see the JEF-284 amendment to +`true` for `QuarantineWorkload` — the model is never consulted (see the amendment to [ADR-0022](0022-quarantine-the-entry-is-the-default-containment.md)); (3) the model emits only a 4-value verdict and chooses no cut (scope is the deterministic `containment_for` precedence). Operator rationale: *"if determinism worked, someone would have solved this @@ -46,7 +46,7 @@ decision itself. The VISION north star names this: the model is the incident res **human-proposal fallback** when the model is unavailable/uncertain (nothing auto-fires without the model). The `is_live_corroborated` unconditional-`true` branch is **deleted**. 6. **Internal-only actively-exploited pods (no internet path) → propose-only** — outside the - north star's two lanes; retires the JEF-284/JEF-322 auto-cut asymmetry in the north-star + north star's two lanes; retires the auto-cut asymmetry in the north-star direction. 7. **Rails unchanged (deterministic):** shadow-default + per-class arming + `enforceScope` ([ADR-0021](0021-two-setting-operating-posture.md)); blast-radius/alive-collateral gate; @@ -54,7 +54,7 @@ decision itself. The VISION north star names this: the model is the incident res zero-egress; fenced/budgeted untrusted text; view-never-gates ([ADR-0016](0016-severity-vs-urgency.md)). -Supersedes **[ADR-0022](0022-quarantine-the-entry-is-the-default-containment.md)'s JEF-284 +Supersedes **[ADR-0022](0022-quarantine-the-entry-is-the-default-containment.md)'s amendment *as a decision procedure*** (the per-pod deterministic bar is no longer the auto-action trigger; internal-only pods become propose-only) — its containment vocabulary, additive/reversible shapes, and precedence ladder survive as the menu's ordering/annotation @@ -62,7 +62,7 @@ and the proposal fallback. **Evolves [ADR-0009](0009-asymmetric-action-bar.md)** adjudicator moves from a one-way veto over a deterministically-selected action to the *selector* of the cut; the `corroborated ∧ adjudicated` auto-gate survives and is extended to the whole path (the `QuarantineWorkload` unconditional auto-fire that bypassed it is removed). -Resolves **JEF-322 / JEF-547**. The responder **judge tier** is deferred to **ADR-0033** +Resolves ****. The responder **judge tier** is deferred to **ADR-0033** pending the extended bakeoff (do **not** assume qwen3:1.7b; expected qwen3:4b). ## Consequences diff --git a/docs/adr/0033-cut-choice-judge-tier.md b/docs/adr/0033-cut-choice-judge-tier.md index 1ce29f1d..04b5efa7 100644 --- a/docs/adr/0033-cut-choice-judge-tier.md +++ b/docs/adr/0033-cut-choice-judge-tier.md @@ -7,7 +7,7 @@ [ADR-0034](0034-cut-choice-contract.md) fixes the cut-choice contract — the model emits `{assessment, reason, contain:[node-key…]}`, determinism resolves each named node to its -narrowest reversible cut — but left one question to be **measured, not assumed** (JEF-568 / +narrowest reversible cut — but left one question to be **measured, not assumed** ( T2b): can the deployed judge (qwen3:1.7b, a 1.7B CPU model) emit it reliably — correct 3-value assessment, exact minimal cut-set, no over-cut — or must the judge escalate to a 4B model? ADR-0034's own premise is that the contract "must be one **1.7b can emit reliably** … escalate @@ -17,15 +17,15 @@ prompt. ## Decision **The judge stays qwen3:1.7b.** A cut-choice bench (`scripts/judge_bakeoff_cutchoice.py`, -JEF-568) scores the ADR-0034 schema on the deployed judge across both evidence directions +) scores the ADR-0034 schema on the deployed judge across both evidence directions (entry loaded-CVE; downstream behavioral / exposed-secret), the minimality centerpiece (clean -entry + live-compromised downstream → contain the downstream *only*), the JEF-588 -downstream-CVE cut trap, and the JEF-402 / broad-RBAC refute traps. On the **deployed pod** +entry + live-compromised downstream → contain the downstream *only*), the +downstream-CVE cut trap, and the / broad-RBAC refute traps. On the **deployed pod** (temp-0, the greedy prod path): - **Assessment: 8/8**, and **every refute/cut trap passes** — 1.7b never over-cuts a clean workload, and correctly returns `no_attack` / `[]` on a downstream loaded-CVE behind a clean - edge (JEF-588) and on broad RBAC / reachable-secret-no-evidence (JEF-402). + edge and on broad RBAC / reachable-secret-no-evidence. - **The cut-set lands with a tightened output instruction.** The first deployed run exposed an *under-cut*: 1.7b recognized the attack but returned `contain=[]` on 3 of 4 real attacks — a recognized breach with no proposed cut. Pinning `contain` to **exactly the evidence-bearing @@ -34,7 +34,7 @@ downstream-CVE cut trap, and the JEF-402 / broad-RBAC refute traps. On the **dep entry-loaded-CVE → `{entry}`; clean-entry + live-downstream → `{downstream}` only (the minimality centerpiece, no entry over-cut); both-evidenced → `{entry, downstream}`. -**That tuned prompt is the one JEF-570 wires into `build_judgment_prompt`.** It is validated on +**That tuned prompt is the one wires into `build_judgment_prompt`.** It is validated on the deployed judge, not guessed. Escalation to 4B is deferred — unnecessary on this evidence, and not authoritatively comparable without first adding 4B to a cluster ollama pod (see Methodology). @@ -58,5 +58,5 @@ for exactly this reason. score and `--flip` over-cut mass, **run on the deployed pod**. - A future escalation to 4B (e.g. if the downstream/pivot lane stresses 1.7b) requires an on-cluster 4B bench first; local numbers do not transfer. -- [JEF-570] wires the `incident/` module (ADR-0034, merged in #296) and this validated prompt +- wires the `incident/` module (ADR-0034, merged in #296) and this validated prompt into `adj_pass` / `reconcile` / the journal. diff --git a/docs/adr/0034-cut-choice-contract.md b/docs/adr/0034-cut-choice-contract.md index fb18d9ca..3d856c95 100644 --- a/docs/adr/0034-cut-choice-contract.md +++ b/docs/adr/0034-cut-choice-contract.md @@ -10,7 +10,7 @@ left its **decision output** sketched as a *menu of mechanisms* (§3: the model `cuts:[menu-id…]` selecting `QuarantineEntry` / `QuarantineWorkload` / `DenyNetworkPath` edge-cut lines). Two things force that open question closed and, on examination, redirect it: -1. **The judge is a 1.7B CPU model, first (JEF-568 re-scope).** The parent plan assumed a 4B +1. **The judge is a 1.7B CPU model, first (re-scope).** The parent plan assumed a 4B judge ("do not assume 1.7b"). Re-scoped: qwen3:1.7b is the deployed judge and passes the current 4-value verdict (14/15 this session, the miss a mislabeled fixture; ADR-0026 12/12). The contract must be one **1.7b can emit reliably** — strict JSON, correct ids, correct @@ -87,7 +87,7 @@ ladder, and entry-exclusion all survive as the resolver + fallback.) 6. **Ledger consumption (strengthened Q5).** `MitigationLedger::reconcile` takes per-entry decisions as input. Desired set = model-chosen cuts whose entry still has a proven - justifying chain (they clear the JEF-566 auto-action gate), **plus** `containment_for` + justifying chain (they clear the auto-action gate), **plus** `containment_for` fallback proposals for every breach-relevant entry with *no current decisive decision* (model unavailable / uncertain / parse-degraded), stamped `adjudicated=false` so they can never auto-apply. The deterministic `quarantine_targets` desired-set insertion in @@ -108,22 +108,22 @@ ladder, and entry-exclusion all survive as the resolver + fallback.) cold-re-judges). Old `Breach` lines replay display-only; entries cold-re-judge for cuts (accepted ~20-min startup cost). -9. **Prompt shape.** Holistic single document, **no few-shot, no numbered procedure** (JEF-134). +9. **Prompt shape.** Holistic single document, **no few-shot, no numbered procedure**. The containment-options section goes **last, immediately before the output instruction** (recency maximizes copy fidelity). The word "quarantine" appears only inside fixed mechanism - strings, never in the instructions (JEF-451 — don't make the cut words the most-primed + strings, never in the instructions (— don't make the cut words the most-primed n-grams). `incident/` module dir keeps every file < 1000 lines. 10. **Transport unchanged; constrained decoding is escalation step 1, not a dependency.** Keep the current call + tolerant parser. If T2b's failing bar is *JSON validity* (not content), the first escalation is Ollama grammar-constrained structured output (native `format` schema), A/B'd like any prompt change. Only if *content* fails does the model tier escalate - (4B → 8B), per JEF-568 → recorded in ADR-0033. + (4B → 8B), → recorded in ADR-0033. ## Consequences -- **T3 (JEF-570)** builds against a fixed target (D1–D9); its Option-A description is - superseded. **T2b (JEF-568)** extends the bakeoff to score assessment (ground truth remapped +- **T3** builds against a fixed target (D1–D9); its Option-A description is + superseded. **T2b** extends the bakeoff to score assessment (ground truth remapped 4→3), cut-set (exact-set primary), the refute traps (incl. downstream-CVE-only must not appear in `contain`), minimality, and **temp-0.8 over-cut mass** (the one metric guards can't backstop), and gates the judge on the deployed 1.7B before wiring. diff --git a/docs/adr/README.md b/docs/adr/README.md index d5d8706a..20fb5965 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -16,18 +16,18 @@ Copy [`0000-template.md`](0000-template.md) to start one. | [0005](0005-attack-objectives.md) | Objectives are ATT&CK outcomes, not just secrets | Accepted | | [0006](0006-build-vs-adopt.md) | Build the substrate; treat KubeHound/IceKube as catalogue and optional provider | Accepted | | [0007](0007-live-cuts-via-adminnetworkpolicy.md) | Live network cuts are additive AdminNetworkPolicy Deny rules | Accepted | -| [0009](0009-asymmetric-action-bar.md) | Asymmetric action bar: live evidence acts, latent exposure proposes | Accepted (amended by 0011, 0013, 0016, 0017, 0022; corroboration made tool-agnostic + per-objective by 0014/JEF-305) | +| [0009](0009-asymmetric-action-bar.md) | Asymmetric action bar: live evidence acts, latent exposure proposes | Accepted (amended by 0011, 0013, 0016, 0017, 0022; corroboration made tool-agnostic + per-objective by 0014) | | [0010](0010-flannel-actuator-workload-isolation.md) | Flannel actuator: quarantine the source with a default-deny NetworkPolicy | Accepted (amended by 0022) | | [0011](0011-positive-judgement.md) | The model corroborates positively; operator access is out of scope, defended in depth | Superseded in part by 0013 | | [0012](0012-exposure-observed-or-declared.md) | Exposure is observed where possible, declared (annotation) where it can't be — tunnels | Accepted | | [0013](0013-proof-winnows-model-decides.md) | Proof winnows the search space; the model makes the exploitability call (positive gate + breach-relevance) | Accepted (amended by 0016) | -| [0014](0014-behavioral-telemetry-ebpf.md) | First-party behavioral telemetry via eBPF, behind a tool-agnostic port (potential vs actual) | Accepted (amended by JEF-305: per-objective corroboration landed; the Retire-Falco parity bar = measured decision-path coverage, retire the adapter not the port) | -| [0015](0015-advisory-evidence-egress.md) | Advisory evidence is mounted-snapshot-only (zero egress); structurally extracted + capped for injection safety | Accepted (advisory feed retired per JEF-242; Rekor egress carve-out amended by 0020) | +| [0014](0014-behavioral-telemetry-ebpf.md) | First-party behavioral telemetry via eBPF, behind a tool-agnostic port (potential vs actual) | Accepted (amended: per-objective corroboration landed; the Retire-Falco parity bar = measured decision-path coverage, retire the adapter not the port) | +| [0015](0015-advisory-evidence-egress.md) | Advisory evidence is mounted-snapshot-only (zero egress); structurally extracted + capped for injection safety | Accepted (advisory feed retired; Rekor egress carve-out amended by 0020) | | [0016](0016-severity-vs-urgency.md) | The breach model: prove chains, enrich them, the model decides and isolates until clear | Accepted (amended by 0017) | | [0017](0017-isolation-persists-on-the-breach-condition.md) | Isolation persists on the breach condition: chain ∧ enrichment fingerprint (revert keys on `entry_fingerprint`) | Accepted | | [0018](0018-operator-configured-redacted-breach-notifier.md) | The breach notifier is the one sanctioned outbound path: operator-configured, off by default, redacted by default | Accepted | -| [0019](0019-dashboard-v3-presentation-architecture.md) | Dashboard v3: server-rendered (maud), zero-egress, light-theme presentation — the view_model/component/page split + the honesty invariants | Accepted (amended by JEF-281: finding detail shows all proven paths; presentation *mechanism* superseded in part by 0025 — IA + honesty axes survive) | -| [0020](0020-signature-continuity.md) | Supply-chain trust is signature continuity: observe every image, learn a per-repo TOFU baseline, treat the signed→unsigned / identity-change regression as the signal — not prefix-gated single-identity (amended: JEF-280 baseline-relative downgrade; JEF-275 build-provenance as a second continuity axis) | Accepted | +| [0019](0019-dashboard-v3-presentation-architecture.md) | Dashboard v3: server-rendered (maud), zero-egress, light-theme presentation — the view_model/component/page split + the honesty invariants | Accepted (amended: finding detail shows all proven paths; presentation *mechanism* superseded in part by 0025 — IA + honesty axes survive) | +| [0020](0020-signature-continuity.md) | Supply-chain trust is signature continuity: observe every image, learn a per-repo TOFU baseline, treat the signed→unsigned / identity-change regression as the signal — not prefix-gated single-identity (amended: baseline-relative downgrade; build-provenance as a second continuity axis) | Accepted | | [0021](0021-two-setting-operating-posture.md) | Two-setting operating posture: `mode` (audit default / enforce) + one `enforceScope` arms all three enforcement surfaces (signature + mesh webhooks + engine live cut), fail-closed webhook selector and actuation RBAC derived from it — no per-surface toggle, no wildcard | Accepted | | [0022](0022-quarantine-the-entry-is-the-default-containment.md) | Quarantine the internet-facing entry is the default containment (entry-only, additive/reversible default-deny); the surgical edge-cut is the refinement used only when it suffices | Accepted | | [0023](0023-delta-aware-adjudication.md) | Delta-aware adjudication: the full cluster state is the context, the change is the question | Accepted | @@ -35,12 +35,12 @@ Copy [`0000-template.md`](0000-template.md) to start one. | [0025](0025-dashboard-v4-preact-client-render.md) | Dashboard v4: a bundled Preact client reconciling from same-origin read-only JSON — supersedes 0019's maud server-render *mechanism* (its IA + honesty axes survive); view_model/props retained as the serde JSON contract, bundle built-from-source + gitignored, honesty stays server-derived | Accepted (its server-rendered strip/nav superseded in part by 0027 — the body is now root-only) | | [0026](0026-adjudication-judge-qwen3-1.7b.md) | Promote qwen3:1.7b as the adjudication judge (bakeoff: 12/12, the only clean sweep of all three evidence types + every refute; deployed qwen2.5:3b-instruct is 11/12, misses exposed-secret-in-field) — pending Pi latency/RAM validation, strict-JSON on-Pi, the delta-aware prompt path, and in-cluster zero-egress availability | Proposed | | [0027](0027-dashboard-root-only-shell-client-strip.md) | Dashboard: the server emits a ROOT-ONLY shell (`` + `#dash-root`); the status strip + tab nav move to the Preact client — supersedes 0025's server-rendered strip/nav. Honesty preserved (blank ≠ green; the all-clear/watching/`judging-state` tokens stay server-derived). Also fixes the reversed-args `setInterval` bug (dead poll + blank tab-swaps + CSP eval violation) with the CSP kept strict; SSR/hydration deferred | Accepted | -| [0028](0028-dashboard-client-local-state-simplification.md) | Dashboard client: local state by default — `App` holds the 5 shared fields (+ the callback-decoupled poll) as plain `useState`, the hand-rolled store + reconcile tombstone are deleted, expansion/disclosure is local & ephemeral (native `
`; sessionStorage persistence dropped), keyed removal replaces the tombstone (a future cleared-cue is server-shipped), and the npm deps prune to build+test only (zero runtime). Extends 0025/0027 (both stand); the JEF-408 poll/CSP fix + JEF-410 strip persistence + server-derived honesty are retained | Accepted | +| [0028](0028-dashboard-client-local-state-simplification.md) | Dashboard client: local state by default — `App` holds the 5 shared fields (+ the callback-decoupled poll) as plain `useState`, the hand-rolled store + reconcile tombstone are deleted, expansion/disclosure is local & ephemeral (native `
`; sessionStorage persistence dropped), keyed removal replaces the tombstone (a future cleared-cue is server-shipped), and the npm deps prune to build+test only (zero runtime). Extends 0025/0027 (both stand); the poll/CSP fix + strip persistence + server-derived honesty are retained | Accepted | | [0029](0029-adjudication-verdict-is-authoritative.md) | The adjudicating model's verdict is authoritative: NO deterministic guards that override/second-guess its breach judgement, and NO capping/summarizing the evidence to steer it — the full enriched chain goes to the model and its call stands. Rare false-`exploitable` flips on huge borderline entries are a temp-0 tail event (diagnosed: not a config/model/prompt bug), accepted as a known cost under shadow-first; addressed only at the model layer (bakeoff-evaluated), never a verdict gate. Reaffirms 0013 (model decides) + 0016 (shadow-first). Does not remove the anti-fabrication `guard_fabricated_cve` (output grounding, not a judgement override) | Accepted | | [0030](0030-app-level-oidc-verification-supersedes-edge-trust.md) | App-level, provider-agnostic OIDC verification supersedes edge-only trust: the dashboard + `/api/*.json` today carry only CSP (auth trusted entirely at the Cloudflare Access edge), so one `kubectl port-forward` reads the whole graph. Protector becomes an OAuth RESOURCE SERVER (verifies sig/iss/aud/exp/nbf, alg pinned to the issuer's asymmetric family — never from the token's own `alg`; extracts subject + a configurable tier) against a CONFIGURABLE issuer; it ships NO IdP (Dex/Keycloak out of scope; supersedes the in-cluster-Dex idea) and cannot actuate. ID-JAG is just a JWT the same verifier accepts. The JWKS/discovery GET is the ADR-0015 same-request-for-everyone lane (public keys in, no cluster datum out), not an egress breach. FAIL-CLOSED when configured (every error → 401/403/503, JWKS-unreachable is 503 not bypass); UNCONFIGURED behaves as today but logs loudly — the only bypass, and it announces itself. CF Access already issues a verifiable JWT, so pointing the verifier at the CF issuer closes the hole with no new interactive flow. Reaffirms 0016 (auth gates WHO MAY VIEW, not the verdict path) | Proposed | | [0031](0031-read-only-mcp-server-tiered-redaction.md) | A read-only, tiered-redaction MCP server — the second sanctioned egress carve-out (pull-side sibling of 0018's push notifier). Four read-only tools (`list_findings` / `explain_verdict` / `get_coverage` / `signing_inventory`); NO actuation tool exists by construction (view-never-a-gate / shadow-first, 0016). Three tiers `redacted`/`forensic`/`raw` with the tier a server-enforced CEILING from a verified ADR-0030 claim (arg may only narrow) — `redacted` safe-by-construction, `forensic`/`raw` off-by-default, opt-in, journaled operator-owned egress; secret VALUES have no unlock tier. Redaction is server-side/in-cluster BEFORE egress, so protector IS the remote HTTP MCP server (reusing 0018's lifted scrubbers). Transport: RMCP behind our OIDC verifier (single-pathed, in-tree trust decisions; hand-rolled JSON-RPC fallback if it won't compose), ID-JAG protected-resource discovery for zero-touch enterprise auth. Depends on 0030 | Proposed | -| [0032](0032-model-is-incident-responder.md) | The model is the **incident responder**: over the whole internet-facing path it decides what is an attack and which cut to apply — choosing from a menu that determinism ENUMERATES — at minimum scope. Determinism proves/enriches/feeds/bounds; it does NOT decide the cut. The model now SEES per-node downstream evidence (was entry-only); the `is_live_corroborated` unconditional `QuarantineWorkload` auto-fire is deleted; internal-only actively-exploited pods become propose-only. Supersedes 0022's JEF-284 decision procedure (its vocabulary/shapes/ladder survive as the menu + proposal fallback); evolves 0009 (adjudicator veto → cut selector); resolves JEF-322/547. Judge tier deferred to 0033 (do NOT assume qwen3:1.7b; expected qwen3:4b). Realizes the VISION north star | Proposed | -| [0034](0034-cut-choice-contract.md) | The **cut-choice contract**: refines 0032 §3 from a *mechanism-menu* to **target-choice** — the model outputs `{assessment: attack/no_attack/uncertain, reason, contain:[node-key…]}`, naming the compromised on-path nodes; determinism resolves each to its narrowest legal cut (the `containment_for` ladder / `QuarantineWorkload`), because per-target minimality is monotone/deterministic (no judgment to delegate) and a node-key is what a 1.7B can reliably copy from attended evidence. Collapses the 4-value verdict → 3-value assessment. Menu rendered as advisory input (mechanism + blast-radius); guards (menu-membership + per-node containment-grounding) downgrade to Uncertain, never Refuted; skeptic default is inert both ways (a model outage neither cuts nor lifts a standing cut); journal v2 stores the resolved `cut_signature`+fingerprint so replay can't repoint. Chosen for 1.7B-viability (JEF-568 re-scope: 1.7b-first); residual risk is grounded over-cut, measured by the T2b temp-0.8 bench. Supersedes 0032 §3 | Proposed | +| [0032](0032-model-is-incident-responder.md) | The model is the **incident responder**: over the whole internet-facing path it decides what is an attack and which cut to apply — choosing from a menu that determinism ENUMERATES — at minimum scope. Determinism proves/enriches/feeds/bounds; it does NOT decide the cut. The model now SEES per-node downstream evidence (was entry-only); the `is_live_corroborated` unconditional `QuarantineWorkload` auto-fire is deleted; internal-only actively-exploited pods become propose-only. Supersedes 0022's decision procedure (its vocabulary/shapes/ladder survive as the menu + proposal fallback); evolves 0009 (adjudicator veto → cut selector); realizes the north-star model-decides-the-cut shift. Judge tier deferred to 0033 (do NOT assume qwen3:1.7b; expected qwen3:4b). Realizes the VISION north star | Proposed | +| [0034](0034-cut-choice-contract.md) | The **cut-choice contract**: refines 0032 §3 from a *mechanism-menu* to **target-choice** — the model outputs `{assessment: attack/no_attack/uncertain, reason, contain:[node-key…]}`, naming the compromised on-path nodes; determinism resolves each to its narrowest legal cut (the `containment_for` ladder / `QuarantineWorkload`), because per-target minimality is monotone/deterministic (no judgment to delegate) and a node-key is what a 1.7B can reliably copy from attended evidence. Collapses the 4-value verdict → 3-value assessment. Menu rendered as advisory input (mechanism + blast-radius); guards (menu-membership + per-node containment-grounding) downgrade to Uncertain, never Refuted; skeptic default is inert both ways (a model outage neither cuts nor lifts a standing cut); journal v2 stores the resolved `cut_signature`+fingerprint so replay can't repoint. Chosen for 1.7B-viability (re-scope: 1.7b-first); residual risk is grounded over-cut, measured by the T2b temp-0.8 bench. Supersedes 0032 §3 | Proposed | | [0036](0036-break-glass-disarm.md) | Disarm is a real, fast kill switch: the self-revert loop now reverts a standing cut when its OWN action class is no longer armed (not just on health/chain retirement, closing the enforce→audit orphaned-cut gap); a break-glass flag file (fixed mount path, presence-only, no content parsed) clamps actuation to dry-run and drives every standing cut to revert within one pass, with no image rebuild and no GitOps sync — narrows only, never arms wider than `mode`/`enforceScope` already permit. Chosen over a local admin endpoint: no new listener/auth surface, keeps working even if the dashboard/mesh/OIDC path is itself down | Accepted | | [0037](0037-shadow-bake-arm-readiness.md) | Shadow-bake arm-readiness: the human-read exit criterion (bake duration, zero unexplained model-over-cut on a clean workload, 0033's bench threshold, a model-under-cut spot-check, continuous coverage) an operator reads from the model-vs-deterministic cut-divergence comparator before the single ADR-0021 `enforce` flip, narrowest ADR-0035 rung first — the comparator only informs; nothing here or in code auto-arms | Proposed | diff --git a/docs/alerts.md b/docs/alerts.md index c3644c01..7133e1ec 100644 --- a/docs/alerts.md +++ b/docs/alerts.md @@ -18,7 +18,7 @@ specifically so "the judge went quiet" and "the engine cut something" are never These sit alongside the existing model-health instruments, useful for the SAME alert group: `protector.engine.model_calls{result="unavailable"}` (a model call came back -inconclusive), `protector.engine.skipped` (a re-judge was skipped for breaker/backoff — JEF-234), +inconclusive), `protector.engine.skipped` (a re-judge was skipped for breaker/backoff —), and `protector.engine.model_latency_ms` (the model's response-time tail). ## Example PromQL rules diff --git a/docs/ebpf-testing-on-nodes.md b/docs/ebpf-testing-on-nodes.md index a24d8fad..e36428f5 100644 --- a/docs/ebpf-testing-on-nodes.md +++ b/docs/ebpf-testing-on-nodes.md @@ -45,7 +45,7 @@ module declaring only the structs the probes read, each field placed at its runn byte offset. It is NOT the full `aya-tool` dump (that exceeded the 1,000-line file cap and silently rotted across kernel upgrades). Critically there is **no CO-RE field relocation** here — the bpf object bakes each field access as a constant offset — so the offsets in that -file MUST match the fleet kernel or `bpf_d_path` is verifier-rejected (JEF-324). Re-verify +file MUST match the fleet kernel or `bpf_d_path` is verifier-rejected. Re-verify on any kernel struct change by dumping BTF from a node (`kubectl` a hostPath-`/sys/kernel/ btf` pod, then `bpftool btf dump … format c`, or parse the raw BTF) on **every** fleet arch and confirming the read fields share one offset. As of 2026-07-05 the fleet is `7.0.0` diff --git a/docs/ideas/cut-choice-contract.md b/docs/ideas/cut-choice-contract.md index ba044ca6..e6ecf698 100644 --- a/docs/ideas/cut-choice-contract.md +++ b/docs/ideas/cut-choice-contract.md @@ -1,8 +1,8 @@ # Idea — the incident-responder cut-choice contract **Status:** decided (2026-07-28). Realized by [ADR-0034](../adr/0034-cut-choice-contract.md); -refines [ADR-0032](../adr/0032-model-is-incident-responder.md) §3. Sprint tickets: JEF-568 -(bench), JEF-570 (build), JEF-569 (shadow+arm), plus the new `incident/` module chunk. +refines [ADR-0032](../adr/0032-model-is-incident-responder.md) §3. Sprint tickets: +(bench) (build) (shadow+arm), plus the new `incident/` module chunk. ## Idea @@ -14,10 +14,10 @@ keystone left open. Settle the "minimality fork" so T3 can build it and T2b can ADR-0032 commits protector to "the model is the incident responder" but left the decision contract's exact shape open, sketched as a *menu of mechanisms* (Option A). The re-scoped -constraint (JEF-568: **qwen3:1.7b-first**, escalate only on measured failure) invalidates the +constraint (**qwen3:1.7b-first**, escalate only on measured failure) invalidates the parent idea's "plan of record: 4B." The contract must be one a **1.7B CPU judge can emit reliably**, or the refactor lands unarmed. Everything around it is settled: per-node -downstream evidence in the prompt (JEF-565, live), the uniform auto-action gate (JEF-566, +downstream evidence in the prompt (live), the uniform auto-action gate ( live in `respond/mod.rs::is_live_corroborated`), the delta/cache gate (ADR-0023), grounding-guard doctrine (ADR-0029). Only the decision output and its consumers remain. @@ -36,7 +36,7 @@ grounding-guard doctrine (ADR-0029). Only the decision output and its consumers zero authority gained. 3. **"1.7b can produce the cut contract" — shaky but shapeable.** It provably does the 4-value verdict (14/15 this session; ADR-0026 12/12). Its documented failures are n-gram parroting - of primed instruction phrases (JEF-134, JEF-451) and size-correlated tail-flips at temp>0 + of primed instruction phrases and size-correlated tail-flips at temp>0 (ADR-0029). Both hit an *opaque-id, mechanism-comparing* output (A) far harder than a *copy-the-node-key-you-just-analyzed* output (B). Choose the contract that sits inside what it provably does; T2b decides whether it holds. @@ -98,6 +98,6 @@ deployed 1.7B; it is the honest gate on "does 1.7B hold or do we escalate." ## Handoff -Tickets already exist (JEF-568/570/569) — this brief **reconciles** them to B rather than +Tickets already exist for this work — this brief **reconciles** them to plan B rather than creating new ones; the one addition is the pure `incident/` module as the unblocked first -chunk. JEF-570's Option-A description is superseded by ADR-0034. +chunk. The original Option-A description is superseded by ADR-0034. diff --git a/docs/ideas/model-as-incident-responder.md b/docs/ideas/model-as-incident-responder.md index bbbb84d6..ef74ed6d 100644 --- a/docs/ideas/model-as-incident-responder.md +++ b/docs/ideas/model-as-incident-responder.md @@ -1,7 +1,7 @@ # Idea — The model as incident responder **Status:** design brief (→ ADR-0032 Proposed; → `/plan-sprint`). Realizes the -[VISION.md](../VISION.md) north star. Resolves JEF-547 (supersedes the JEF-322 pivot +[VISION.md](../VISION.md) north star. Resolves (supersedes the pivot asymmetry). **Idea in one line:** make the local model the incident responder over the whole @@ -55,7 +55,7 @@ and the model's decision is a *closed-vocabulary selection*, guardable by member - **What the model sees** (one incident document per internet-facing entry; the whole prompt is the cache key): the entry's calibrated content unchanged; **one evidence block per downstream workload on a proven path** (same `entry_evidence`/`entry_findings` accessors — - they already work for any node; same JEF-453 reachable-CVE filter, same fencing/budgets, + they already work for any node; same reachable-CVE filter, same fencing/budgets, now per-node with a per-incident aggregate cap); clean path nodes get a one-line "no evidence observed"; the objectives list unchanged; and **the action menu** — each legal cut as a line with a content-derived id, mechanism, target, and a deterministic @@ -76,12 +76,12 @@ and the model's decision is a *closed-vocabulary selection*, guardable by member zero-egress; fenced/budgeted untrusted text; view-never-gates (ADR-0016). The model chooses *what*; the rails bound *how*. - **Internal-only actively-exploited pods (no internet path) → propose-only** — outside the - north star's two lanes; retires the JEF-322/JEF-284 auto-cut asymmetry. + north star's two lanes; retires the auto-cut asymmetry. ### Model capacity — the plain answer **Do not ship the responder contract on qwen3:1.7b, and do not assume it.** The 1.7B's -documented failure modes — n-gram parroting (JEF-134), tag fabrication (JEF-451), and +documented failure modes — n-gram parroting, tag fabrication, and tail-flips that *grow with prompt size* — are exactly what a bigger whole-path prompt plus a structured decision output will amplify. **Plan of record: qwen3:4b-class** (on the 32GB CPU minis RAM is a non-issue — 4B Q4 ≈ 2.6GB, even 14B fits; latency is the only cost, ~15–25s @@ -96,7 +96,7 @@ validation) decides the final pick and is recorded in **ADR-0033**; the design a bounds and does not decide the cut; incident-scoped prompt; menu-choice decision contract; deletes the `QuarantineWorkload` unconditional auto-fire; demotes `containment_for` to a human-proposal fallback; internal-only live-alert → propose-only. Supersedes ADR-0022's - JEF-284 decision procedure (its containment vocabulary/shapes/ladder survive as the menu); + decision procedure (its containment vocabulary/shapes/ladder survive as the menu); evolves ADR-0009's adjudicator from veto → cut-selector. 2. **Menu-choice, not free-form actions** — closed, engine-rendered cut vocabulary; membership guards are grounding-class under ADR-0029. diff --git a/docs/model-calibration.md b/docs/model-calibration.md index b7c13cd2..3777c808 100644 --- a/docs/model-calibration.md +++ b/docs/model-calibration.md @@ -26,7 +26,7 @@ The model must, on cluster-representative cases, get the call right: → **refute** — same principle: a scary OUTCOME being reachable is not the outcome being exploited. -The principle (JEF-134): the deterministic layer PROVES + ENRICHES — reachability, the +The principle: the deterministic layer PROVES + ENRICHES — reachability, the `[RBAC-GRANTED]` / `[MOUNTED]` / `[same-ns]` / `[cross-ns]` reach tags, and the CVE / runtime evidence — and the **model decides breach holistically** from the *conjunction* of reachability and evidence. Neither half alone is a breach: authorized-but-unevidenced @@ -38,9 +38,9 @@ longer pre-decides via deterministic "promotion grounds" (those mis-gated ArgoCD is not a decision gate. See the holistic prompt in `build_judgment_prompt` (`engine/src/engine/reason/adjudicate.rs`). -### Edge vs. downstream — the exploitability path is not the same evidence bar (JEF-565/JEF-567) +### Edge vs. downstream — the exploitability path is not the same evidence bar -JEF-565 gave the model its own evidence block for every workload on an entry's *proven* path, + gave the model its own evidence block for every workload on an entry's *proven* path, not just the entry — but the two positions in the chain are not judged identically: - **Edge (the internet-facing entry) — the CVE-exploitability path.** A critical CVE observed @@ -57,7 +57,7 @@ not just the entry — but the two positions in the chain are not judged identic - `downstream_only_cve` — a downstream hop with a loaded-at-runtime CVE and *no* behavioral evidence, behind a clean edge → **refute**. This is a deliberate keep-honest trap: the model must not over-promote on downstream reachability + a loaded CVE alone. (Whether a - downstream CVE like this is exploitable via some other proxy/exposure path is the JEF-587 + downstream CVE like this is exploitable via some other proxy/exposure path is the problem — deferred, not judged here.) - `downstream_behavioral_compromise` — a downstream hop with an alert / hands-on-keyboard signal and no CVE of its own, behind a clean edge → **exploitable**, the same bar as a @@ -65,7 +65,7 @@ not just the entry — but the two positions in the chain are not judged identic - `downstream_clean_marker` — a downstream hop explicitly checked with nothing found → **refute**. -> **Recalibration gate (follow-up — JEF-50 arming, not the engine change):** removing the +> **Recalibration gate (follow-up — arming, not the engine change):** removing the > deterministic grounds makes "is argo a breach" the *model's* call, so whether the prod > model (granite4:3b-h) decides correctly under the holistic prompt is verified by the > bake-off + the `#[ignore]`d e2e gate below — a follow-up gate on arming a class, **not** @@ -98,7 +98,7 @@ The `#[ignore]`d e2e test in `engine/src/engine/reason/adjudicate.rs` drives the judgement path (`build_judgment_prompt` → the model → `parse_verdict`) end-to-end against a live endpoint, and **hard-asserts the anchor cases**: log4shell on a reachable internet-facing entry → `Exploitable`; the same chain with no CVE / no runtime evidence -(own-app `[MOUNTED]` secret) → `Refuted`; and the JEF-134 argo anchor — an internet-facing +(own-app `[MOUNTED]` secret) → `Refuted`; and the argo anchor — an internet-facing controller RBAC-granted secrets across many tenants (broad, some high-impact) with no CVE and no behavior → `Refuted`. It fails the build if the candidate misses any, so it is a real gate when run, not just a print. @@ -126,10 +126,10 @@ pointed at the candidate.) ## Follow-ups (not yet implemented) - **Circuit breaker** around the model call (trip after sustained failures / timeouts so a - degraded endpoint stops being retried every pass). Deferred from JEF-109 as a larger + degraded endpoint stops being retried every pass). Deferred from as a larger change; the bounded client timeout + the `protector.engine.model_client_fallback` and `model_calls{result=unavailable}` metrics are the current backstops. -- **Prompt text for the edge/downstream split (JEF-567 follow-up):** `build_judgment_prompt`'s +- **Prompt text for the edge/downstream split (follow-up):** `build_judgment_prompt`'s "Downstream evidence" paragraph still tells the model a downstream CVE observed loading-at-runtime is exploitation evidence "exactly as if it were on the entry" — the SAME bar as an edge CVE. This doc's edge/downstream framing above (and the `downstream_only_cve` diff --git a/engine/examples/dashboard_preview/fixtures.rs b/engine/examples/dashboard_preview/fixtures.rs index a4b8abcb..b203957c 100644 --- a/engine/examples/dashboard_preview/fixtures.rs +++ b/engine/examples/dashboard_preview/fixtures.rs @@ -78,7 +78,7 @@ pub(crate) fn breach_finding() -> Finding { evidence, recency: None, node: None, - // A model-chosen cut-set (ADR-0034 / JEF-674): the entry front door plus the downstream + // A model-chosen cut-set (ADR-0034): the entry front door plus the downstream // workload it pivots through — demonstrates the finding detail's cut-set list. incident: Some(IncidentSummary { assessment: Assessment::Attack, @@ -123,7 +123,7 @@ pub(crate) fn simple_finding(entry: &str, objective: &str) -> Finding { } } -/// A wide, NO-CUT finding (JEF-281): an internet-facing front door reaching one secret via TWO +/// A wide, NO-CUT finding: an internet-facing front door reaching one secret via TWO /// redundant backends, so no single edge severs the objective. Showcases the multi-path detail — /// both proven paths stacked, and the "reachable via N redundant paths" reason line. pub(crate) fn redundant_finding() -> Finding { diff --git a/engine/examples/dashboard_preview/main.rs b/engine/examples/dashboard_preview/main.rs index 4c29ac8c..f73208ec 100644 --- a/engine/examples/dashboard_preview/main.rs +++ b/engine/examples/dashboard_preview/main.rs @@ -27,7 +27,7 @@ //! //! This changes NOTHING about the shipped `serve_dashboard` or the repo's `dashboard.js`. //! -//! Split into a module directory per the CLAUDE.md 1,000-line file cap (JEF-562), decomposed by +//! Split into a module directory per the CLAUDE.md 1,000-line file cap, decomposed by //! preview scenario/section: [`fixtures`] (shared finding skeletons), [`sample_data`] (shared //! journal/policy-log/bake/readiness fixtures), [`scenarios`] (one submodule per honesty state), //! [`render`] (the public render-path calls), [`server`] (the axum handlers), and [`samples`] diff --git a/engine/examples/dashboard_preview/render.rs b/engine/examples/dashboard_preview/render.rs index b8d310f7..7b564f30 100644 --- a/engine/examples/dashboard_preview/render.rs +++ b/engine/examples/dashboard_preview/render.rs @@ -72,7 +72,7 @@ fn preview_admission(state: &DashboardState) -> view_model::props::AdmissionView view_model::build_admission_view(preview_strip(state), &state.policy_log.snapshot()) } -/// Build the "Access" view props (JEF-490) through the public render path — a raw-tier preview +/// Build the "Access" view props through the public render path — a raw-tier preview /// caller over the scenario's (empty) audit sink, so the preview exercises the same builder /// production serves. fn preview_access(state: &DashboardState) -> view_model::props::AccessViewProps { @@ -84,7 +84,7 @@ fn preview_access(state: &DashboardState) -> view_model::props::AccessViewProps ) } -/// Render the ROOT-ONLY document shell for a tab through the dashboard's PUBLIC render path (JEF-408, +/// Render the ROOT-ONLY document shell for a tab through the dashboard's PUBLIC render path ( /// superseding ADR-0025's server-rendered strip/nav): the `` + the Preact `#dash-root` mount. /// ALL body HTML — the status strip, the tab nav, and the view body — is client-rendered from the /// `/api/{tab}.json` snapshot (served below), so this preview exercises the SAME path production serves. diff --git a/engine/examples/dashboard_preview/sample_data.rs b/engine/examples/dashboard_preview/sample_data.rs index b70a3e8d..79484dc6 100644 --- a/engine/examples/dashboard_preview/sample_data.rs +++ b/engine/examples/dashboard_preview/sample_data.rs @@ -182,8 +182,8 @@ pub(crate) fn sample_policy_log() -> Arc { Arc::new(log) } -/// Seed the signing sweep's per-image observation rows (JEF-261 shape) so the Admission tab's -/// signing inventory (JEF-262) renders every posture: a GitHub Actions keyless signature, a +/// Seed the signing sweep's per-image observation rows (shape) so the Admission tab's +/// signing inventory renders every posture: a GitHub Actions keyless signature, a /// human/Google-issued signature, an invalid signature (loud), a plain not-signed (calm), and a /// transient checking. Keyed `Image/` with the posture in the `signature` word + `reason` /// prose, exactly as `engine::signing_sweep` records them. @@ -222,7 +222,7 @@ fn record_signing_inventory(log: &PolicyDecisionLog) { "checking", "signing posture not yet known (registry/log unreachable)", ); - // A signing-regression finding (JEF-264): the api-gateway repo — with an established signed + // A signing-regression finding: the api-gateway repo — with an established signed // history — is now signed by a NEW identity (the push-access-compromise signal). Audit-only: // the image is still admitted; the loud banner surfaces before→after in full. log.record(PolicyDecisionRecord::now( @@ -237,7 +237,7 @@ fn record_signing_inventory(log: &PolicyDecisionLog) { via https://token.actions.githubusercontent.com | before: \ https://github.com/acme/api-gateway/.github/workflows/release.yaml@refs/tags/v1.8.2", )); - // An "exception accepted" (JEF-265): the export repo legitimately rotated its signer, and the + // An "exception accepted": the export repo legitimately rotated its signer, and the // operator opted THAT drift out via a scoped, recorded exception. Rendered CALM + distinctly // labelled "exception accepted" (never green-cleared), kept visible, never counted as breach. sweep( diff --git a/engine/examples/dashboard_preview/scenarios/breach.rs b/engine/examples/dashboard_preview/scenarios/breach.rs index f8787a1e..1baff86a 100644 --- a/engine/examples/dashboard_preview/scenarios/breach.rs +++ b/engine/examples/dashboard_preview/scenarios/breach.rs @@ -26,7 +26,7 @@ pub(super) fn build_breach() -> DashboardState { let mut rows: Vec = vec![ // BREACH — internet-facing, proven multi-hop, KEV CVE, runtime alert, proposed cut. breach_finding(), - // NO-CUT — one secret reachable via two redundant backends (JEF-281 multi-path view). + // NO-CUT — one secret reachable via two redundant backends (multi-path view). redundant_finding(), // AWAITING — a breach-relevant entry the model has not yet reached (no verdict). simple_finding( @@ -97,7 +97,7 @@ pub(super) fn build_breach() -> DashboardState { } // UNCERTAIN: a model-timeout verdict. Its posture is `Unknown`, never `Safe` — an - // inconclusive read is never green (JEF-302 honesty). + // inconclusive read is never green (honesty). let uncertain = "deployment/web/storefront"; verdicts.set_display( uncertain, diff --git a/engine/examples/dashboard_preview/scenarios/watching.rs b/engine/examples/dashboard_preview/scenarios/watching.rs index 86826789..cbfa0b5c 100644 --- a/engine/examples/dashboard_preview/scenarios/watching.rs +++ b/engine/examples/dashboard_preview/scenarios/watching.rs @@ -74,7 +74,7 @@ pub(super) fn build_watching() -> DashboardState { armed: false, tuf_cache_age_secs: Some(3 * 60 * 60), unverifiable_spike: false, - checking_images: 2, // degraded — two images stuck 'checking' (JEF-326). + checking_images: 2, // degraded — two images stuck 'checking'. }); findings.set_model_health(ModelHealth::Ok); findings.mark_pass(SystemTime::now()); diff --git a/engine/src/engine/adj_gate.rs b/engine/src/engine/adj_gate.rs index a4f6ce4e..3bb71619 100644 --- a/engine/src/engine/adj_gate.rs +++ b/engine/src/engine/adj_gate.rs @@ -3,20 +3,20 @@ //! under the file-size cap (CLAUDE.md) and to hold the layered gate in one readable place. //! //! The gate layers, in order (first match wins): -//! 1. **Exact-fingerprint LRU hit (JEF-390)** — the model's input is byte-identical to a +//! 1. **Exact-fingerprint LRU hit** — the model's input is byte-identical to a //! recently-judged state; serve that decisive verdict, no model call. -//! 2. **Purely-subtractive delta hold (ADR-0023, JEF-391)** — a fingerprint miss but nothing was +//! 2. **Purely-subtractive delta hold (ADR-0023)** — a fingerprint miss but nothing was //! ADDED to the entry's surface since its last DECISIVE verdict (something was only removed — //! a pod vanished, a peer aged out). The prior decisive verdict still holds (its surface only //! shrank; removal can only reduce breach risk), so serve it without a fresh call. This is //! what stops the ephemeral-churn ping-pong at its root. Fails toward re-judging: a //! non-additive delta always has a baseline (a missing baseline is additive → first judgment), //! so a stray absent baseline re-judges rather than skips. -//! 3. **Breaker / backoff skip (JEF-234)** — the model looks down (global breaker) or this entry +//! 3. **Breaker / backoff skip** — the model looks down (global breaker) or this entry //! is in inconclusive-adjudication backoff; synthesize an Uncertain and send nothing. //! 4. Otherwise **re-judge** — a genuine cache miss with new (additive) surface. //! -//! **Positive re-verify (JEF-445):** layers 1 and 2 do NOT apply to the model's own positive +//! **Positive re-verify:** layers 1 and 2 do NOT apply to the model's own positive //! (`Exploitable`) — it is always re-verified against the live model (falling to layer 3/4). That //! verdict is the one the temp-0 judge occasionally fabricates (the argocd loaded-at-runtime //! tail-flip, accepted at the model layer per ADR-0029); serving it from cache would freeze a @@ -31,14 +31,14 @@ use super::{Engine, PendingEntry, reason}; /// The classification outcome for one entry this pass — decided WITHOUT calling the model. #[cfg_attr(test, derive(Debug))] pub(super) enum AdjGate { - /// Serve a decisive verdict with no model call: an exact-fingerprint LRU hit (JEF-390) or a - /// purely-subtractive delta hold (JEF-391). `held` is true only for the delta hold, so the + /// Serve a decisive verdict with no model call: an exact-fingerprint LRU hit or a + /// purely-subtractive delta hold. `held` is true only for the delta hold, so the /// pass log can show how much churn the delta gate absorbed. Resolved { verdict: reason::adjudicate::Verdict, held: bool, }, - /// Skip the model this pass and carry the prior display forward (JEF-234 breaker / backoff). + /// Skip the model this pass and carry the prior display forward (breaker / backoff). Skipped(reason::adjudicate::Verdict), /// Queue for a fresh model call — a genuine re-judge. Judge, @@ -47,13 +47,13 @@ pub(super) enum AdjGate { impl Engine { /// Build one breach-relevant entry's [`PendingEntry`] for this pass: read its delta-aware /// baseline (ADR-0023), build the model's complete prompt WITH the "Changes since…" delta - /// section, derive the verdict-cache key from that prompt (JEF-350) and the churn fingerprints - /// (JEF-387), and project this pass's surface (snapshotted as the next baseline on a decisive + /// section, derive the verdict-cache key from that prompt and the churn fingerprints + /// and project this pass's surface (snapshotted as the next baseline on a decisive /// verdict). Returns the pending record, whether the delta since the baseline is ADDITIVE /// (re-judge) vs subtractive (the prior verdict holds), and the baseline itself (the gate /// serves its verdict on a subtractive hold). Built before the cache lookup so the cached-on /// and sent prompt bytes can never drift. - // 9 args (JEF-570 added `menu`): each is a distinct, already-computed piece of this pass's + // 9 args (added `menu`): each is a distinct, already-computed piece of this pass's // per-entry state (no natural sub-grouping that wouldn't just be a wrapper struct for its // own sake — see the same call already made in `run_loop.rs`/`supply_chain/mod.rs`). #[allow(clippy::too_many_arguments)] @@ -81,7 +81,7 @@ impl Engine { // The verdict-cache key is the FULL-STATE hash (excludes the "Changes since…" section) so // an identical full state always keys identically regardless of the delta — see // `build_delta_prompt_asn` for why (ADR-0023's fingerprint↔delta-gate resolution). The - // menu is part of that full state (JEF-570): a mapping change is a prompt change is a + // menu is part of that full state: a mapping change is a prompt change is a // re-judge (ADR-0034 D4). let fingerprint = delta.cache_key; let chain = reason::adjudicate::chain_shape_hash(&objectives); @@ -107,7 +107,7 @@ impl Engine { /// [`state::VerdictStore`] — directly unit-testable without a full engine. `additive` and /// `baseline` come from the delta build (ADR-0023): `additive` is false only when a decisive /// baseline exists AND nothing was added since it. `now` is the pass's single injected clock -/// (shared with the JEF-234 backoff). The subtractive-hold path warms the LRU under the current +/// (shared with the backoff). The subtractive-hold path warms the LRU under the current /// fingerprint so the settled steady state HITS next pass. pub(super) fn classify_adjudication( verdicts: &state::VerdictStore, @@ -118,7 +118,7 @@ pub(super) fn classify_adjudication( ) -> AdjGate { use reason::adjudicate::Verdict; // The model's OWN positive verdict — `Exploitable` — is never served from the cache or the - // subtractive hold: it is re-verified against the live model every pass (JEF-445). It is the + // subtractive hold: it is re-verified against the live model every pass. It is the // one verdict the temp-0 judge occasionally FABRICATES (the argocd loaded-at-runtime tail-flip, // accepted at the model layer per ADR-0029); replaying it from cache would FREEZE a one-time // flip into a permanent false breach, replayed every time the entry's oscillating surface @@ -129,7 +129,7 @@ pub(super) fn classify_adjudication( // let a model `Refuted` veto a live attack. let must_reverify = |v: &Verdict| matches!(v, Verdict::Exploitable(_)); - // 1. Exact-fingerprint LRU hit (JEF-390): byte-identical input, serve the cached verdict — + // 1. Exact-fingerprint LRU hit: byte-identical input, serve the cached verdict — // unless it is a positive that must be re-verified (fall through to the re-judge path). if let Some(verdict) = verdicts.cached_for(&pending.entry_key, &pending.fingerprint) && !must_reverify(&verdict) @@ -139,7 +139,7 @@ pub(super) fn classify_adjudication( held: false, }; } - // 2. Purely-subtractive / unchanged delta since a decisive baseline (JEF-391): the prior + // 2. Purely-subtractive / unchanged delta since a decisive baseline: the prior // verdict holds — again unless it is a positive that must be re-verified. `!additive` // implies a baseline exists; a defensive absent baseline falls through to a re-judge (never // suppress a judgment on possibly-new surface). @@ -157,7 +157,7 @@ pub(super) fn classify_adjudication( held: true, }; } - // 3. JEF-234 breaker / backoff: the model looks down — skip and carry the display forward. + // 3. breaker / backoff: the model looks down — skip and carry the display forward. if verdicts.breaker_open(now) { return AdjGate::Skipped(Verdict::Uncertain( "model unavailable (breaker open)".into(), diff --git a/engine/src/engine/adj_gate_tests.rs b/engine/src/engine/adj_gate_tests.rs index f5678576..e09d958e 100644 --- a/engine/src/engine/adj_gate_tests.rs +++ b/engine/src/engine/adj_gate_tests.rs @@ -1,4 +1,5 @@ -//! Tests for the layered adjudication re-judge gate (ADR-0023 / JEF-391, over JEF-390 / JEF-234). +//! Tests for the layered adjudication re-judge gate (ADR-0023, over the fingerprint-LRU and +//! breaker/backoff layers). //! `classify_adjudication` reads only the verdict store, so these drive it directly with a real //! [`state::VerdictStore`] and a hand-built [`PendingEntry`] — no full engine. Extracted to a //! sibling file to keep `adj_gate.rs` under the file-size cap (CLAUDE.md). @@ -103,7 +104,7 @@ fn subtractive_delta_holds_prior_verdict() { ); } -/// JEF-445: a cached `Exploitable` is NEVER replayed from the LRU — it is re-judged against the +/// a cached `Exploitable` is NEVER replayed from the LRU — it is re-judged against the /// live model every pass, so a one-time temp-0 tail-flip can't freeze into a permanent false /// breach. (Contrast [`exact_fingerprint_hit_serves_unheld`], where a cached `Refuted` DOES serve.) #[test] @@ -124,7 +125,7 @@ fn cached_exploitable_is_rejudged_not_replayed() { ); } -/// JEF-445: the subtractive-hold path also does not replay a positive — an `Exploitable` baseline +/// the subtractive-hold path also does not replay a positive — an `Exploitable` baseline /// on a purely-subtractive delta is re-judged, not held. #[test] fn subtractive_hold_does_not_replay_exploitable() { @@ -142,7 +143,7 @@ fn subtractive_hold_does_not_replay_exploitable() { assert_eq!(store.cached_for("entry", "fp-shrunk"), None); } -/// JEF-445 scope guard: a corroborated `Confirmed` (backed by live evidence, not the model's own +/// scope guard: a corroborated `Confirmed` (backed by live evidence, not the model's own /// positive) STILL serves from the cache — only `Exploitable` is force-re-verified, so re-judging /// can never let a model `Refuted` veto a live attack. #[test] @@ -172,7 +173,7 @@ fn not_additive_without_baseline_still_rejudges() { )); } -/// An exact-fingerprint LRU hit (JEF-390) serves the cached verdict as a plain hit (`held = +/// An exact-fingerprint LRU hit serves the cached verdict as a plain hit (`held = /// false`), taking precedence over the delta gate. #[test] fn exact_fingerprint_hit_serves_unheld() { @@ -190,7 +191,7 @@ fn exact_fingerprint_hit_serves_unheld() { } } -// ---- JEF-565 LOAD-BEARING regression: a downstream-only change must re-judge ------------- +// ---- LOAD-BEARING regression: a downstream-only change must re-judge ------------- /// A downstream workload `workload/app/downstream-pod`, optionally carrying a critical CVE /// (loaded-at-runtime — exploitation evidence) on its image. The SAME identity either way, so @@ -238,7 +239,7 @@ fn graph_with_downstream(with_cve: bool) -> (SecurityGraph, NodeKey) { (g, key) } -/// THE trap this ticket closes (JEF-565): downstream evidence must land in the PROMPT *and* the +/// THE trap this ticket closes: downstream evidence must land in the PROMPT *and* the /// SURFACE, or a downstream-only change busts the exact-fingerprint LRU (layer 1, a genuine /// prompt-text miss) but the layer-2 subtractive-delta hold silently serves the prior decisive /// verdict forever — since a fingerprint miss alone isn't enough; the gate's second layer only diff --git a/engine/src/engine/adj_pass.rs b/engine/src/engine/adj_pass.rs index bd000d67..238c75ea 100644 --- a/engine/src/engine/adj_pass.rs +++ b/engine/src/engine/adj_pass.rs @@ -1,6 +1,6 @@ //! The four-phase adjudication pass — the model-as-judge stage of [`Engine::process`], //! extracted whole to keep the orchestrator under the file-size cap (CLAUDE.md) and to hold -//! the pass in one readable, independently-testable place (JEF-370). +//! the pass in one readable, independently-testable place. //! //! Given this pass's proven chains (already published for display), it judges every //! breach-relevant path exactly as the analyst would (ADR-0013): @@ -8,9 +8,9 @@ //! - **Phase 1 — classify** each breach-relevant ENTRY without calling the model: group the //! chains by their internet-facing front door, build each entry's delta-aware prompt + //! cache key ([`Engine::prepare_pending`]), then run the layered re-judge gate -//! ([`super::adj_gate`], JEF-390 LRU / JEF-391 delta hold / JEF-234 breaker+backoff). A +//! ([`super::adj_gate`] LRU delta hold breaker+backoff). A //! cache/hold/skip resolves with no model call; a genuine miss queues for dispatch. -//! - **Phase 2 — dispatch** the fresh model calls CONCURRENTLY (JEF-337), bounded by +//! - **Phase 2 — dispatch** the fresh model calls CONCURRENTLY, bounded by //! `model_concurrency`; each transport error resolves to an Uncertain for that entry alone. //! - **Phase 3 — fold** each fresh verdict back into the per-entry store: cache a decisive one //! + baseline it + close the breaker; arm backoff on an Uncertain; record latency/outcome. @@ -22,7 +22,7 @@ //! (`verdicts`, `journal`, `notifier`, `findings`, `metrics`) and stamps verdicts onto the //! passed-in `chains` in place. The caller re-publishes the enriched chains afterward. //! -//! **ADR-0034 (JEF-570):** each entry's model call now returns an +//! **ADR-0034:** each entry's model call now returns an //! [`incident::IncidentDecision`] (a 3-value assessment + the engine-resolved cuts it chose //! from the entry's deterministic menu, built in Phase 1), not the bare legacy //! [`reason::adjudicate::Verdict`]. The pass folds it two ways: `to_verdict()` derives the @@ -96,12 +96,12 @@ impl Engine { } let current_entries: HashSet = by_entry.keys().cloned().collect(); let mut verdict_counts: HashMap<&'static str, u64> = HashMap::new(); - // JEF-234: cache misses we DECLINE to send to the model this pass because the entry + // cache misses we DECLINE to send to the model this pass because the entry // (or the whole fleet, via the global breaker) is in inconclusive-adjudication backoff. // A sustained nonzero rate means the model is down and we are correctly NOT hammering it. let mut cached = 0u64; let mut skipped = 0u64; - // ADR-0023 (JEF-391): fingerprint misses HELD on a purely-subtractive delta (the prior + // ADR-0023: fingerprint misses HELD on a purely-subtractive delta (the prior // decisive verdict served, no model call). Folded into `cached` for the OTLP counter; // tracked separately only so the pass log shows how much churn the delta gate absorbed. let mut held = 0u64; @@ -114,7 +114,7 @@ impl Engine { // model dispatch below. let mut resolved: Vec<(PendingEntry, reason::adjudicate::Verdict)> = Vec::new(); let mut to_judge: Vec = Vec::new(); - // One immutable ASN snapshot for the whole pass (JEF-380): a hot-reload that lands + // One immutable ASN snapshot for the whole pass: a hot-reload that lands // mid-pass swaps the next pass's snapshot, never this one — so every entry judged this // pass sees a consistent provider table (mirrors the KEV/EPSS per-pass snapshot). let asn = self.asn.snapshot(); @@ -128,28 +128,28 @@ impl Engine { objectives.sort_by(|a, b| a.0.0.cmp(&b.0.0)); objectives.dedup_by(|a, b| a.0 == b.0); - // JEF-565: the deduped, sorted workload set on this entry's PROVEN paths, excluding + // the deduped, sorted workload set on this entry's PROVEN paths, excluding // the entry itself — every workload the model's prompt now renders its own evidence // block for (see `downstream_workloads`). let downstream = downstream_workloads(&entry, idxs, chains); - // ADR-0034 D4 (JEF-570): the deterministic cut-choice menu for this entry, unioned + // ADR-0034 D4: the deterministic cut-choice menu for this entry, unioned // across every one of its objective-chains (see `entry_menu`) — the SAME menu the // prompt's containment-options section renders and the model's `contain` reply // resolves against. let menu = entry_menu(idxs, chains, graph, health); // Build the entry's delta-aware pending record (prompt + fingerprint + projected - // surface) and read its baseline — see [`Engine::prepare_pending`] (ADR-0023 / JEF-350 - // / JEF-387). `additive` says whether the delta since the baseline is additive. + // surface) and read its baseline — see [`Engine::prepare_pending`] (ADR-0023 + // ). `additive` says whether the delta since the baseline is additive. let (pending, additive, baseline) = self.prepare_pending( entry_key, entry, objectives, downstream, idxs, graph, &asn, menu, ); - // ADR-0034 D8 (JEF-639): attempt the double replay-lock BEFORE the re-judge gate — + // ADR-0034 D8: attempt the double replay-lock BEFORE the re-judge gate — // a no-op once this run already has a LIVE decision for the entry, or when nothing // was journal-restored for it. See `try_rearm_decision`/`rearm_restored_decision`. self.try_rearm_decision(&pending); - // The layered re-judge gate (JEF-390 LRU / JEF-391 delta hold / JEF-234 breaker + + // The layered re-judge gate (LRU delta hold breaker + // backoff / re-judge), decided WITHOUT a model call — see [`adj_gate`]. match adj_gate::classify_adjudication( &self.verdicts, @@ -168,7 +168,7 @@ impl Engine { resolved.push((pending, verdict)); } adj_gate::AdjGate::Judge => { - // ADJ-MISS-DIAG (JEF-387): one compact churn-attribution line per re-judge. + // ADJ-MISS-DIAG: one compact churn-attribution line per re-judge. churn_diag::log_rejudge(&pending); to_judge.push(pending); } @@ -178,7 +178,7 @@ impl Engine { // the fingerprint is churning (re-judging unchanged entries) — watch it for model load. let judged = to_judge.len() as u64; - // Phase 2 — dispatch the fresh model calls CONCURRENTLY (JEF-337). protector no longer + // Phase 2 — dispatch the fresh model calls CONCURRENTLY. protector no longer // serializes model calls behind a process-wide 1-permit gate; ollama owns concurrency // (`OLLAMA_NUM_PARALLEL` + its queue) and is sized for the node it runs on. // `buffer_unordered` keeps at most `model_concurrency` calls in flight — a @@ -221,7 +221,7 @@ impl Engine { // did per fresh call — only the dispatch shape (concurrent, above) changed. for (pending, decision, elapsed) in judged_results { // Time the (slow, CPU-bound) model call so its latency tail is observable in - // shadow (JEF-100). Recorded for every fresh call; `result` labels the outcome. + // shadow. Recorded for every fresh call; `result` labels the outcome. self.metrics .model_latency_ms .record(elapsed.as_secs_f64() * 1000.0, &[]); @@ -235,7 +235,7 @@ impl Engine { let result = match &verdict { reason::adjudicate::Verdict::Uncertain(why) => { tracing::info!(entry = %pending.entry.0, objectives = pending.objectives.len(), %why, "adjudication inconclusive (will retry)"); - // JEF-234: arm this entry's exponential backoff and advance the global + // arm this entry's exponential backoff and advance the global // breaker's failure run, so the next pass does NOT re-judge it immediately. self.verdicts .record_inconclusive(&pending.entry_key, pass_now); @@ -246,11 +246,11 @@ impl Engine { } decisive => { tracing::info!(entry = %pending.entry.0, objectives = pending.objectives.len(), verdict = ?decisive, "adjudicated entry"); - // ADR-0034 D8 (JEF-639): durably record THIS pass's decisive cut-choice + // ADR-0034 D8: durably record THIS pass's decisive cut-choice // decision — the double replay-lock's source material on a future restart // (see `rearm_restored_decision`). Only when it actually CHANGED from the // decision already standing for this entry: `Exploitable`/`Attack` is - // re-verified every pass (JEF-445), so an unchanged standing incident would + // re-verified every pass, so an unchanged standing incident would // otherwise write an identical line every pass — the exact per-pass spam the // journal's rotation-window design (several restarts' worth of history) // depends on NOT happening. A no-op when the journal is disabled. @@ -282,7 +282,7 @@ impl Engine { pending.fingerprint.clone(), verdict.clone(), ); - // ADR-0023 (JEF-391): snapshot THIS pass's judged surface + verdict as the + // ADR-0023: snapshot THIS pass's judged surface + verdict as the // entry's new baseline, so the next pass measures additions against what this // call saw. Only decisive verdicts baseline (the `Uncertain` arm never does), // so a failed call can't suppress a later re-judge. @@ -291,14 +291,14 @@ impl Engine { pending.surface.clone(), verdict.clone(), ); - // JEF-234: a decisive answer means the model is alive — clear this entry's + // a decisive answer means the model is alive — clear this entry's // backoff and close the global breaker so judging resumes for the fleet. // Also stamps the actuation-trust clock (`decisive_at`) `pass_now` reads. self.verdicts.record_decisive(&pending.entry_key, pass_now); "ok" } }; - // Piggyback the readiness aggregation's LIVE model health (JEF-160) on this call's + // Piggyback the readiness aggregation's LIVE model health on this call's // outcome — cheap, no extra call: decisive ⇒ answered, Uncertain ⇒ timed out / // endpoint down. The readiness aggregation reads this back. self.findings.set_model_health(match result { @@ -325,24 +325,24 @@ impl Engine { for (entry_key, (pending, verdict)) in &outcomes { let objectives = &pending.objectives; let entry = &pending.entry; - // Resolve AND record what to DISPLAY for this entry in one place (JEF-371): the store + // Resolve AND record what to DISPLAY for this entry in one place: the store // owns the full carry-forward precedence — a decisive verdict shows as-is; an // inconclusive pass (a transient model timeout) carries the prior decisive verdict // forward rather than regressing the posture to "uncertain"; a live verdict supersedes // any journal-restored summary. It writes the chosen verdict to the single source of - // truth (JEF-157) the MOMENT it's decided, so the findings snapshot resolves it with no + // truth the MOMENT it's decided, so the findings snapshot resolves it with no // end-of-pass re-publish. The action logic below still uses this pass's real `verdict`. let display = self.verdicts.resolve_display(entry_key, verdict); - // Record this pass's display POSTURE for the Δ / recency column (JEF-201): the + // Record this pass's display POSTURE for the Δ / recency column: the // store sets `first_seen` on first sight and diffs against the previous pass to - // derive the Δ glyph. Shares `pass_now` with the JEF-234 backoff (one injected + // derive the Δ glyph. Shares `pass_now` with the backoff (one injected // clock). Pure presentation metadata — it gates nothing (ADR-0016: recency is a view). self.verdicts.record_recency( entry_key, state::StoredPosture::of_verdict(Some(&display)), pass_now, ); - // Append the breach decision to the durable journal (JEF-141) — only a DECISIVE + // Append the breach decision to the durable journal — only a DECISIVE // verdict, and only when it changed from the last line for this entry, so a // steady-state cluster doesn't append an identical line every pass. Uncertain is // skipped (mirrors the cache discipline). A no-op when the journal is disabled. @@ -350,11 +350,11 @@ impl Engine { if !matches!(display, reason::adjudicate::Verdict::Uncertain(_)) && self.verdicts.journaled(entry_key).as_ref() != Some(&summary) { - // Re-derive the structured enrichment-coverage (JEF-145) from the SAME evidence + // Re-derive the structured enrichment-coverage from the SAME evidence // the model was given, so the would-have-acted report classifies a coverage gap // from fact instead of grepping the verdict prose for a `CVE-` token. Cheap+pure. let coverage = reason::adjudicate::entry_coverage(graph, entry); - // JEF-301: persist fingerprint + TYPED verdict so a restart re-seeds the cache. + // persist fingerprint + TYPED verdict so a restart re-seeds the cache. // Pair them ONLY when THIS pass judged decisively for THIS fingerprint; a // carried-forward prior (this pass Uncertain) doesn't belong to the current // fingerprint, so persist `None` rather than seed a stale pair (re-judge on boot). @@ -371,7 +371,7 @@ impl Engine { verdict_typed: decisive_now.then(|| verdict.clone()), }); self.verdicts.set_journaled(entry_key, summary.clone()); - // The ONE sanctioned outbound notification (JEF-144, ADR-0018), fired on the + // The ONE sanctioned outbound notification (ADR-0018), fired on the // SAME decision identity as the journal write above — a decisive verdict whose // summary changed for this entry — so dedupe and durability share one key and // a steady-state cluster notifies once, never per pass. The payload is redacted @@ -389,7 +389,7 @@ impl Engine { .await; } // The entry's verdict applies to every chain from it. The findings snapshot - // derives the verdict from the shared store (JEF-157); this per-chain stamp is + // derives the verdict from the shared store; this per-chain stamp is // kept for the timer path's `chain.emit()` log and as the `from_chain` fallback. for &i in &pending.idxs { *verdict_counts.entry(verdict.label()).or_insert(0) += 1; @@ -412,7 +412,7 @@ impl Engine { .verdicts .record(*count, &[opentelemetry::KeyValue::new("verdict", *verdict)]); } - // How much judging this pass did, as proper cumulative counters (JEF-100, replacing + // How much judging this pass did, as proper cumulative counters (replacing // the prior `verdicts{verdict="judged_this_pass"}` gauge hack): `judged` = fresh // model calls (cache misses), `cached` = reused verdicts. Steady state should be // judged≈0; a sustained nonzero rate means fingerprint churn — the thing to watch @@ -426,8 +426,8 @@ impl Engine { if skipped > 0 { self.metrics.skipped.add(skipped, &[]); } - // Mirror the shadow would-have-acted report headline (JEF-143) to OTLP, like the - // bake counts: the gates-exiting-shadow figures (JEF-50) over the default window, + // Mirror the shadow would-have-acted report headline to OTLP, like the + // bake counts: the gates-exiting-shadow figures over the default window, // read back from the durable journal we just appended this pass's breach decision // to. Cheap no-op when the journal is disabled (replay is empty). Read-only. let report = state::default_window_report(&self.journal); @@ -453,7 +453,7 @@ impl Engine { "adjudication pass (model calls = judged)" ); } - // ADR-0034 D6/D7 (JEF-570): drop decisions for entries that no longer exist this pass + // ADR-0034 D6/D7: drop decisions for entries that no longer exist this pass // (mirrors `self.verdicts.retain_present` above) — a stale decision must never outlive // the entry it was judged for. Every entry STILL present keeps its last DECISIVE // decision even on a cache-hit/held/backoff pass this cycle (no fresh call ran), which @@ -462,7 +462,7 @@ impl Engine { // (skipped/backoff) never clears it. self.decisions .retain(|k, _| current_entries.contains(k.as_str())); - // ADR-0034 D8 (JEF-639): a journal-restored decision that never got the chance to be + // ADR-0034 D8: a journal-restored decision that never got the chance to be // checked this run (its entry wasn't breach-relevant this pass, or vanished before // `try_rearm_decision` ran) can't outlive the entry either — same prune as above. self.restored_decisions @@ -470,7 +470,7 @@ impl Engine { self.decisions.clone() } - /// ADR-0034 D8 (JEF-639): attempt to re-arm a journal-restored decision for this entry + /// ADR-0034 D8: attempt to re-arm a journal-restored decision for this entry /// against THIS pass's freshly-rebuilt fingerprint + menu — the double replay-lock (see /// [`rearm_restored_decision`]). A no-op once a LIVE decision already governs the entry /// this run (a fresh Phase 3 judgment always wins over a restored one) or when nothing @@ -539,7 +539,7 @@ pub(super) fn rearm_restored_decision( }) } -/// The deduped, sorted workload [`graph::NodeKey`]s on this entry's PROVEN paths (JEF-565), +/// The deduped, sorted workload [`graph::NodeKey`]s on this entry's PROVEN paths, /// EXCLUDING the entry itself (its own evidence is the entry's dedicated prompt fields). Scope /// is deliberately EVERY workload node across ALL of `idxs`' chains' `ProvenChain::paths` — not /// just `quarantine_targets`, which is narrower (only nodes that already carry their OWN @@ -563,7 +563,7 @@ fn downstream_workloads( nodes } -/// The deterministic cut-choice menu for one entry (ADR-0034 D4, JEF-570), unioned across +/// The deterministic cut-choice menu for one entry (ADR-0034 D4), unioned across /// EVERY one of its objective-chains — [`incident::build_menu`] itself takes just one /// [`reason::proof::ProvenChain`] (an (entry, objective) pair), but the model is judged once /// PER ENTRY over every objective it reaches, so the menu it's shown must be the union of what diff --git a/engine/src/engine/adj_pass_tests.rs b/engine/src/engine/adj_pass_tests.rs index 31363386..d6d0e757 100644 --- a/engine/src/engine/adj_pass_tests.rs +++ b/engine/src/engine/adj_pass_tests.rs @@ -1,4 +1,4 @@ -//! Unit tests for [`rearm_restored_decision`] — the ADR-0034 D8 double replay-lock (JEF-639), +//! Unit tests for [`rearm_restored_decision`] — the ADR-0034 D8 double replay-lock, //! pure and directly testable without spinning up a whole `Engine`. The end-to-end restart //! behavior (journal write → boot restore → live re-arm, including the enforce-mode standing- //! cut acceptance case) is covered by the `engine::journal_tests` integration tests instead — diff --git a/engine/src/engine/churn_diag.rs b/engine/src/engine/churn_diag.rs index 9b205c5b..f0d1112e 100644 --- a/engine/src/engine/churn_diag.rs +++ b/engine/src/engine/churn_diag.rs @@ -1,4 +1,4 @@ -//! ADJ-MISS-DIAG — the per-re-judge churn-attribution diagnostic (JEF-387). +//! ADJ-MISS-DIAG — the per-re-judge churn-attribution diagnostic. //! //! Every cache MISS (an entry the engine is about to re-judge) emits ONE compact, structured //! log line here. Over a 24h `kubectl logs` window `scripts/churn_analysis.py` ingests these @@ -12,14 +12,14 @@ //! sec_runtime= sec_cves= sec_secrets= sec_posture= sec_objectives= sec_entry= //! ``` //! -//! Emitted at DEBUG (the churn is fixed — JEF-390/JEF-391 — so it's silent by default); raise the +//! Emitted at DEBUG (the churn is fixed — — so it's silent by default); raise the //! engine to `debug` to collect a fresh window. //! //! Field meanings the collector relies on: //! - `entry` — the entry key: the per-entry timeline key. //! - `fp` — the FULL-STATE prompt hash (the verdict-cache key; excludes the delta-only -//! "Changes since…" section, JEF-391). UNCHANGED from the entry's prior line ⇒ an -//! Uncertain-retry (JEF-234: model verdict churn, not prompt). CHANGED ⇒ state churn, +//! "Changes since…" section). UNCHANGED from the entry's prior line ⇒ an +//! Uncertain-retry (model verdict churn, not prompt). CHANGED ⇒ state churn, //! attributed to whichever `sec_*` field moved. //! - `chain` — the objective/technique-SET shape hash: entries with the same shape group. //! - `sec_*` — the six per-section fingerprints; the one that changed between two consecutive @@ -34,7 +34,7 @@ use super::PendingEntry; /// full-prompt dump when `PROTECTOR_ADJ_DIAG_FULL` is set. See the module docs for the format. pub(super) fn log_rejudge(pending: &PendingEntry) { let sections = &pending.sections; - // DEBUG level: the churn is understood + fixed (JEF-390/JEF-391), so this stays silent in + // DEBUG level: the churn is understood + fixed, so this stays silent in // normal operation. Raise the engine to `debug` to collect a fresh window for // `scripts/churn_analysis.py`. tracing::debug!( @@ -89,7 +89,7 @@ mod tests { } } - /// JEF-387: the compact ADJ-MISS-DIAG line the collector depends on carries every field + /// the compact ADJ-MISS-DIAG line the collector depends on carries every field /// as space-free `key=value`. This LOCKS that contract — if the field set or a value's /// rendering changes, `scripts/churn_analysis.py` breaks, and this fails first. #[test] diff --git a/engine/src/engine/cut_divergence_tests.rs b/engine/src/engine/cut_divergence_tests.rs index af2fe2f9..00af2c93 100644 --- a/engine/src/engine/cut_divergence_tests.rs +++ b/engine/src/engine/cut_divergence_tests.rs @@ -17,7 +17,7 @@ use crate::engine::respond::ProposedAction; /// pivot-then-objective compromise takes: the front door is popped, then the attacker walks one /// hop laterally to the workload actually holding the crown-jewel credential. Both `payments` and /// `ledger` are independently compromisable and network-reachable from the internet foothold, so -/// BOTH qualify as `RemotelyExploitable` quarantine candidates (JEF-284/JEF-322) on top of the +/// BOTH qualify as `RemotelyExploitable` quarantine candidates on top of the /// entry's own surgical edge-cut — the downstream (not just entry) divergence surface this /// fixture exists to exercise. fn multi_hop_incident_snapshot() -> Snapshot { diff --git a/engine/src/engine/dashboard/api_json_tests.rs b/engine/src/engine/dashboard/api_json_tests.rs index 0c8fe581..4c29f3ae 100644 --- a/engine/src/engine/dashboard/api_json_tests.rs +++ b/engine/src/engine/dashboard/api_json_tests.rs @@ -1,9 +1,9 @@ -//! HTTP-level tests for the read-only per-view JSON endpoints (ADR-0025, JEF-395): +//! HTTP-level tests for the read-only per-view JSON endpoints (ADR-0025): //! `GET /api/{findings,action,readiness,admission,alerts}.json`. They assert that each endpoint //! serves the SAME view-model its tab renders (byte-for-byte the serialized props — no drift, no //! second DTO), that it is GET-only (a write verb 405s — the view is never a gate), and that it //! carries `Cache-Control: no-store` (the per-session-gated, zero-egress snapshot must never sit -//! in a shared edge cache — JEF-283). They drive the real axum router via `tower::oneshot`. +//! in a shared edge cache —). They drive the real axum router via `tower::oneshot`. use std::sync::Arc; @@ -224,7 +224,7 @@ async fn divergence_json_is_get_only_no_store_and_a_raw_array() { /// REAL router and asserts the header is present and strict on `GET /api/findings.json`. It is the /// regression net that a future router edit can't silently drop the layer off the JSON API: pin /// `connect-src 'self'` / `script-src 'self'` / `frame-ancestors 'none'` / `form-action 'self'`, -/// and forbid `'unsafe-inline'` / `'unsafe-eval'` (Finding 1, JEF-395 / JEF-396). +/// and forbid `'unsafe-inline'` / `'unsafe-eval'` (Finding 1). #[tokio::test] async fn the_json_api_carries_the_strict_csp() { let router = super::router(empty_state(), None); diff --git a/engine/src/engine/dashboard/auth/claims.rs b/engine/src/engine/dashboard/auth/claims.rs index 15e38ef2..d3e0961b 100644 --- a/engine/src/engine/dashboard/auth/claims.rs +++ b/engine/src/engine/dashboard/auth/claims.rs @@ -64,7 +64,7 @@ impl Tier { } } - /// Resolve the CEILING tier for a verified identity (JEF-501), with this precedence: + /// Resolve the CEILING tier for a verified identity, with this precedence: /// /// 1. An explicit, **recognized** `tier` claim wins — the IdP's own statement is authoritative, /// even over a configured grant (e.g. a claim of `forensic` beats a `raw` grant for the same @@ -107,7 +107,7 @@ impl Tier { /// ever match its own field: an identifier containing `@` is an **email** (matched only against a /// **verified** `email` claim, case-insensitively); one without is a **sub** (matched only against /// `sub`, exactly). This closes a cross-field collision a single untyped OR would otherwise allow -/// (JEF-501 HIGH fix): without typing, an operator's `raw=alice@example.com` (meant as an email) +/// (HIGH fix): without typing, an operator's `raw=alice@example.com` (meant as an email) /// would ALSO match a token whose opaque `sub` happened to equal that exact string, silently /// widening the granted set beyond what was configured — and symmetrically for a bare `sub` /// identifier that happens to collide with someone's `email`. `@`-presence is an unambiguous split @@ -131,7 +131,7 @@ impl GrantId { } /// Whether this identifier matches the verified identity. An [`GrantId::Email`] NEVER matches - /// unless `email_verified` is `true` (JEF-501 HIGH fix: a self-asserted, unverified `email` + /// unless `email_verified` is `true` (HIGH fix: a self-asserted, unverified `email` /// claim proves nothing about ownership — only that the IdP minted *a* token, not that the /// subject controls that address). fn matches(&self, sub: &str, email: Option<&str>, email_verified: bool) -> bool { @@ -144,7 +144,7 @@ impl GrantId { } } -/// Operator-configured identity→tier grants (`PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS`, JEF-501): +/// Operator-configured identity→tier grants (`PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS`): /// resolves the tier ceiling from the VERIFIED token identity (`sub`/verified-`email`) when the /// IdP mints no `tier` claim at all — e.g. Cloudflare Access relaying GitHub, which emits neither. /// A grant is a CEILING like the claim it stands in for: it can only be READ here, never combined @@ -174,7 +174,7 @@ impl TierGrants { /// The highest tier granted to a verified identity. `sub` is matched EXACTLY against a /// sub-typed grant only; `email` is matched CASE-INSENSITIVELY against an email-typed grant - /// only, and only when `email_verified` is `true` (JEF-501 — an unverified `email` claim is + /// only, and only when `email_verified` is `true` (— an unverified `email` claim is /// never a match candidate). Neither matching ⇒ [`Tier::Redacted`] (an unlisted/absent identity /// stays at the floor — a grant never widens beyond what's configured). pub fn resolve(&self, sub: &str, email: Option<&str>, email_verified: bool) -> Tier { @@ -193,7 +193,7 @@ impl TierGrants { } /// The decoded token claims the verifier reads: the required `sub`, the optional `email` + -/// `email_verified` (JEF-501 — used, together with `sub`, to match an operator-configured tier +/// `email_verified` (— used, together with `sub`, to match an operator-configured tier /// grant), plus every other claim captured flat in `extra` so the operator-configured tier claim /// can be looked up from it without this struct having to name the IdP's claim schema (ADR-0030 /// §1: protector reads the tier claim, it does not define it). @@ -207,7 +207,7 @@ pub struct Claims { #[serde(default)] pub email: Option, /// The verified token's `email_verified` claim. **Absent ⇒ `false`** — the safe default - /// (JEF-501 HIGH fix): a signature only proves the IdP minted the token, never that the + /// (HIGH fix): a signature only proves the IdP minted the token, never that the /// subject owns the `email` it carries, unless the IdP itself asserts it verified that /// ownership. An email-typed [`TierGrants`] entry never matches without this being `true`. #[serde(default)] diff --git a/engine/src/engine/dashboard/auth/enforce.rs b/engine/src/engine/dashboard/auth/enforce.rs index 30831d36..52a4929b 100644 --- a/engine/src/engine/dashboard/auth/enforce.rs +++ b/engine/src/engine/dashboard/auth/enforce.rs @@ -1,4 +1,4 @@ -//! Content-negotiating enforcement (JEF-487, ADR-0030 §6): mounts the JEF-485 [`Verifier`] as the +//! Content-negotiating enforcement (ADR-0030 §6): mounts the [`Verifier`] as the //! LIVE dashboard access gate and shapes every fail-closed denial by **route class**, so the client //! contract holds: //! @@ -15,7 +15,7 @@ //! serve; this matches the [`AuthError::status`](super::AuthError::status) mapping the verifier //! already defines. Never a bypass. //! -//! Every denial carries `Cache-Control: no-store` (JEF-283: a cached `302`→login is exactly the +//! Every denial carries `Cache-Control: no-store` (a cached `302`→login is exactly the //! Cloudflare-edge bug), and — because this layer is mounted UNDER the CSP layer — the strict CSP //! rides every rejection too. The gate is the ONLY thing that can turn a request into a `next.run`; //! there is no path that serves the graph on a verification error (the fail-*open* trap ADR-0030 §6 @@ -50,7 +50,7 @@ const ENV_LOGIN_URL: &str = "PROTECTOR_DASHBOARD_OIDC_LOGIN_URL"; /// verified identity passes), so configuring an issuer never, by itself, forbids a valid token. const ENV_MIN_TIER: &str = "PROTECTOR_DASHBOARD_OIDC_MIN_TIER"; -/// The live dashboard access gate: the JEF-485 [`Verifier`] plus the content-negotiation policy +/// The live dashboard access gate: the [`Verifier`] plus the content-negotiation policy /// (where to send an unauthenticated browser, and the minimum authorization tier). Built ONLY when /// an issuer is configured — its absence is the loud edge-only bypass (ADR-0030 §6), which the /// caller handles by simply not mounting this layer. @@ -210,7 +210,7 @@ fn redirect(location: &str) -> Response { } /// Stamp `Cache-Control: no-store` on a denial so a shared edge (Cloudflare) never caches a -/// `302`→login (or any rejection) against the URL and serves it to the next caller (JEF-283). +/// `302`→login (or any rejection) against the URL and serves it to the next caller. fn no_store(mut response: Response) -> Response { response .headers_mut() diff --git a/engine/src/engine/dashboard/auth/enforce_tests.rs b/engine/src/engine/dashboard/auth/enforce_tests.rs index 5ed2487f..cd82f810 100644 --- a/engine/src/engine/dashboard/auth/enforce_tests.rs +++ b/engine/src/engine/dashboard/auth/enforce_tests.rs @@ -1,6 +1,6 @@ -//! Integration tests for the LIVE-router OIDC enforcement (JEF-487 / ADR-0030 §6). They drive the +//! Integration tests for the LIVE-router OIDC enforcement (ADR-0030 §6). They drive the //! REAL `dashboard::router` with a configured [`Enforcer`] built over an in-memory JWKS (no egress, -//! via the JEF-485 `test_support` seam), and assert the content-negotiated fail-closed contract: +//! via the `test_support` seam), and assert the content-negotiated fail-closed contract: //! `/api/*.json` denials are `401` JSON and are NEVER `302`'d; a document `GET /` denial is a `302` //! to login; a below-tier identity is `403`; JWKS-down is `503`; every rejection still carries the //! strict CSP + `no-store`; and the unconfigured router (no enforcer) serves without rejecting. @@ -220,7 +220,7 @@ async fn api_findings_with_valid_token_is_200_with_the_view_model_and_oidc_auth_ // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- -// /api/access.json (JEF-490) — inherits the OIDC gate (401 unauthenticated), and the audit rows are +// /api/access.json — inherits the OIDC gate (401 unauthenticated), and the audit rows are // redacted to the CALLER's own tier: a redacted-tier caller never learns a raw pull's target; a // forensic/raw-tier caller does. // ------------------------------------------------------------------------------------------------- @@ -375,7 +375,7 @@ async fn a_401_still_carries_the_strict_csp_and_no_store() { assert_eq!( response.headers().get(header::CACHE_CONTROL).unwrap(), "no-store", - "a cached 401/302 is the JEF-283 edge bug — every rejection is no-store" + "a cached 401/302 is the edge bug — every rejection is no-store" ); } @@ -390,7 +390,7 @@ async fn a_302_login_redirect_is_no_store() { assert_eq!( response.headers().get(header::CACHE_CONTROL).unwrap(), "no-store", - "a cached 302->login is exactly the Cloudflare edge bug (JEF-283)" + "a cached 302->login is exactly the Cloudflare edge bug" ); } diff --git a/engine/src/engine/dashboard/auth/mod.rs b/engine/src/engine/dashboard/auth/mod.rs index 5c24731a..bd578c19 100644 --- a/engine/src/engine/dashboard/auth/mod.rs +++ b/engine/src/engine/dashboard/auth/mod.rs @@ -10,13 +10,13 @@ //! `Authorization: Bearer` token (incl. ID-JAG, `aud=protector`) — because the verifier only ever //! sees a JWT, not how it arrived (ADR-0030 §3/§6/§7). //! -//! **Scope (JEF-485):** this module is the verifier primitive + a mountable middleware layer. The +//! **Scope:** this module is the verifier primitive + a mountable middleware layer. The //! content-negotiating enforcement wiring that mounts it on the live [`super::router`] (login -//! redirect vs JSON `401`, the loud unconfigured-mode passthrough) lives in [`enforce`] (JEF-487). +//! redirect vs JSON `401`, the loud unconfigured-mode passthrough) lives in [`enforce`]. //! [`OidcConfig::from_env`] models the UNCONFIGURED state (issuer absent) so that wiring can choose //! the passthrough behavior. //! -//! **Env namespace (JEF-487).** The dashboard-auth env vars are `PROTECTOR_DASHBOARD_OIDC_*` — a +//! **Env namespace.** The dashboard-auth env vars are `PROTECTOR_DASHBOARD_OIDC_*` — a //! namespace DISTINCT from the sigstore/cosign signature-verification `PROTECTOR_OIDC_ISSUER` //! (the Fulcio keyless *cert-identity* issuer, unrelated to who may VIEW the dashboard). The two //! would otherwise collide: the chart sets the signature issuer unconditionally, which — under a @@ -59,7 +59,7 @@ const ENV_AUDIENCE: &str = "PROTECTOR_DASHBOARD_OIDC_AUDIENCE"; const ENV_TIER_CLAIM: &str = "PROTECTOR_DASHBOARD_OIDC_TIER_CLAIM"; /// `PROTECTOR_DASHBOARD_OIDC_ALGORITHM` — the pinned asymmetric algorithm (`RS256` | `ES256`). const ENV_ALGORITHM: &str = "PROTECTOR_DASHBOARD_OIDC_ALGORITHM"; -/// `PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS` — operator identity→tier grants (JEF-501): resolves the +/// `PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS` — operator identity→tier grants: resolves the /// ceiling from the VERIFIED `sub`/`email` when the IdP mints no `tier` claim at all (e.g. /// Cloudflare Access relaying GitHub). Format `tier=id1,id2;tier=id3`, e.g. /// `raw=alice@example.com;forensic=bob@example.com`. Unset/empty = no grants (unchanged behavior). @@ -118,7 +118,7 @@ pub struct OidcConfig { pub tier_claim: String, /// The pinned asymmetric algorithm. pub algorithm: SigningAlgorithm, - /// Operator identity→tier grants (JEF-501): resolves the ceiling from the verified `sub`/ + /// Operator identity→tier grants: resolves the ceiling from the verified `sub`/ /// `email` when no `tier` claim is present. Empty (default) = no grants — current behavior. pub tier_grants: TierGrants, } @@ -140,7 +140,7 @@ pub enum ConfigError { )] UnsupportedTier(String), /// A `PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS` entry names a tier that is not one of the - /// recognized tiers — a loud misconfiguration (JEF-501), never a silently-dropped grant. + /// recognized tiers — a loud misconfiguration, never a silently-dropped grant. #[error( "PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS names an unrecognized tier `{0}` \ (redacted, forensic, raw)" @@ -236,7 +236,7 @@ pub struct Identity { /// The resolved authorization tier — see [`Tier::from_claims_with_grants`] for the precedence /// (an explicit recognized `tier` claim, else an identity→tier grant, else the floor). pub tier: Tier, - /// The verified token's `email` claim, if present (JEF-501). Not an identity in its own + /// The verified token's `email` claim, if present. Not an identity in its own /// right — `subject` remains the principal — but threaded through so callers/logs that want it /// (and the tier-grant resolution above) can read it without re-decoding the token. pub email: Option, @@ -312,7 +312,7 @@ impl AuthError { /// The HTTP status this failure maps to as the fail-closed default. A JWKS-unreachable /// condition is a `503` (we could not verify); every other failure is a `401`. The finer - /// content negotiation (login redirect, JSON body) is JEF-487; this is only the safe default. + /// content negotiation (login redirect, JSON body) is ; this is only the safe default. pub fn status(&self) -> StatusCode { match self { AuthError::JwksUnreachable => StatusCode::SERVICE_UNAVAILABLE, @@ -324,7 +324,7 @@ impl AuthError { impl IntoResponse for AuthError { fn into_response(self) -> Response { // Return the status only — the specific variant is for logs/tests, never leaked to the - // caller (which check failed is not the caller's business). JEF-487 shapes the body. + // caller (which check failed is not the caller's business). shapes the body. self.status().into_response() } } @@ -423,7 +423,7 @@ fn build_validation(config: &OidcConfig) -> Validation { /// /// This is the sibling shape to [`super::security_headers::set_csp`] — a mountable layer that emits /// a BARE status on failure (no content negotiation). The live [`super::router`] mounts the -/// content-negotiating [`enforce`] layer instead (JEF-487, ADR-0030 §6), which reuses the shared +/// content-negotiating [`enforce`] layer instead (ADR-0030 §6), which reuses the shared /// [`authenticate`] seam this layer also uses — so the verification logic lives in exactly one /// place. Mount this bare form with `axum::middleware::from_fn_with_state(Arc, require_oidc)`. pub async fn require_oidc( diff --git a/engine/src/engine/dashboard/auth/test_support.rs b/engine/src/engine/dashboard/auth/test_support.rs index 071ac2c9..a6414b4f 100644 --- a/engine/src/engine/dashboard/auth/test_support.rs +++ b/engine/src/engine/dashboard/auth/test_support.rs @@ -1,5 +1,5 @@ -//! Shared, zero-egress test scaffolding for the OIDC auth suites (JEF-485 verifier unit tests + -//! JEF-487 enforcement integration tests). Fixed test RSA keypairs are embedded, and the JWKS is +//! Shared, zero-egress test scaffolding for the OIDC auth suites (verifier unit tests + +//! enforcement integration tests). Fixed test RSA keypairs are embedded, and the JWKS is //! served in-memory by [`TestFetcher`] — so the whole suite runs with NO network: the verifier's //! [`JwksFetcher`] seam is what lets a test hand it a key set without a fetch. Extracted here so the //! two suites mint valid/invalid/rotated tokens from ONE source (no fixture duplication). diff --git a/engine/src/engine/dashboard/auth/tests.rs b/engine/src/engine/dashboard/auth/tests.rs index 20621471..56cede55 100644 --- a/engine/src/engine/dashboard/auth/tests.rs +++ b/engine/src/engine/dashboard/auth/tests.rs @@ -1,4 +1,4 @@ -//! Unit tests for the OIDC verifier (JEF-485 / ADR-0030). +//! Unit tests for the OIDC verifier (ADR-0030). //! //! Keys and the in-memory (zero-egress) JWKS fetcher live in [`super::test_support`] — the whole //! suite mints valid/invalid/rotated tokens through that shared seam without a network fetch. diff --git a/engine/src/engine/dashboard/auth/tier_grants_tests.rs b/engine/src/engine/dashboard/auth/tier_grants_tests.rs index e014b7c1..c32fbe69 100644 --- a/engine/src/engine/dashboard/auth/tier_grants_tests.rs +++ b/engine/src/engine/dashboard/auth/tier_grants_tests.rs @@ -1,4 +1,4 @@ -//! Unit tests for `PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS` (JEF-501): identity→tier grants that +//! Unit tests for `PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS`: identity→tier grants that //! resolve the ceiling from a VERIFIED `sub`/`email` when the IdP mints no `tier` claim at all — //! the Cloudflare-Access-over-GitHub case, verified live, that motivated this ticket. Split out of //! `tests.rs` to keep both files well under the repo's 1,000-line cap (CLAUDE.md). @@ -240,7 +240,7 @@ fn missing_or_empty_email_still_resolves_a_sub_based_grant() { #[tokio::test] async fn verifier_end_to_end_resolves_raw_and_forensic_grants_from_a_signed_token() { - // Mints real signed tokens (no `tier` claim) through the JEF-485 scaffolding and drives the + // Mints real signed tokens (no `tier` claim) through the scaffolding and drives the // FULL verify() path — proving the grant wiring works end to end, not just at the claims layer. let fetcher = Arc::new(TestFetcher::new(jwk_set(&[(KID_A, KEY_A_N)]))); let config = OidcConfig { diff --git a/engine/src/engine/dashboard/mod.rs b/engine/src/engine/dashboard/mod.rs index 7b50aa72..fe78c686 100644 --- a/engine/src/engine/dashboard/mod.rs +++ b/engine/src/engine/dashboard/mod.rs @@ -2,8 +2,8 @@ //! for the engine's read-only output state. Zero-egress, same-origin only — the security graph and //! evidence never leave the cluster. Presentation is a VIEW, never a decision gate (ADR-0016). //! -//! Under the v4 cutover (ADR-0025 / JEF-398) the engine is **Preact-only**: the maud *body* -//! renderers and the per-tab flag are gone. Under JEF-408 (superseding ADR-0025 / see ADR-0027) +//! Under the v4 cutover (ADR-0025) the engine is **Preact-only**: the maud *body* +//! renderers and the per-tab flag are gone. Under (superseding ADR-0025 / see ADR-0027) //! the LAST server-rendered body parts — the status strip and the tab nav — moved to the client //! too: the server now emits a ROOT-ONLY document shell (`` + the `#dash-root` mount), and the //! bundled Preact client renders ALL body HTML (strip, nav, and every view body) reconciling from @@ -79,23 +79,23 @@ pub struct DashboardState { /// report. Named `decision_journal` (not `journal`) so it never collides with the /// `JudgementLog` the run-loop binds as `journal`. pub decision_journal: Arc, - /// The webhook's admission-decision log (JEF-226/237) — the bounded, deduped ring of policy + /// The webhook's admission-decision log — the bounded, deduped ring of policy /// decisions read by the Admission tab (the webhook floor). Read-only here. pub policy_log: Arc, /// The cluster label shown in the strip. pub cluster: String, - /// The SERVER-derived app-level auth mode (ADR-0030 / JEF-487): `Oidc` when the dashboard mounts + /// The SERVER-derived app-level auth mode (ADR-0030): `Oidc` when the dashboard mounts /// the enforcing verifier (an issuer is configured), `EdgeOnly` when unconfigured (the loud - /// bypass, §6). Folded into the persistent strip so the client renders the honest pill (JEF-489) + /// bypass, §6). Folded into the persistent strip so the client renders the honest pill /// and derives nothing. Set by the caller (`run_loop`) from the same config it uses to build the /// [`auth::enforce::Enforcer`], so the pill can never disagree with what is actually enforced. pub auth_mode: AuthMode, - /// The durable forensic/raw MCP disclosure audit sink (ADR-0031 §4, JEF-490) — the SAME `Arc` + /// The durable forensic/raw MCP disclosure audit sink (ADR-0031 §4) — the SAME `Arc` /// the MCP server appends to. The "Access" tab reads its records (redacted to the caller's own /// tier); read-only here, like every other handle. Present even when the MCP server isn't served /// (then it simply holds no records — an honest empty log, not a hidden tab). pub mcp_audit: Arc, - /// The shadow-bake divergence log (ADR-0035's bake step, JEF-569 rescope): the bounded ring + /// The shadow-bake divergence log (ADR-0035's bake step rescope): the bounded ring /// of recent model-vs-deterministic cut-choice classifications the engine appends each pass. /// Read-only here, like every other handle — served RAW (no `strip`/props envelope; it is a /// diagnostic bake feed for the human arm-readiness review, not a navigable tab) at @@ -120,7 +120,7 @@ impl DashboardState { let health: ModelHealth = self.findings.model_health(); let last_pass: Option = self.findings.last_pass(); let runtime = self.findings.runtime_coverage(); - // Overlay the cross-pass coverage-stall register (JEF-421): a covering runtime feed that has + // Overlay the cross-pass coverage-stall register: a covering runtime feed that has // gone dark past the debounce escalates the runtime row to `stalled`. Per-pass derivation // can't see the edge, so it's folded in here from the stall tracker's decided state. derive_readiness(&config, health, last_pass, &runtime) @@ -143,7 +143,7 @@ impl DashboardState { self.findings.last_pass(), ); let (breach, uncertain) = self.signing_regression_counts(); - // Overlay the cross-pass coverage-stall register (JEF-421) so a stalled runtime feed reads + // Overlay the cross-pass coverage-stall register so a stalled runtime feed reads // loud (and forbids green) on EVERY tab, exactly like a standing signing regression. let alert = view_model::coverage_stall_alert(&self.findings.coverage_state()); strip @@ -153,7 +153,7 @@ impl DashboardState { } /// The standing signing-regression counts `(established, cold)` from the admission-decision log - /// (JEF-264) — folded into the persistent strip so a standing regression keeps it non-green on + /// — folded into the persistent strip so a standing regression keeps it non-green on /// EVERY tab, without routing through the reachability findings pipeline. fn signing_regression_counts(&self) -> (usize, usize) { view_model::signing_regression_counts(&self.policy_log.snapshot()) @@ -184,7 +184,7 @@ impl DashboardState { view } - /// Build the Alerts view props (JEF-323): the persistent strip + the live "alarming-now" + /// Build the Alerts view props: the persistent strip + the live "alarming-now" /// activity events derived from the SAME per-pass findings snapshot the Findings view /// reads (a current-window view — runtime signals live one pass — not a persisted log) + the /// calm blind-node caveat for the quiet state. The strip carries the real findings counts (and @@ -225,7 +225,7 @@ impl DashboardState { view_model::build_admission_view(self.status_strip(), &rows) } - /// Build the "Access" view props (JEF-490): the persistent strip + the CALLER's own tier chip + + /// Build the "Access" view props: the persistent strip + the CALLER's own tier chip + /// the newest-first forensic/raw MCP disclosure pulls, each redacted to the caller's own tier. /// `caller_tier` comes from the verified [`Identity`] the OIDC layer inserted (or the /// most-restricted [`Tier::Redacted`] default when unauthenticated/edge-only), so a lower-tier @@ -307,7 +307,7 @@ impl ScopePreviewQuery { } /// `GET /` — the ROOT-ONLY document shell for the requested tab (default Findings): the `` -/// (cluster-labelled title + css) + the Preact `#dash-root` mount point (JEF-408, superseding +/// (cluster-labelled title + css) + the Preact `#dash-root` mount point (superseding /// ADR-0025's server-rendered strip/nav). The client renders ALL body HTML — the status strip, the /// tab nav, and the view body — reconciling from `/api/{tab}.json`. The honesty tokens (all-clear / /// watching / judging-state) stay server-derived in that JSON; a blank before the first fetch is @@ -326,7 +326,7 @@ async fn index(State(state): State, Query(q): Query) - /// server-derived honesty tokens (`all-clear`/`watching`, per-row `posture`, `is-cleared`, the /// blind caveat) are already decided in the props and serialize as decided values — the client /// performs zero honesty derivation. `Cache-Control: no-store` mirrors the CSS/JS routes: this is -/// a per-session-gated, zero-egress snapshot that must never sit in a shared edge cache (JEF-283). +/// a per-session-gated, zero-egress snapshot that must never sit in a shared edge cache. fn view_json(view: T) -> Response { ([(header::CACHE_CONTROL, "no-store")], Json(view)).into_response() } @@ -357,7 +357,7 @@ async fn admission_json(State(state): State) -> Response { view_json(state.admission_view()) } -/// `GET /api/access.json` — the read-only "Access" view-model snapshot (JEF-490): the forensic/raw +/// `GET /api/access.json` — the read-only "Access" view-model snapshot: the forensic/raw /// MCP disclosure audit, redacted to the CALLER's own tier. GET-only, `no-store`, inherits the OIDC /// gate from the router-wide enforce layer (a 401 fires there before this handler runs — no second /// gate). The caller's tier is read from the verified [`Identity`] the enforce layer inserted; when @@ -399,7 +399,7 @@ async fn scope_preview_json( /// `GET /assets/dashboard.css` — the light-theme stylesheet, same-origin. /// -/// `Cache-Control: no-store` is load-bearing behind Cloudflare Access (JEF-283): Cloudflare +/// `Cache-Control: no-store` is load-bearing behind Cloudflare Access: Cloudflare /// caches `.css`/`.js` by file extension even with no origin directive, and it caches 302s — /// so an unauthenticated edge hit gets Access's 302→login cached against this URL and then /// served (as HTML) to authenticated users, leaving the dashboard unstyled. no-store keeps this @@ -416,7 +416,7 @@ async fn dashboard_css() -> Response { } /// `GET /assets/dashboard.js` — the zero-dep client script, same-origin. -/// `Cache-Control: no-store` for the same Access/edge-cache reason as the stylesheet (JEF-283). +/// `Cache-Control: no-store` for the same Access/edge-cache reason as the stylesheet. async fn dashboard_js() -> Response { ( [ @@ -429,7 +429,7 @@ async fn dashboard_js() -> Response { } /// Build the dashboard router with the read-only state and, when configured, the app-level OIDC -/// enforcement gate (ADR-0030 / JEF-487). +/// enforcement gate (ADR-0030). /// /// Every response carries the strict same-origin CSP (ADR-0025) via a single /// [`security_headers::set_csp`] layer — the layer covers all routes, so a route added @@ -489,15 +489,15 @@ pub async fn serve_dashboard(addr: SocketAddr, state: DashboardState, auth: Opti } } -// JEF-395: HTTP-level tests for the read-only per-view JSON endpoints (ADR-0025) — same-view-model, +// HTTP-level tests for the read-only per-view JSON endpoints (ADR-0025) — same-view-model, // GET-only, no-store, strict CSP, and the never-a-false-green honesty guard at the JSON boundary. -// These are the retained honesty proof after the v4 cutover (JEF-398): the maud-render honesty +// These are the retained honesty proof after the v4 cutover: the maud-render honesty // tests are gone because their view-model is unchanged and its guarantee is now asserted here (the // serialized props the client consumes) + in the client `vitest` suite. #[cfg(test)] mod api_json_tests; -// JEF-398: page-shell tests — the Preact-only page emits, for every tab, the server-rendered strip +// page-shell tests — the Preact-only page emits, for every tab, the server-rendered strip // + nav + the `#dash-root` mount point (calm-when-blind first paint stays server-side). #[cfg(test)] mod page_tests; diff --git a/engine/src/engine/dashboard/page.rs b/engine/src/engine/dashboard/page.rs index 335fb875..a14cc4d4 100644 --- a/engine/src/engine/dashboard/page.rs +++ b/engine/src/engine/dashboard/page.rs @@ -1,8 +1,8 @@ -//! Page composition (ADR-0019 §2, cut over by ADR-0025, and by JEF-408 to a ROOT-ONLY body): +//! Page composition (ADR-0019 §2, cut over by ADR-0025, and by to a ROOT-ONLY body): //! assemble the document shell — the `` (title with the cluster label, meta, css link) around //! the single Preact mount point. This is the only place that knows the document ``. //! -//! Under JEF-408 (superseding ADR-0025's "strip + nav stay SERVER-RENDERED"): the body is now +//! Under (superseding ADR-0025's "strip + nav stay SERVER-RENDERED"): the body is now //! ROOT-ONLY — just `
` + the deferred bundle `