Skip to content

AE datavolume: DX-steered vs save-ALL reduction measurement#63

Merged
entlein merged 12 commits into
ae-prodfrom
ae-datavolume-dx-steering
Jun 19, 2026
Merged

AE datavolume: DX-steered vs save-ALL reduction measurement#63
entlein merged 12 commits into
ae-prodfrom
ae-datavolume-dx-steering

Conversation

@entlein

@entlein entlein commented Jun 17, 2026

Copy link
Copy Markdown

Stacked on #53 (ae-prod). Continues the AE datavolume work: now that the F1 10k-cap fix is live-verified on aeprod11 (passthrough firehose captures the full window, 42,516 http rows/query, read==wrote), this PR measures the datavolume reduction of DX-steered AE (rev-3 streaming — AE writes only the pods DX steers into its activeSet over the control surface) vs saving ALL data (passthrough firehose).

What

  • New harness src/e2e_test/adaptive_export_loadtest/harness/exp_dx_steering_reduction.sh: two arms (ALL passthrough vs DX streaming), same fixed load, forensic_db active-part deltas (rows + on-disk bytes) per table; reduction = 1 - DX/ALL.
  • Successor to ae_vs_all.sh (whose AE arm used the rev-2 controller gate + a stale malformed JNDI). This uses rev-3 DX streaming + the canonical resolvable JNDI FQDN so the chain fires and DX classifies.

Measurement (filled from the live run on the rig)

  • ALL bytes/table, DX bytes/table, reduction % per table + total — to follow.

Measures the AdaptiveExport value prop: datavolume REDUCTION of DX-steered AE
(rev-3 streaming, AE writes only DX-steered activeSet pods over the control
surface) vs saving ALL data (passthrough firehose). Two arms, same fixed load,
forensic_db active-part deltas (rows+bytes) per table; reduction = 1 - DX/ALL.
Uses the canonical resolvable JNDI FQDN (attacker.attacker-ns.svc.cluster.local)
so the chain fires + DX can classify (a malformed host → NXDOMAIN → no steer).
Successor to ae_vs_all.sh, whose AE arm used the rev-2 controller gate + stale JNDI.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b8459eed-2eb0-4e79-866d-ca7de56eb6c3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ae-datavolume-dx-steering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@entlein

entlein commented Jun 17, 2026

Copy link
Copy Markdown
Author

Run 1 — INVALID (methodology), not a reduction number

First DX-vs-ALL run on rig 6a32e7a9 (AE aeprod11, DX 0.2.0-rc5). The raw output showed "TOTAL reduction=100.0%" — but that is a dead DX arm artifact, not a real reduction. Root cause, three compounding issues:

  1. Stale activeSet — the DX arm rehydrated adaptive_attribution export windows for pods that no longer exist (ld-12/22/25/28/31 = deleted F1-cap-test loaders, plus observer/postgres). AE's streaming scanner ran mode=whitelist pods=7 but every query returned rows=0 because it was querying dead pods.
  2. log4shell chain did not fire — marshalsec Send LDAP reference count = 0, so the JNDI fire produced no LDAP egress → DX never got a log4shell signal → the backend (the traffic-bearing target) was never steered.
  3. DX verdicts indeterminate (on observer), no log4shell-rce-exfil.

DX steering is mechanically alive (it classified + created export windows; AE activeSet populated, scanner in whitelist mode) — but the test let stale windows pollute the activeSet and the attack didn't fire.

Harness fixes before a valid number:

  • Clear/expire adaptive_attribution at DX-arm start so the activeSet only contains freshly-steered, alive pods.
  • Ensure the steered pod is alive and traffic-bearing for the whole DX window.
  • For the attack-steered variant: confirm the log4shell chain actually fires (marshalsec serving + backend vulnerable + FQDN resolves) so DX issues a log4shell-rce-exfil verdict and steers the backend.

Next: fix the harness (clear stale windows + live-pod steering), re-run.

Entlein added 2 commits June 18, 2026 07:42
Measures all AE non-functional requirements under steady load on the rig:
throughput (rows+bytes/sec), capture completeness (AE read vs broker count =
F1 cap proof), write fidelity (read==wrote + write-error count), end-to-end
freshness latency (now - max(time_) in CH), resource footprint (AE pod cpu/mem
idle vs loaded), per-cycle cadence. Emits a structured report; companion to
exp_dx_steering_reduction.sh. Real-data only.
…ring + live-pod guard)

Run-1 reported false 100% reduction because stale adaptive_attribution windows
rehydrated DEAD pods (deleted loaders) into the activeSet → AE streamed dead pods
→ 0 rows. Clear adaptive_attribution before the DX arm so the activeSet only gets
freshly-steered LIVE pods; add a guard that prints the steered pods + marshalsec
fire count + live log4j-poc pods so a dead-arm result is caught, not reported as
a reduction.
@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

AE NFR benchmark — run 1 (aeprod11, rig 6a3383d0, passthrough firehose, 180s window)

Real metrics (forensic_db active-part deltas + kubectl top + ae_reconcile). Two metrics are excluded as measurement artifacts (fixes in progress, see notes) rather than reported wrong.

Throughput (AE write rate to forensic_db, under steady frontend load):

table rows/s bytes/s rows/180s
http_events 33 2,120 5,893
conn_stats 21 315 3,864
dns_events 5 104 839

Resource footprint (sum of 2 AE pods):

state CPU (m) mem (Mi)
idle 429 27
under load 527 32

Write fidelity: write_err count = 0 across all passthrough cycles (http 7, conn 6, dns 7 cycles) → clean sink, no dropped batches. (read==wrote verified exactly in the dedicated F1 runs.)

Excluded (harness measurement bugs, not AE issues):

  • Capture completeness % — the broker-count snapshot was taken after load ended vs AE cycles during the window (misaligned), yielding a nonsensical 623%. The cap/completeness proof is the dedicated F1 test (max_read > 10000, posted separately). Harness fix: align the broker window to the measurement window.
  • E2E latencynow() - max(time_) hit a DateTime64 type error (na). Fix: dateDiff('second', max(time_), now()).

More NFR runs (variance) + the corrected completeness/latency to follow.

lag query used now()-DateTime64 (type error -> na); use dateDiff(second,...).
Capture-completeness vs broker was window-misaligned (623% artifact) -> drop it;
report tot_read vs tot_wrote (read==wrote) + errs instead. The 10k-cap/completeness
proof is the dedicated F1 test (max_read>10000 vs broker for the SAME window).
@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

F1-cap re-confirm on fresh rig 6a3383d0 — read==wrote clean (conclusive >10k proof is on PR #53)

aeprod11 passthrough, 60 min window, ae_reconcile http cycles:

ts read_count wrote_count
06:26:54 7,181 7,181
06:26:33 5,926 5,926
06:25:56 4,854 4,854

read==wrote exactly, errs=0 — clean sink. Note this rig's broker in-memory retention (~6.5 min of http at the current rate) caps a single-window pull at ~7k, so I can't reproduce a >10,000 single-window read here. The conclusive cap-lift proof is on PR #53 (same aeprod11 binary, read=42,516 >> the old hard 10,000 cap). This re-confirm shows the binary behaves identically (uncapped up to what the broker holds, read==wrote) on a second rig.

@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

DX-vs-ALL reduction — run 2 INVALID (rig node failure), not a number

Re-ran on 6a3383d0 with the attacker up + chain firing (marshalsec 5/5 pre-run). The run is invalidnode-01 went NotReady mid-run → TaintManagerEviction evicted the log4j-poc pods (backend vc2t2→lnmjg, observer, postgres, cl-* all Terminating/ContainerCreating). Consequences:

  • backend restarted mid-run → JNDI hit a restarting pod → marshalsec_fires=0 at the guard.
  • the two arms ran in different time windows under different (churning) load, so the deltas aren't comparable.
  • byte-delta "reduction" came out negative (−226% http) — system.parts byte delta is compaction-noisy (e.g. pgsql −930), unsuitable as the primary metric.

What DID work (mechanism is sound): DX classified + steered the live backend into the activeSet (adaptive_attribution: backend = 3 windows) and AE streamed it (35,068 http rows in the DX arm). So DX→AE steering is functioning; the rig node failure invalidated the measurement.

Fixes for the valid run:

  1. Cluster must be stable (node-01 Ready) — flagged to the rig owner.
  2. Switch the reduction metric to rows (compaction-noise-free), measured over the SAME wall-clock window for both arms where possible.
  3. Methodology: steering the backend (the dominant-traffic pod) bounds the reduction — will also report the backend's share of total cluster traffic so the number is interpretable.

Re-run pending node recovery.

Entlein added 2 commits June 18, 2026 08:43
…ary)

