Skip to content

M1 walking skeleton: end-to-end MVP (collector → Redpanda → pipeline → Postgres → gateway → web) - #1

Merged
Karthik-Sethu-Raman merged 18 commits into
mainfrom
worktree-m1-walking-skeleton
Sep 19, 2026
Merged

Karthik-Sethu-Raman merged 18 commits into
mainfrom
worktree-m1-walking-skeleton

Conversation

@Karthik-Sethu-Raman

Copy link
Copy Markdown
Owner

M1 walking skeleton — complete

docker compose up on one machine → simulated multi-vendor traffic → collector → Redpanda → pipeline (idempotent raw-first persistence + per-partition hash chain + parsing) → Postgres → gateway → React Overview with live SSE feed and raw traceability.

What's in here

  • libs/ulpf-core — single implementation of fingerprinting (shape-hash unknowns), parsing (regex timeout, OCSF allow-list), deterministic UUIDv5 ids
  • libs/ocsf-schema — curated OCSF Network Activity subset + validator
  • services/collector — syslog UDP/TCP :5514 + HTTP :8080 → raw.logs
  • services/pipeline — sole consumer group; raw persisted before parsing in the same batch; staged COPY + ON CONFLICT for replay idempotency; seek-back on failed batches (lossless); validated seed rules; DLQ; canary
  • services/gateway — stats / events / raw-trace / SSE / chain-head API (contract frozen at M1)
  • web/ — React 19 + TS + Vite Overview page, Caddy-served, air-gapped (no CDN)
  • simulator/ — steady-mode traffic player over the golden corpora
  • deploy/ — compose stack, least-privilege DB roles, migrations, topic init
  • scripts/smoke.py — acceptance gate: lossless counts, parsed/unparsed split, 20/20 traceability with recomputed hashes, forced-replay idempotency (consumer-group delete → replay → raw counts unchanged, chain appended), full independent verification of every merkle root
  • legacy-demo/ — the original hackathon demo, moved here frozen (history preserved via git mv)

Verification

  • pytest 102 passed, ruff clean, web tests + build green
  • Smoke 5/5 PASS end-to-end on the real compose stack (rebuilt images), sustained across repeated runs on an accumulating DB
  • Final whole-branch review + scoped re-review: clean

Quickstart

cd deploy && docker compose --profile sim up -d --build
python scripts/smoke.py # needs psycopg[binary] on the host
open http://localhost:3000

Postgres publishes on host 5433 (5432 is typically taken by a local Windows Postgres).

M2 (onboarding loop + SLM) builds on the frozen API contract from here.

Karthik-Sethu-Raman and others added 18 commits September 19, 2026 11:50
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…n bench

Known-format ids (cef_<vendor>, leef_<vendor>, json, syslog, csv, xml) port
the legacy-demo heuristics and add csv/xml branches; unknown lines get a
provisional auto_<8hex> id from their shape hash so no shared unknown bucket
exists (spec §9). Shape hashing collapses numerics, empty key=value values
and free-text runs so same-format events hash identically; the bench measures
this with 500 seeded mutants across 5 golden corpora (100% stable) and fails
on known-format fragmentation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…imeouts

libs/ocsf-schema ships the curated Network Activity subset (draft 2020-12,
root additionalProperties:false) and validate_document() for activation-time
and canary use only (spec §6.4); time/severity_id/action/message are nullable
so timestamp-less rules validate pre-pipeline, and jsonschema rides as the
optional `activation` extra. ulpf_core.parsing is the one implementation
everything imports: legacy apply_rule ported onto the regex module with a
per-match ms timeout (spec §10), allow-listed mapping targets enforced at
document build, safe severity/port casts (fixes the demo hot-path crash),
full OCSF document shape, and EXTENSION_KV as the single shared grammar for
M2 onboarding. validate_rule_output adds pattern length/group/nesting caps,
compile, allow-list, sample-parse and document-validation checks for the
human activation gate.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ation

Review findings on 0d4ffe7: the extension key=value scan ran un-timeouted on
untrusted line text — a crafted 32KB CEF extension stalled parse() ~3s under
the default 50ms timeout (quadratic backtracking of (\w+)= against an a-wall);
the scan now runs under the same ms-scale timeout and a timeout returns the
standard error (spec §10). Activation validation now fails closed when
ocsf-schema is absent (schema validator unavailable -> error, never a silent
pass); the validator's internal jsonschema fallback is unchanged. Port casts
keep only string values (dict/list -> None), and the exact OCSF_TARGETS
contract is pinned by a committed test.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…logs

Wraps every inbound line in a RawEnvelope and produces it to raw.logs,
keyed by source_id; zero parsing (spec §7). UDP datagrams and TCP lines
on :5514 derive source_id from the peer address (udp-<ip>/tcp-<ip>,
controller R7 ruling — no device registry until post-M1); HTTP
POST /v1/ingest takes the request-supplied source_id and optional
format_hint. Envelopes are validated against RawEnvelope before produce;
oversized/empty lines -> HTTP 422, produce failures -> HTTP 503 /
syslog drop with error log (M1 queue-down deviation; spooling is M3).
Tests use an injected fake producer with UDP disabled; root .dockerignore
(R12) keeps build contexts from shipping the whole worktree.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…se, normalized emit

Sole raw.logs consumer (group id exactly "pipeline" for Task 12's forced
replay, R3b). Per 500-msg/2s batch, spec §4 ordering: build_rows assembles
RawRows with fingerprint_id classified at build time (R3 — pipeline_role
has no UPDATE) -> persist_batch commits one txn of staging COPY ->
INSERT .. ON CONFLICT (raw_id, received_at) DO NOTHING -> per-partition
raw_batches append (batch_seq = COALESCE(MAX,0)+1, prev_hash -> prior
merkle_root, genesis 64 zeros, ordered row_hashes JSONB) -> active rules
refreshed -> parse (cap re-checked; parse_error + pipeline.dlq with
raw_id/fingerprint/error; canary validates every 100th parsed doc) ->
normalized.events produced keyed fingerprint_id -> persist_normalized
committed on (event_id, parsed_at) -> only then consumer.commit().
parsed_at := raw received_at so replays collide instead of duplicating;
superseded_by_event_id stays NULL in M1 (R9). psycopg/confluent_kafka
import lazily so unit tests run without them (R14). Dockerfile installs
both libs editable (R15); regex~=2024.7 (R8); redpanda-init.sh topics
--if-not-exists (R10); root pythonpath gains services/pipeline (R13).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…seeded)

Co-Authored-By: Claude Code <noreply@anthropic.com>
… head

Co-Authored-By: Claude Code <noreply@anthropic.com>
…p by_fingerprint

Review fixes: _json_default now serializes uuid.UUID (dict_row rows carry
UUID objects; SSE died on the first real row), zero_fill_statuses consumes
the {"status","n"} dict rows the query actually returns, by_fingerprint is
capped at 500 (R20), limit=500 boundary accepted, and test fixtures rebuilt
to mirror real psycopg dict_row shapes so this bug class gets caught here.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
scripts/smoke.py drives the live stack: lossless ingest (delta-based, DB never
reset), per-fingerprint parsed/unparsed split via /api/stats, raw traceability
(HTTP /raw + SQL join with recomputed sha256), forced Kafka replay (group
deleted after the consumer stops, pipeline restarted: raw_events unchanged,
raw_batches grows), and an independent recomputation of every raw_batches
merkle root plus full per-partition chain linkage from genesis.

Also: R19 hardening — _is_xml catches RecursionError alongside ParseError so a
deeply-nested hostile line falls through to the shape hash instead of wedging
a pipeline batch; redpanda-init.sh creates missing topics via list-then-create
(v24.1.7 rpk has no --if-not-exists flag); Postgres publishes on host 5433 so
host connections cannot land on a native Postgres owning 5432; ruff made
repo-clean for CI (frozen legacy-demo excluded; import-order, blind-except and
small dead-variable fixes); CI workflow (py3.12 ruff+pytest, node 22 web).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… review

_classify recursed once per '=' in a token, so a hostile single token
('a=' * 1200) raised RecursionError out of shape_hash and wedged a pipeline
batch — the failure class R19 fixed in _is_xml. Classification now loops
partition('=') with identical semantics (KV prefixes, empty value -> W);
golden + mutation-stability suites green unchanged.

Also per review: smoke host deps stated as psycopg[binary] only (Kafka
interaction is rpk via docker compose exec, not confluent_kafka); the
MEMBERS-0 wait tracks saw_members_zero and reports timeouts truthfully;
group-exists reuses group_listed(); CI pins ruff==0.16.8 and installs
simulator requirements; corpora-total comment corrected to 56. Plus:
compose() decodes docker output as UTF-8 explicitly — the Windows ANSI
codepage crashed a subprocess reader thread on build output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er final review

CRITICAL-1: consume() advances the consumer position regardless of
processing success and no-arg commit() commits that position, so a failed
batch (rollback, no commit) was silently covered by the next successful
batch's commit — its events never redelivered. Every failure path
(persist_batch, persist_normalized, the R16 catch-all) now seeks each
affected partition back to the failed batch's first offset before the loop
continues; fake-consumer unit tests assert the exact seeks and zero commits
on both persist-failure paths and the run()-level crash.

CRITICAL-2: _known_format's JSON probe caught only JSONDecodeError, but
deeply-nested hostile text (17,995 chars, under the 65,536 cap) raises
RecursionError out of json.loads, escaping build_rows via fingerprint_id and
blowing up the batch — silent loss before C1, infinite redelivery wedge
after; landed together. Regression tests at the fingerprint and build_rows
layers, red first.

Also: worker comments/docstrings now describe the actual seek-back
mechanism; compose notes the single-replica requirement for M1 batch_seq
safety (scale-out = M3 with fencing/epochs); README quickstart points at
--loop sustained traffic for the dashboard.

Co-Authored-By: Claude Code <noreply@anthropic.com>
ruff 0.16.8 EXE001 fired on the linux runner for bench/fingerprint_fragmentation.py,
deploy/migrate.py, scripts/smoke.py — each carries a deliberate #!/usr/bin/env python3
but was committed mode 644 (staged from Windows, where the exec bit is invisible to
every local check). Set the executable bit via git; file content unchanged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…py3.13

CI runs CPython 3.12, whose C json scanner parses depth-3000 nesting
without error, so the Final-C2 regression tests (written against 3.13,
which raises there) passed vacuously on the runner. Depth 10,000 raises
RecursionError on both generations, and the 60,001-char line stays under
the 65,536-char envelope cap, so it still exercises the ingest path.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@Karthik-Sethu-Raman
Karthik-Sethu-Raman merged commit 1da239a into main Sep 19, 2026
4 checks passed
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.

1 participant