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..85ffc3f6 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 `