Run-2 byte-delta reduction came out negative because system.parts byte delta is
compaction-noisy (merges land mid-window). Report rows reduction as primary
(actual captured-row count, noise-free); keep bytes as secondary context.
Eviction-RCA finding (PR #63 NFR campaign): AE had NO memory limit (only cpu
300m) and was CPU-pinned at 300m under concurrent passthrough. AE measured tiny
(16-38Mi steady), but the raised 1M-row passthrough cap can spike, so cap at 1Gi
so AE can never memory-pressure a node; raise cpu limit 300m->1 core (was
throttling). NOTE node evictions were NOT AE/OOM — node-01 went NotReady
(network/heartbeat); the memory consumer is PEM (1365Mi, OOMs at the 2Gi default).
@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

DX-vs-ALL reduction — run 3 INVALID, root cause = upstream detection gap (bob#140), NOT AE

This time the cluster was stable (no node churn, all log4j-poc pods Running) and the chain fired 38× (marshalsec). Yet the DX arm wrote 0 rows → false "100%". Root cause is upstream of AE:

  • DX steered observer (4 windows) + postgres (2) — NOT the backend (the log4shell target).
  • dx-daemon produced verdicts only for observer/postgres (all triage=indeterminate); no backend verdict at all.
  • → DX never gets a backend case, because kubescape doesn't flag the backend's log4shell (the R0011 "Unexpected Egress" DNS-gate suppresses DNS-named egress — filed bob#140; R0005 nuances). No kubescape anomaly on the backend ⇒ no DX referral ⇒ no steer ⇒ AE streams the wrong (low-traffic) pods ⇒ 0.

What this confirms: the DX→AE steering mechanism + AE streaming + the chain firing all work; the DX-steered-log4shell reduction is blocked on the upstream kubescape detection of log4shell (bob#140) — not on AE or DX. Reported, not faked.

To still get a real reduction number (independent of bob#140): directly steer the backend via the AE control surface and measure AE-streams-backend vs ALL (isolates the AE streaming-reduction mechanism). Pursuing that next.

(Resource hardening applied this cycle: AE measured 16–38Mi → capped 1Gi + cpu→1 core; PEM → 3Gi via pemMemoryLimit (was OOMing at the 2Gi default → PEM restarts disrupt AE). Node evictions were node-01 NotReady = network/heartbeat, not OOM.)

@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

AE e2e evidence campaign — consolidated summary (aeprod11, rig 6a3383d0)

Real data only; invalid runs reported as RCAs, never faked.

✅ Proven

  • F1 10k query-cap fix (live): read=42,516 http (PR adaptive_export: production AE — streaming export + write-integrity #53), read==wrote, errs=0. Re-confirmed read==wrote on this rig (broker retention caps a single window at ~7k here, so the >10k repro lives on PR adaptive_export: production AE — streaming export + write-integrity #53).
  • NFR (passthrough, under load): throughput http 33 rows/s (2,120 B/s), conn 21/s, dns 5/s; footprint idle 429m CPU/27Mi → load 527m/32Mi (AE later measured 16–38Mi mem — tiny — but CPU-pinned at the old 300m limit); write fidelity write_err=0 across all cycles (read==wrote).
  • DX→AE steering MECHANISM works: DX classifies, creates adaptive_attribution export windows, AE activeSet populates, streaming scanner runs whitelist-mode (it steered the live backend in run-2 before the node failed).

🔧 Hardening applied (this campaign)

  • AE limits cpu=1, memory=1Gi (measured 16–38Mi; was no-mem-limit + CPU-throttled at 300m).
  • PEM pemMemoryLimit=3Gi (was OOMing at the 2Gi default → PEM restarts disrupt AE capture).

🔎 Node-eviction RCA (memory was suspected)

NOT memory: nodes ran 55–60%, MemoryPressure=False. Evictions were TaintManagerEviction from node-01 going NotReady (network/kubelet-heartbeat drop), not OOM.

⛔ Datavolume reduction (DX/AE vs save-ALL) — blocked, two real causes

  1. bob#140 — kubescape doesn't flag the backend's log4shell (R0011 DNS-gate), so DX never gets a backend case → it steers the wrong (low-traffic) pods (observer/postgres, indeterminate) → 0 captured → false 100%. The chain fires (marshalsec 38×) and steering works mechanically; the gap is upstream detection.
  2. Rig instability — AE secret (clickhouse-dsn + pixie-api-key) reverts to placeholders on every AE roll → reduction runs (which roll AE) keep breaking. Escalated to makefile-agent.
  • Modeled note: reduction = 1 − (steered-pod traffic ÷ total cluster traffic) — entirely scenario/load-dependent. A clean number needs (a) DX steering the actual attack pod (bob#140) and (b) a representative cluster load.

Harness changes committed to this PR

dead-arm guard + stale-steering clear; rows-primary reduction metric; NFR dateDiff lag fix + read==wrote; AE manifest mem-limit. Note: exp_row_reconcile.sh's ?probe= tag doesn't survive (Pixie strips query strings from req_path) — fidelity is instead evidenced by ae_reconcile read==wrote/errs=0.

Entlein added 2 commits June 18, 2026 12:40
Root cause of the recurring "AE unauthenticated / writes 0 / crashloop" reverts:
kustomization.yaml bundled adaptive_export_secrets.yaml (placeholder pixie-api-key)
with the role+deployment, so EVERY infra re-apply (make log4j) clobbered the real
key that ae-auth had written. Separation of concerns: remove the secret from the
kustomization — infra (role+deployment) stays re-appliable; the secret holds real
creds and is owned solely by `make ae-auth`, created once, never touched by infra
re-applies. Secret manifest kept as a hand-applied seed-only template (documented).
The DX-steered arm was failing because the harness only fired stage-1 (JNDI/LDAP).
That generates ldap-egress but NO kubescape R0001 → backend never flagged → DX no
case → indeterminate → AE steers wrong/no pods. R0001 comes from stage-2 (post-
exploitation exec). fire() now does stage-1 (JNDI) + stage-2 (whoami/shadow/token/
getent in the backend) → kubescape R0001+R0006 → DX rules backend MALIGNANT →
backend enters AE activeSet → reduction is measurable. Verified live: DX evidence
unexpected-spawn+sensitive-file-read → verdict ruled_in generic=MALIGNANT.
@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

DX-steered reduction — run 4: STEERING FIXED ✅, blocked now on cluster health (not AE)

The stage-2 fix worked — DX now steers the backend: steered-pods this run = backend ×7 (top), attacker ×3, observer/postgres ×1. marshalsec 29×, all log4j-poc pods Running (no churn). So the whole chain up to steering is proven: stage-2 → kubescape R0001(336)+R0006(24) on backend → DX ruled_in generic=MALIGNANT{execution,collection} → backend in AE activeSet.

But the DX arm still wrote 0 rows — and the AE streaming logs show why: pixieapi: stream: rpc error: code = Unavailable desc = cluster is not in a healthy state on nearly every streaming query. vizierPhase=Unhealthy (all pl pods Running, but the cluster status is degraded). The ALL/passthrough arm captured fine just minutes earlier (15,719 http rows) — the cluster degraded between arms. So the 100% is again an artifact, this time from Pixie cluster health, not AE or steering.

Chain of reduction blockers, each a distinct real cause (all now understood):

  1. stale activeSet (dead pods) → fixed (clear adaptive_attribution).
  2. node-01 NotReady eviction churn → rig.
  3. only stage-1 fired → no R0001 → no DX case → fixed (two-stage fire); backend now steered.
  4. vizierPhase=Unhealthy → streaming queries rejected → this run. Rig/cluster health.

Re-run pending a healthy cluster. AE write-path + DX steering are not the blockers.

Entlein added 4 commits June 18, 2026 14:01
…dd DX-steering diagnostics

Standing terminology rule: allowlist/blocklist, never whitelist/blacklist.
Pure rename (no behavior change) of the rev-3 streaming filter:
  FilterModeWhitelist  → FilterModeAllowlist
  MaxWhitelistSize     → MaxAllowlistSize
  ADAPTIVE_STREAM_MAX_WHITELIST → ADAPTIVE_STREAM_MAX_ALLOWLIST  (env)
  mode=whitelist log string → mode=allowlist
plus all comments/identifiers/tests in streaming, activeset, cmd/main.

DX-steering diagnostics (the reason DX-arm-writes-0 has been hard to RCA —
we could not tell "empty ActiveSet" from "broker returned 0 rows"):
  - scanner: log the empty-allowlist short-circuit (was silent) so an
    empty ActiveSet is visible in logs, distinct from "query completed rows=0".
  - FilterUpdater: emitted-filter log Debug→Info so the steered pod count
    per ActiveSet change is visible without debug logging.

NOTE: ADAPTIVE_STREAM_MAX_WHITELIST env renamed → tooling that sets the old
name must switch to ADAPTIVE_STREAM_MAX_ALLOWLIST.
The Pixie dx_evidence_graph UI reads dx_attack_graph via px.DataFrame
clickhouse_dsn, whose query template hardcodes event_time + hostname and
ORDER BY event_time. A table without those columns fails 'Unknown
identifier event_time'; a table created by hand (local, not via the
operator) isn't globally registered. Fix: make AE own it like the other
forensic tables.

- schema.sql: dx_attack_graph DDL with event_time(UInt64 nanos) + hostname,
  edge columns, fromUnixTimestamp64Nano partition/TTL (nanos-correct).
- KnownTables + OperatorOwnedTables: register it so Apply creates it at boot.
- apply_test: assert last-applied DDL == last OperatorOwnedTables entry
  (robust to appended operator tables) instead of hardcoding trigger_watermark.

go test ./.../clickhouse green.
Pixie's clickhouse_dsn type mapper reads UInt8 as BOOLEAN and does not
handle UInt16/UInt32/Float32 -> px fails with 'Column[N] given incorrect
type' rendering the dx_evidence_graph. weight/max_severity/num_findings
-> Int64, confidence -> Float64 (map cleanly to INT64/FLOAT64). Verified
live: px run returns all 6 edges with every column. event_time stays
UInt64 (matches kubescape_logs, which px reads).
…canner buildPxL

The DX/streaming arm silently capped each per-table pull at Pixie's default
10000-row limit while the passthrough/ALL arm (pxl.CompilePassthrough /
QueryFor) already raises it to 1,000,000 via the broker's #px:set query flag.
Validated live on 6a33dac0: a single streaming http_events pull returned
exactly rows=10000 (the cap). Left unfixed this UNDER-counts the DX arm and
OVERSTATES the DX-vs-ALL volume reduction. Prepend the same #px:set directive
to the streaming scanner's PxL so both arms are uncapped and comparable.
@entlein

entlein commented Jun 18, 2026

Copy link
Copy Markdown
Author

Data-volume reduction — DX-steered AE vs save-ALL (basic log4shell, 2026-06-18)

Result: 99.87% row reduction / 99.33% byte reduction. DX-steered AdaptiveExport writes ~0.13% of the save-everything firehose. 5 reps each arm, reproducible, on a verified-healthy AE (aeprod14).

Mean per 2-min incident window (5 reps each)

table ALL (firehose) DX (steered) row reduction
http_events 7,135 10 99.86%
dns_events 13,839 3 99.98%
conn_stats 2,762 18 99.35%
TOTAL 23,736 rows (~1.09 MB) 31 rows (~7.4 KB) 99.87% rows / 99.33% bytes

Exact conditions

  • AE image: ghcr.io/k8sstormcenter/vizier-adaptive_export_image:0.14.19-aeprod14-x86_64 @ sha256:e03ce08a87aaf460de78e4f960233d479fbfa0042553aa91180adad22ab35512
  • Commit: e3f7f8102 (branch ae-datavolume-dx-steering); tag release/vizier/v0.14.19-aeprod14. Carries the streaming #px:set max_output_rows_per_table=1000000 cap fix (de-biases the DX arm) on top of aeprod12 (f88650331) + aeprod13 (3e2b93a5a).
  • dx-daemon: ttl.sh/dxgo-c6110ff-0609 @ sha256:14e43e0a… (dx commit c6110ff)
  • Vizier (stock): ghcr.io/k8sstormcenter/vizier-*_image:0.14.17; deployed with the official pixie-io px v0.8.8 CLI.
  • Stack: node-agent sbob-rc1-2026-05-16, kubescape v3.0.47, vector 0.56.0, clickhouse-server 24.8; workload log4j-chain-backend-vulnerable@sha256:72655e… + log4j-chain-attacker@sha256:c4dd5f….
  • Rig: k3s v1.36.1+k3s1, cluster-id 7877e560-9f94-4b60-a2af-a0c55cfeb7f0, cloud …:443. PEM mem req/limit 2Gi/3Gi; AE limit 1Gi.
  • AE env: ADAPTIVE_SKIP_APPLY=true, CONTROL_ADDR=:9100, ADAPTIVE_RECONCILE=true; ALL arm ADAPTIVE_PASSTHROUGH=true win/refresh 60/60; DX arm ADAPTIVE_WRITE_MODE=streaming win/refresh 60/60.

Protocol

Per config {ALL, DX}, 5 reps: rollout (abort if AE not Running) → restart query-broker, wait vizier Healthy → TRUNCATE all forensic_db tables → 2-min window, single full-chain fire at t=60s (JNDI/LDAP stage-1 + stage-2 in-pod command exec + DNS exfil of /etc/shadow) → measure every forensic_db table (post-truncate sizes = exactly what that config wrote) → 180s settle. No synthetic load (app-only). 4 pods steered every rep (backend/attacker/postgres/observer); chain fired 1×/rep.

Caveats

  1. app-only noise model → ALL is dominated by cluster-wide infra/system traffic (kube-dns chatter + AE→CH self-traffic), not workload; the 99.9% mostly reflects "DX ignores the whole-cluster firehose and stores only the incident pods."
  2. DX ≈ the attack footprint only (~31 rows); the steered pods carry no ambient load.
  3. cap-clean: DX volume (≪10k) is far under the per-table cap, so it is not truncation-biased (unlike the earlier aeprod12 run where DX http hit exactly 40000 = 4×10k → 92.9% upper bound).

Full reproduction record (images, SHAs, env, harness, raw per-rep CSV, failure modes): internal biz/PoC/log4j/datavolume/june18/ (REPRODUCE.md + exp_log4j_reps.sh + aefix_per_pg.sh + log4j_reps_aeprod14.{txt,tsv}).

@entlein

entlein commented Jun 19, 2026

Copy link
Copy Markdown
Author

AE Non-Functional benchmark (NFR) — throughput, footprint, and end-to-end no-data-loss

Measured live on the rig under the log4shell attack-chain workload + realistic noise, two AE modes back-to-back (passthrough firehose, then DX-steered streaming). ADAPTIVE_RECONCILE=true.

End-to-end NO DATA LOSS — verified against physical ClickHouse counts

Not just AE's internal accounting — for every table we cross-checked broker read_count == AE wrote_count == actual count(*) in forensic_db:

mode table broker_read AE_wrote CH count(*)
passthrough http_events 9,866 9,866 9,866
passthrough dns_events 42,535 42,535 42,535
passthrough conn_stats 3,856 3,856 3,856
streaming http_events 271 271 271
streaming dns_events 14 14 14
streaming conn_stats 126 126 126

Zero rows lost anywhere in the broker → AE → ClickHouse path, in both modes.

Throughput

  • passthrough (firehose): 56,257 rows / 156 s = 360.6 rows/s (per 2-node cluster)
  • DX-streaming: 411 rows / 155 s = 2.7 rows/s — it writes only the steered-pod subset; the ~99% data-volume reduction shows up directly as throughput.

Footprint

  • AE exporter: 67 MiB peak (passthrough), 38 MiB peak (streaming) — both pods summed, against a 1 GiB limit (~5% of budget). The exporter is featherweight in either mode.
  • PEM: ~1.8 GiB/pod — this is Pixie's own resident cost (Stirling table store, PL_TABLE_STORE_DATA_LIMIT_MB=1228), not AE.

Takeaway: AdaptiveExport is lossless (proven against actual CH rows) and cheap (≈40–70 MiB); the only heavy resident is the stock Pixie PEM. Harness: e2e_test/adaptive_export_loadtest/harness/nfr.sh.

@entlein entlein requested a review from ConstanzeTU June 19, 2026 11:52

@ConstanzeTU ConstanzeTU left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets get this merged, and you code-agent please dont overwrite it anymore

@entlein entlein merged commit 98ed54f into ae-prod Jun 19, 2026
8 of 12 checks passed
@entlein entlein deleted the ae-datavolume-dx-steering branch June 19, 2026 18:45
entlein pushed a commit that referenced this pull request Jun 21, 2026
Eviction-RCA finding (PR #63 NFR campaign): AE had NO memory limit (only cpu
300m) and was CPU-pinned at 300m under concurrent passthrough. AE measured tiny
(16-38Mi steady), but the raised 1M-row passthrough cap can spike, so cap at 1Gi
so AE can never memory-pressure a node; raise cpu limit 300m->1 core (was
throttling). NOTE node evictions were NOT AE/OOM — node-01 went NotReady
(network/heartbeat); the memory consumer is PEM (1365Mi, OOMs at the 2Gi default).
entlein pushed a commit that referenced this pull request Jun 21, 2026
Eviction-RCA finding (PR #63 NFR campaign): AE had NO memory limit (only cpu
300m) and was CPU-pinned at 300m under concurrent passthrough. AE measured tiny
(16-38Mi steady), but the raised 1M-row passthrough cap can spike, so cap at 1Gi
so AE can never memory-pressure a node; raise cpu limit 300m->1 core (was
throttling). NOTE node evictions were NOT AE/OOM — node-01 went NotReady
(network/heartbeat); the memory consumer is PEM (1365Mi, OOMs at the 2Gi default).

Signed-off-by: entlein <einentlein@gmail.com>
entlein added a commit that referenced this pull request Jun 21, 2026
)

* adaptive_export: production AE — streaming export + write-integrity

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: ADAPTIVE_PASSTHROUGH firehose loop

New env-gated background loop that runs the same PxL shape AE's
anomaly-gated path uses, but with an empty Target (no ns/pod predicate)
and over a configurable rolling window. Writes via the existing sink so
the byte-shape of forensic_db rows is comparable between the
PASSTHROUGH=1 phase (EVERYTHING) and the PASSTHROUGH=0 phase
(AE-FILTER). One-shot A/B that yields the per-table capture fraction of
the adaptive write path.

- internal/passthrough/passthrough.go — Loop + Config; defaults to
  30s window / 30s refresh / clickhouse.PixieTables() table list.
- internal/passthrough/passthrough_test.go — 6 tests; the load-bearing
  one is TestLoop_EmitsEmptyTargetPxL (asserts neither df.namespace nor
  df.pod predicates appear in the emitted PxL).
- cmd/main.go: ADAPTIVE_PASSTHROUGH + _WINDOW_SEC + _REFRESH_SEC env
  knobs. Adapter is constructed unconditionally when passthrough is on
  (joins the existing PushPixie / streaming construction path so the
  same pxapi grpc stream is reused). Loop is registered with the
  shutdown WaitGroup so SIGTERM waits for the in-flight tick.
- cmd/BUILD.bazel: drop @px// load (other AE BUILD.bazel files use
  //bazel — sticking out as the only one with @px is a leftover from a
  prior gazelle run; align). Add passthrough dep.

Signed-off-by: entlein <einentlein@gmail.com>

* ci: dx-image workflow — build + publish dx-daemon to ghcr

Stand-alone workflow that builds entlein/dx (private Active-Diagnosis
Framework) into ghcr.io/k8sstormcenter/dx-daemon. Separates the dx image
publish from the bazel-based vizier_release pipeline; the dx repo ships
its own Dockerfile.dxd (Go cross-compile + distroless final stage) so it
doesn't need to live as a submodule inside src/vizier/services/dx.

Triggers:
- tag push 'release/dx/v*' on this repo cuts a release build, image tag
  derived from the tag suffix (release/dx/v0.1.0 -> image tag 0.1.0).
- workflow_dispatch lets us build any dx ref on demand with a custom tag
  (default: short sha of the resolved dx commit).

Pulls dx via DX_ENTLEIN_PAT (already configured on the repo). Multi-arch
build (linux/amd64, linux/arm64); Dockerfile.dxd cross-compiles in the
native BUILDPLATFORM stage and the final stage is COPY-only, so target
emulation isn't required.

Signed-off-by: entlein <einentlein@gmail.com>

* Revert ci: dx-image workflow — wrong repo

The dx image build pipeline lives in entlein/dx itself (PR #53,
branch feat/bazel-release): bazel-based with @px external pin to
pixie's ae-prod tip, pushes to docker.io/entlein/dx-daemon on a
release/dx/v* tag in the dx repo. The pixie-side buildx workflow
this reverts duplicated that intent in the wrong repo + the wrong
build system (docker buildx instead of bazel + pl_go_image macros)
+ the wrong registry (ghcr.io/k8sstormcenter instead of
docker.io/entlein).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: unit-normalize trigger watermark cursor + load-test affordances

Fixes the silent-halt bug: the trigger gated on a RAW event_time high-water-mark,
so a single anomaly in a larger unit (ms/ns) drove the watermark past all real
seconds rows and AE stopped processing forever (data still on Pixie). Normalize
event_time to canonical nanoseconds in the poll SELECT filter+order and in the
in-memory/persisted cursor, boundary-dedup, and maxSeen (normalizeEventTimeNanos
+ chNormEventTimeNanos). Validated at the data layer: vs a poisoned watermark the
raw filter returns 0 rows, the normalized filter recovers all 60.

Also adds ADAPTIVE_PUSH_REFRESH_SEC (negative = single-shot pull) for the
reproducible load-test harness, an in-package trigger unit test, and an e2e
hermetic load test (mock PixieQuerier, exact rows+bytes).

Signed-off-by: entlein <einentlein@gmail.com>

* e2e_test/adaptive_export_loadtest: AE fixture-isolation load-test harness

Consolidate the adaptive_export load-test harness under src/e2e_test/ (matching
vzconn_loadtest / px_cluster conventions). Control-plane experiments (E1-E4, E6,
E8 sustained) are proven exactly-reproducible on a live rig; the data-plane
experiments (E5, E8 data-mode) are authored and pending live validation on a
vizier-registered rig (status documented in README + FINDINGS_AND_BACKLOG).

- harness/: shell + python (inject, exp_control, exp_e8, stats, ...) + lib helpers
- fixtures/EXPERIMENTS.md: curated kubescape_logs data-set catalog + expected outputs
- k8s/: isolated sinks + per-rep generator pod (no probes)
- tools/loadgen/: cleanloadgen + httpsink (docker-built test tool; .bazelignore'd
  pending a bazel target — lib/pq is already vendored in the module)
- FINDINGS_AND_BACKLOG.md: F8 watermark-poison bug + the fix + AE backlog

The AE Go unit/e2e tests live with the service (internal/{trigger,e2e}).

Signed-off-by: entlein <einentlein@gmail.com>

* e2e_test/adaptive_export_loadtest: document AE implied contracts (C1-C14) + diagrams

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: C15 write-duration contract + DX-steering diagram; gen sustained-DNS mode

C15 = AE must keep re-pulling+writing an active pod until t_end or DX stop (the
contract DX steers on; last week's 'wrote then stopped' is its violation). Add
DX-steering sequence diagram. Generator gains SUSTAIN_SEC (distinct-DNS trickle,
a Pixie-traced protocol) + configurable SETTLE_PRE_MS warm-up for fresh-pod
capture; harness wires GEN_SUSTAIN_SEC/GEN_SETTLE_MS.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export/trigger: update test SQL substrings for multiIf normalisation

The 700821d trigger unit-normalisation wrapped event_time in a
multiIf(...) inside both the WHERE filter and the ORDER BY. Three
existing tests in watermark_test.go + one in clickhouse_test.go pinned
the raw 'event_time >= N' substring and broke at HEAD.

Update each test's expected substring to match the new normalized form
(') >= <ns-scaled N>' — the closing paren of multiIf, then the value
in canonical nanoseconds). Per-test ns-scaling:

  watermark_test.go:94  1744000000000000000  already ns -> unchanged
  watermark_test.go:125 InitialWatermark=42  < 1e10 sec -> * 1e9
  watermark_test.go:156 InitialWatermark=7   < 1e10 sec -> * 1e9
  watermark_test.go:297 event_time='5000'    < 1e10 sec -> * 1e9
  clickhouse_test.go:82 ref.T=1744..303e9 ns already ns -> unchanged

go test ./src/vizier/services/adaptive_export/... all green.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: exp_control uses real now_s event_time (no future-stamp watermark poison)

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: ADAPTIVE_RECONCILE per-pull write-fidelity instrument

Records one forensic_db.ae_reconcile row per data-plane pull (read_count vs
wrote_count, window, ns/pod) across ALL three write paths — controller fan-out
(filter), passthrough firehose, and streaming scanner — so a reconcile run
localizes loss to query (R5: read<PEM) vs sink (R6: wrote<read) and quantifies
re-pull dup (C8). Counts alone (write >= read) were proven insufficient.

- new internal/reconcile leaf package (Row, Recorder, Nop) — no import cycle
- sink.Record: CH-backed recorder (INSERT INTO forensic_db.ae_reconcile)
- ae_reconcile table: schema.sql + KnownTables + OperatorOwnedTables (synced);
  not a pixie table (absent from PixieTables, so VerifyPixieSchema ignores it)
- wired: passthrough.tick, controller.pushPixieRows (deferred, all return
  paths), streaming.scanner.Run; gated by ADAPTIVE_RECONCILE=true, else Nop
- unit test proves read/wrote capture incl. the sink-drop read>wrote shape
- fixed apply_test trailing-tables guard for the new operator table
- harness: exp_row_reconcile.sh (row-level PEM<->CH), ae_vs_all.sh, exp_datavolume_extreme.sh

Signed-off-by: entlein <einentlein@gmail.com>

* harness: exp_pipeline_reconcile — skip empty-key rows (0 rows != LOSS 1)

px -o json empty result previously printed one blank line → counted as a phantom
LOSS=1. Guard: empty set → 0-byte keys file; drop all-empty-field keys. Confirmed
against the controlled log4j run (backend http 14/14 exact, conn 66>=12, no loss).

Signed-off-by: entlein <einentlein@gmail.com>

* harness: log4shell_fire.sh — reliably fire + restart the log4j-chain log4shell

Reliable BY CONSTRUCTION against bob#140 (stateful/unreliable exploit on re-fire):
fresh-JVM backend (delete pod) + attacker-before-backend + the WORKING resolvable FQDN
attacker.<ns>.svc.cluster.local:1389 (NOT the bare attacker-ns.svc which NXDOMAINs and
gets dropped), then VERIFY the actual backend->:1389 LDAP egress in forensic_db.conn_stats
and RETRY until confirmed (the validity gate). Never assumes the exploit fired. Node-side.

Signed-off-by: entlein <einentlein@gmail.com>

* harness: log4shell_fire.sh — detection-signal framing (Cyber Verification)

Reword from offensive 'exploit' to detection-signal-generation language: this validates
the kubescape->DX->AE detection chain. No logic change.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export(passthrough): precompiled + concurrent firehose, drop http2

- pxl.CompilePassthrough/Render: precompile per-table PxL once (fixed
  window => constant relative start_time), only the two time_ bounds are
  stamped per tick. Rendered output is byte-identical to QueryFor with an
  empty Target (TestCompilePassthrough_MatchesQueryFor), so this is a
  structural change, not a capture change. upid->pod/ns stays in PxL.
- passthrough: tickConcurrent fans every table out at once (was a serial
  loop); shared pull() helper. Sink/recorder are pool/HTTP-backed and
  already called concurrently elsewhere.
- drop http2_messages.beta from the firehose set (not materialised on
  every cluster => ""Table not found"" every tick); shared PixieTables/DDL
  lists untouched.
- toggle ADAPTIVE_PASSTHROUGH_COMPILED (default on; =false reverts to the
  legacy serial QueryFor path).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: bazel BUILD deps for internal/reconcile + pxl compile.go

Fixes "missing strict dependencies: import of .../internal/reconcile" that
broke the AE image build for passthrough, sink, streaming, controller, cmd
(pre-existing since the ADAPTIVE_RECONCILE commit added the package + imports
without bazel deps; never CI-built). Also wires the new pxl/compile.go srcs
+ passthrough/pxl test srcs (compiled_test.go, reconcile_test.go, compile_test.go).

- new internal/reconcile/BUILD.bazel (go_library, stdlib-only)
- +//internal/reconcile dep: passthrough, sink, streaming, controller, cmd
- pxl go_library +compile.go; pxl_test +compile_test.go; passthrough_test +compiled_test.go,reconcile_test.go (+reconcile dep)

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export(pxl): raise Pixie 10k result cap via #px:set query flag

F1 RCA: Pixie caps every px.display at max_output_rows_per_table (default
10000, query_flags.go) — the planner add_limit_to_batch_result_sink_rule
silently truncates wide firehose windows / busy pods at the READ (write path
is clean: ae_reconcile shows read==wrote). Fix uses Pixie own native knob —
prepend `#px:set max_output_rows_per_table=1000000` to every generated PxL
(QueryFor + CompilePassthrough) so all pull paths are uncapped. Validated on
rig: a 14208-row window returned 10000 (capped) vs 14298 (with flag). No
pagination loop, no extra round-trips. See memory project-ae-passthrough-10k-cap.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export/sink: content_type silent-drop contract suite

Consolidates the recurring content_type silent-drop incident class
into one default-suite test gate (6 tests, ~15ms):

  I1 TestContract_ContentTypeIsInt64InSchema
  I2 TestContract_FastEncodeContentTypeAsInt
  I3 TestContract_SilentDropDetected
  I3.b TestContract_SilentDropNotTriggeredOnSuccess
  I3.c TestContract_SilentDropToleratesMissingSummaryHeader
  I4 TestContract_HTTPEventsRoundTrip

Top-of-file docstring chronicles the incident timeline so future
operators can grep their way to the contract.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: DX-steered-vs-ALL datavolume reduction harness

Measures the AdaptiveExport value prop: datavolume REDUCTION of DX-steered AE
(rev-3 streaming, AE writes only DX-steered activeSet pods over the control
surface) vs saving ALL data (passthrough firehose). Two arms, same fixed load,
forensic_db active-part deltas (rows+bytes) per table; reduction = 1 - DX/ALL.
Uses the canonical resolvable JNDI FQDN (attacker.attacker-ns.svc.cluster.local)
so the chain fires + DX can classify (a malformed host → NXDOMAIN → no steer).
Successor to ae_vs_all.sh, whose AE arm used the rev-2 controller gate + stale JNDI.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: deep AE NFR benchmark harness

Measures all AE non-functional requirements under steady load on the rig:
throughput (rows+bytes/sec), capture completeness (AE read vs broker count =
F1 cap proof), write fidelity (read==wrote + write-error count), end-to-end
freshness latency (now - max(time_) in CH), resource footprint (AE pod cpu/mem
idle vs loaded), per-cycle cadence. Emits a structured report; companion to
exp_dx_steering_reduction.sh. Real-data only.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: fix DX-reduction dead-arm (clear stale steering + live-pod guard)

Run-1 reported false 100% reduction because stale adaptive_attribution windows
rehydrated DEAD pods (deleted loaders) into the activeSet → AE streamed dead pods
→ 0 rows. Clear adaptive_attribution before the DX arm so the activeSet only gets
freshly-steered LIVE pods; add a guard that prints the steered pods + marshalsec
fire count + live log4j-poc pods so a dead-arm result is caught, not reported as
a reduction.

Signed-off-by: entlein <einentlein@gmail.com>

* nfr harness: fix lag (dateDiff) + drop racy broker-pct completeness

lag query used now()-DateTime64 (type error -> na); use dateDiff(second,...).
Capture-completeness vs broker was window-misaligned (623% artifact) -> drop it;
report tot_read vs tot_wrote (read==wrote) + errs instead. The 10k-cap/completeness
proof is the dedicated F1 test (max_read>10000 vs broker for the SAME window).

Signed-off-by: entlein <einentlein@gmail.com>

* dx-reduction harness: report ROWS reduction (primary) + bytes (secondary)

Run-2 byte-delta reduction came out negative because system.parts byte delta is
compaction-noisy (merges land mid-window). Report rows reduction as primary
(actual captured-row count, noise-free); keep bytes as secondary context.

Signed-off-by: entlein <einentlein@gmail.com>

* ae deployment: add memory limit (1Gi) + raise cpu limit to 1 core

Eviction-RCA finding (PR #63 NFR campaign): AE had NO memory limit (only cpu
300m) and was CPU-pinned at 300m under concurrent passthrough. AE measured tiny
(16-38Mi steady), but the raised 1M-row passthrough cap can spike, so cap at 1Gi
so AE can never memory-pressure a node; raise cpu limit 300m->1 core (was
throttling). NOTE node evictions were NOT AE/OOM — node-01 went NotReady
(network/heartbeat); the memory consumer is PEM (1365Mi, OOMs at the 2Gi default).

Signed-off-by: entlein <einentlein@gmail.com>

* ae bootstrap: separate the secret from the re-applied infra bundle

Root cause of the recurring "AE unauthenticated / writes 0 / crashloop" reverts:
kustomization.yaml bundled adaptive_export_secrets.yaml (placeholder pixie-api-key)
with the role+deployment, so EVERY infra re-apply (make log4j) clobbered the real
key that ae-auth had written. Separation of concerns: remove the secret from the
kustomization — infra (role+deployment) stays re-appliable; the secret holds real
creds and is owned solely by `make ae-auth`, created once, never touched by infra
re-applies. Secret manifest kept as a hand-applied seed-only template (documented).

Signed-off-by: entlein <einentlein@gmail.com>

* dx-reduction harness: fire BOTH attack stages so DX steers the backend

The DX-steered arm was failing because the harness only fired stage-1 (JNDI/LDAP).
That generates ldap-egress but NO kubescape R0001 → backend never flagged → DX no
case → indeterminate → AE steers wrong/no pods. R0001 comes from stage-2 (post-
exploitation exec). fire() now does stage-1 (JNDI) + stage-2 (whoami/shadow/token/
getent in the backend) → kubescape R0001+R0006 → DX rules backend MALIGNANT →
backend enters AE activeSet → reduction is measurable. Verified live: DX evidence
unexpected-spawn+sensitive-file-read → verdict ruled_in generic=MALIGNANT.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: rename whitelist→allowlist across streaming path + add DX-steering diagnostics

Standing terminology rule: allowlist/blocklist, never whitelist/blacklist.
Pure rename (no behavior change) of the rev-3 streaming filter:
  FilterModeWhitelist  → FilterModeAllowlist
  MaxWhitelistSize     → MaxAllowlistSize
  ADAPTIVE_STREAM_MAX_WHITELIST → ADAPTIVE_STREAM_MAX_ALLOWLIST  (env)
  mode=whitelist log string → mode=allowlist
plus all comments/identifiers/tests in streaming, activeset, cmd/main.

DX-steering diagnostics (the reason DX-arm-writes-0 has been hard to RCA —
we could not tell "empty ActiveSet" from "broker returned 0 rows"):
  - scanner: log the empty-allowlist short-circuit (was silent) so an
    empty ActiveSet is visible in logs, distinct from "query completed rows=0".
  - FilterUpdater: emitted-filter log Debug→Info so the steered pod count
    per ActiveSet change is visible without debug logging.

NOTE: ADAPTIVE_STREAM_MAX_WHITELIST env renamed → tooling that sets the old
name must switch to ADAPTIVE_STREAM_MAX_ALLOWLIST.

Signed-off-by: entlein <einentlein@gmail.com>

* ae(clickhouse): create forensic_db.dx_attack_graph at boot

The Pixie dx_evidence_graph UI reads dx_attack_graph via px.DataFrame
clickhouse_dsn, whose query template hardcodes event_time + hostname and
ORDER BY event_time. A table without those columns fails 'Unknown
identifier event_time'; a table created by hand (local, not via the
operator) isn't globally registered. Fix: make AE own it like the other
forensic tables.

- schema.sql: dx_attack_graph DDL with event_time(UInt64 nanos) + hostname,
  edge columns, fromUnixTimestamp64Nano partition/TTL (nanos-correct).
- KnownTables + OperatorOwnedTables: register it so Apply creates it at boot.
- apply_test: assert last-applied DDL == last OperatorOwnedTables entry
  (robust to appended operator tables) instead of hardcoding trigger_watermark.

go test ./.../clickhouse green.

Signed-off-by: entlein <einentlein@gmail.com>

* ae(clickhouse): dx_attack_graph numeric cols Int64/Float64 (px-readable)

Pixie's clickhouse_dsn type mapper reads UInt8 as BOOLEAN and does not
handle UInt16/UInt32/Float32 -> px fails with 'Column[N] given incorrect
type' rendering the dx_evidence_graph. weight/max_severity/num_findings
-> Int64, confidence -> Float64 (map cleanly to INT64/FLOAT64). Verified
live: px run returns all 6 edges with every column. event_time stays
UInt64 (matches kubescape_logs, which px reads).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export(streaming): add #px:set max_output_rows cap flag to scanner buildPxL

The DX/streaming arm silently capped each per-table pull at Pixie's default
10000-row limit while the passthrough/ALL arm (pxl.CompilePassthrough /
QueryFor) already raises it to 1,000,000 via the broker's #px:set query flag.
Validated live on 6a33dac0: a single streaming http_events pull returned
exactly rows=10000 (the cap). Left unfixed this UNDER-counts the DX arm and
OVERSTATES the DX-vs-ALL volume reduction. Prepend the same #px:set directive
to the streaming scanner's PxL so both arms are uncapped and comparable.

Signed-off-by: entlein <einentlein@gmail.com>

* ae(clickhouse): create dx_attack_graph_malicious view at boot

Adds the rule-ins-only view (condition != '') to the canonical schema.sql,
registers it in KnownTables + OperatorOwnedTables (after dx_attack_graph), and
teaches DDL() to extract CREATE VIEW headers. AE now creates it on boot so the
dx_evidence_graph UI's default malicious-only read is standard, not a per-rig
manual step. Tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: entlein <einentlein@gmail.com>

* ae(control): /dx/attack_graph ingest endpoint -> ClickHouse

dx POSTs a JSON array of edges to /dx/attack_graph; AE writes them to
forensic_db.dx_attack_graph via JSONEachRow (Applier.WriteAttackGraph). Wired in
main.go when CONTROL_ADDR is set; 501 if the sink is unset. This is the AE half
of the dx->AE->CH attack-graph write path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: convention pass — consolidate CH HTTP, drop dead code, fix stale tests

PR-53 review follow-ups (see review summary in conversation):

1. License headers added to 17 src/e2e_test/adaptive_export_loadtest/
   harness/*.{sh,py} scripts. Matches the convention every other
   src/e2e_test/*/sh in pixie already follows.

2. Dead code removed (was unreachable per golang.org/x/tools/cmd/deadcode):
   - internal/script/script.go: IsClickHouseScript, IsScriptForCluster,
     GetActions, getScriptName, getInterval, templateScript, plus the
     ScriptConfig and ScriptActions types. The cron-script sync flow they
     served was replaced by the streaming model; only the Script and
     ScriptDefinition types remain.
   - internal/pixie/pixie.go: Client.GetPresetScripts (replaced by
     builtinPresetScripts() inline in cmd/main.go).
   - internal/streaming/{supervisor,writer}.go: SupervisorStats,
     TableStats, Stats types + Supervisor.Stats and BatchWriter.Stats
     methods (no production reader). Atomic counters dropped; the
     existing flush log preserves the per-flush summary.

3. Stale passthrough tests fixed.
   TestLoop_DefaultsTablesToPixieTables and TestNew_AppliesDefaults
   asserted len(clickhouse.PixieTables()) == 13 but passthrough.New
   strips excludedTables (http2_messages.beta), yielding 12. Tests now
   compare against filterExcluded(clickhouse.PixieTables()) and add an
   extra assertion that excluded tables were not written.

4. ClickHouse HTTP client consolidation: new internal/chhttp/ package
   collapses three near-identical HTTP CH clients
   (clickhouse.Applier, sink.ClickHouseHTTP, trigger.ClickHouseWatermarkStore)
   into one. Centralises endpoint validation, basic-auth header,
   30s default timeout, fail-loud INSERT settings (4 CH input_format
   knobs), and the X-ClickHouse-Summary read path. The 4 INSERT
   call sites in the three callers all route through chhttp.Client.Insert
   now; SELECT through Query or QueryStream (the latter preserves the
   QueryActive streaming behaviour). Net code: -200 LOC across the
   three callers plus a 200-LOC chhttp package with its own tests.

5. Pixie service scaffold wired into cmd/main.go:
   services.SetupService("adaptive-export", 50900) +
   services.SetupSSLClientFlags() + services.PostFlagSetupAndParse() +
   services.SetupServiceLogging(). Matches the pattern every other
   pixie Go service uses. CheckServiceFlags() is deliberately skipped
   (AE does not run a TLS gRPC server). Existing AE env-var reads
   (ADAPTIVE_*) are untouched and still authoritative for tuning knobs.

All 14 adaptive_export internal packages pass go test (1 new chhttp,
13 unchanged), including the 3 differential oracle tests in
pxl/compile_test.go. arc lint OKAY for everything except 3 pre-existing
findings unrelated to this commit (loadgen has its own go.mod; one
QF1001 De Morgan's-law nit in reconcile_test.go from c9f19b6).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: restore executable bits on harness scripts (post-header)

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: lint pass + restore @px load prefix in cmd/BUILD.bazel

User flagged on review 4536971862:
- cmd/BUILD.bazel:18 dropped the '@px' external-workspace prefix on
  pl_build_system.bzl load. Restored — the standalone AE build pulls
  pixie as @px and needs the qualifier.

Lint cleanup (PR-53 scope, no production code touched outside renames):
- chhttp/chhttp_test.go: errcheck on two w.Write calls + gofumpt on
  the gotSettings declaration.
- passthrough/reconcile_test.go: staticcheck QF1001 (De Morgan's law)
  on the conn_stats sink-drop assertion.
- script/script.go: rename ScriptId -> ScriptID (ST1003). Propagated
  to pixie/pixie.go and cmd/main.go callsites.
- internal/e2e/BUILD.bazel and internal/trigger/BUILD.bazel: gazelle
  drift — adding loadtest_test.go and clickhouse_internal_test.go to
  srcs.
- k8s/00-sinks.yaml + gen-pod.tmpl.yaml: yamllint compliance —
  document-start marker, dedent sequence items per .yamllint
  indent-sequences=false, tighten flow-mapping spaces, collapse
  multi-space after commas. YAML semantics unchanged.
- harness/stats.py: flake8 E501 — split a long line into two.

Final arc lint state on PR-53 file scope: 0 Errors, 14 Warnings
(SHELLCHECK SC2155/SC2086 in pre-existing harness scripts from
ae7b86f, not introduced or modified by this commit). Pre-existing
loadgen typecheck failures (separate go.mod) are unaffected.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: address user review #2 #4 #6 + 4 outstanding CodeRabbit items

User review 4536971862:

#2 passthrough/reconcile_test.go — strengthened with two new tests that
   exercise the FULL chain (loop → real sink.ClickHouseHTTP → httptest
   CH endpoint → reconcile recorder). TestTick_ReconcileCatchesCHSilentDrop
   mimics CH's X-ClickHouse-Summary silent-drop shape (200 OK,
   written_rows=0) and asserts the loop records WroteCount=0 with a
   silent-drop attribution — the exact R6 (sink-layer loss) regression
   the instrument exists to detect. TestTick_ReconcileAttributesCHFailureCorrectly
   covers the 500-response branch. The pre-existing in-process-fake test
   stays as the wiring check.

#4 pixie/pixie.go — added a long comment justifying the API-key auth
   choice. pixie.Client targets the Pixie CLOUD (cloudpb's PluginService),
   whose auth interceptor accepts pixie-api-key and rejects JWT service
   tokens (those are for INSIDE-cluster vizier services). The
   pixieapi.Adapter that talks to vizier directly already uses JWT via
   jwtutils.GenerateJWTForService — same pattern as
   cloud_connector/vizhealth/checker.go:111. So the split is intentional;
   flipping pixie.go to JWT would break cloud auth, not improve it.

#6 pxl/queryfor — hardened escapePxL so a raw \n, \r, \t or NUL byte
   in Target.Pod/Target.Namespace can't terminate the PxL string literal
   and inject a new statement. Added TestQueryFor_RejectsInjectionInTargetFields
   driving QueryFor with 7 adversarial pod/namespace shapes (newline,
   single-quote-only, CR, backslash-escape-of-escape, NUL, tab) plus
   the regex_match fallback path, asserting the output line count and
   every statement's leading token. Extends existing
   TestEscapePxL_TableDriven with the new escape mappings.

CodeRabbit oversights (verified each against current code):

CR r3379377432 (main.go) — wrapped the control-surface listener in
   http.Server with Read/ReadHeader/Write/Idle timeouts so a slow
   client can't pin a goroutine indefinitely.

CR r3379377607 (pixieapi.go) — switched direct-mode dial from
   pxapi.WithDisableTLSVerification (env + addr-gated, brittle) to
   pxapi.WithDirectTLSSkipVerify (added in PR #49 b523ce3 for the
   same node-IP-dial scenario). Removed the cluster.local + PX_DISABLE_TLS
   precondition in NewDirect; the always-skip semantics match the AE
   deployment shape. Refactored the obsolete env-gate test.

CR r3379377645 (streaming/filter.go) — the deltaCh-close path returned
   without calling disarm(), leaking the timer's goroutine on shutdown.
   Now calls disarm() before return on chan-close.

CR r3426923299 (sink/clickhouse.go Record) — capped Record at a 2s
   per-call context timeout. The 30s chhttp default was too long for
   the scanner/passthrough/controller hot paths that call Record inline;
   reconcile is best-effort by contract, so a stalled CH must not pin
   the pull loop.

All 14 AE packages pass go test. arc lint: 0 Errors on PR-53 file
scope.

Signed-off-by: entlein <einentlein@gmail.com>

* test(harness): consolidate to one run-picture + e2e CI workflow

Finish the harness consolidation #53 started: adopt exp_matrix.sh (canonical
ALL-vs-DX reduction matrix) + nfr.sh (throughput/mem/verdict-latency) as the
single runners; cut the overlapping variants (ae_vs_all, exp_datavolume_extreme,
exp_dx_steering_reduction, exp_ae_nfr_benchmark, exp_pipeline_reconcile) and the
superseded standalone setup (deploy_ae, build_gen_image). README rewritten as the
single 'how to run' source: two families — fixture-isolation (run.sh + E-series)
and live-attack e2e (log4shell_fire → exp_matrix → nfr → exp_row_reconcile).

Add e2e_log4shell_soc.yaml: k3s + full SOC stack (Pixie/kubescape/ClickHouse/AE/dx
via k8sstormcenter/soc) on the oracle runner; asserts every canonical harness
script runs + dx rules in; profiles dx in real life (pprof CPU/heap + verdict
latency, uploaded). Uses existing repo secrets.

Signed-off-by: entlein <einentlein@gmail.com>

* revert(bazel): drop stray buildifier attribute-reorder in stirling container_images BUILD

This file is unrelated to adaptive_export — the only change was buildifier
alphabetizing container_type/bazel_sdk_versions (no functional change). Restore
main's version to keep #53's diff to real AE changes.

Signed-off-by: entlein <einentlein@gmail.com>

* ci: fix run-genfiles + run-container-lint on PR 53

run-genfiles: the PR had reordered the kwargs in stirling/.../container_images/
BUILD.bazel alphabetically (bazel_sdk_versions before container_type) — a
local buildifier or gazelle drift from when the file was first committed.
CI gazelle wants the original order (container_type first), so the diff
loop fails. Restored to origin/main's ordering. Local gazelle disagrees
with CI's expected output (tooling-version drift); CI is authoritative.

run-container-lint: two findings.

  1. staticcheck QF1001 (De Morgan's law) in
     pxl/queryfor_test.go:318. Rewrote the !(a || b || c || d) negated
     disjunction as the equivalent !a && !b && !c && !d. Test behaviour
     unchanged; verified locally with TestQueryFor_RejectsInjection.

  2. golangci-lint typechecking failed on
     src/e2e_test/adaptive_export_loadtest/tools/loadgen/cmd/{cleanloadgen,
     httpsink}/main.go because that subtree carries its own go.mod and
     does not resolve as a package under the root px.dev/pixie module.
     Added the loadgen subtree to .arclint's exclude list. Same fix the
     existing entries for other-module subtrees apply.

Local 'arc lint' on the PR-53 file scope: 0 Errors, 14 SHELLCHECK
Warnings + 26 SHELLCHECK Advice (all pre-existing in ae7b86f
harness scripts; not introduced by this PR).

Signed-off-by: entlein <einentlein@gmail.com>

* ci: apply gazelle's actual kwarg order to stirling container_images

run-genfiles CI re-failed after f244ffc reverted this file to main's
ordering — turns out gazelle on this repo IS alphabetizing the kwargs
(bazel_sdk_versions before container_type), and CI runs 'gazelle fix'
then 'git diff' to catch any drift. So main's ordering is no longer
gazelle-stable; the file has to match gazelle's preference, not main's.

Verified locally with 'bazel run //:gazelle -- fix'; the file is now
idempotent (a second gazelle run produces no diff).

Signed-off-by: entlein <einentlein@gmail.com>

* ci: fix container-lint Errors on e2e workflow + new harness scripts

run-container-lint re-failed after the run-genfiles fix because two
files added on this branch (a03aa15) had unfixed lint errors:

.github/workflows/e2e_log4shell_soc.yaml — 5 yamllint Errors:
- 1 indentation: list items under steps: must be parent-aligned per
  the repo's .yamllint config (indent-sequences: false), not 2-indented.
  Dedented every step item + its run: block by 2 spaces.
- 4 line-length (>120 chars): split the long kubectl-set-image, the
  long grep-detection gate, the curl pprof URL, and the verdict-latency
  grep across continuation lines. Semantics unchanged.

src/e2e_test/adaptive_export_loadtest/harness/{exp_matrix.sh, nfr.sh}
— missing Apache headers. Applied via arc lint --apply-patches; exec
bits restored.

Local arc lint on PR-53 file scope: 0 Errors, 14 SHELLCHECK Warnings
+ 26 Advice (all pre-existing in harness scripts, unchanged).

Signed-off-by: entlein <einentlein@gmail.com>

* ci: silence 6 SHELLCHECK Warnings to clear container-lint exit code

arc lint --apply-patches exits non-zero on Warning level too. Resolved
each: replaced unused 'for i/t in ...' loop vars with '_', split a
SC2155 declare-and-assign, dropped two never-referenced hip/pip
assignments.

Signed-off-by: entlein <einentlein@gmail.com>

---------

Signed-off-by: entlein <einentlein@gmail.com>
Co-authored-by: Entlein <eineintlein@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pixie-agent <noreply@local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants