Skip to content

Full-stack ES store coverage audit — implement into the Rust/BFF tier (#1611) - #1617

Merged
Xore merged 7 commits into
port-foundationfrom
worktree-issue-1611-es-audit
Aug 18, 2026
Merged

Full-stack ES store coverage audit — implement into the Rust/BFF tier (#1611)#1617
Xore merged 7 commits into
port-foundationfrom
worktree-issue-1611-es-audit

Conversation

@Xore

@Xore Xore commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

Implements all 7 workstreams from #1611's live-cluster audit directly into backend-service (the Rust/BFF tier), in the suggested order (A → C → B → D → E → G → F):

  • A — per-sensor event detail lines (event_detail.rs, detail_for()), wired into both /api/v1/events and the SSE live stream; suricata alert/http/tls/ssh/smtp/dns/fileinfo rows now render richly instead of empty, and flow/netflow/stats no longer swamp the default view.
  • C — login/auth-kind completeness: a shared logins_filter() now covers cowrie's cowrie.login.{success,failed} (1.26M previously-uncounted docs) and rdp-honeypot's connect+username auth, alongside the existing login/auth_attempt vocabulary.
  • B — mailoney email visibility: es_importer.rs mirrors .eml files into mailoney-mail-v1 (sha256(filename)-keyed); new GET /api/v1/mail/{session_id} joins session_id → body_path → bytes and parses via the mail-parser crate (bodies as plain text, attachments as metadata only — no auto-render, no bytes).
  • D — full ATT&CK technique promotion in ip-enrichment-worker: T1190 (web exploit), T0886/T1692.001 (ICS interaction/write), and empty-credential T1110 login attempts are now tagged at ingest; kill_chain.rs's query-time supplemental filter aggregations are removed.
  • E — 10 concrete gaps from the audit: portbridge OS/ports-touched joined into /api/v1/investigate/ip/{ip}, new /api/v1/ml-health and /api/v1/gpu-queue endpoints, new /api/v1/revdeck/{sha} (previously missing entirely), plus documentation for items that turned out to already be covered (dionaea incidents, Arkime community_id, reporter-stats, auth-events columns, worker-state indices).
  • G — credential hygiene: NUL/control bytes in cowrie usernames/passwords are now stripped at ingest (ip_enrichment/canonical.rs), not just display-side, so ES terms aggs stop splitting identical creds into separate buckets.
  • F — typed field promotion: the geoip-honeypot ingest pipeline now also copies held_ms → a real long field and passworduser.password (keyword). Proof: the endlessh histogram is now a genuine ES range aggregation (was client-side bucketing of up to 10k raw hits), and /api/v1/search's "HTTP paths" group does real substring wildcard matching against url.path (was prefix-only on the flattened field).

Test plan

  • cargo build and cargo build --release clean (only a pre-existing unrelated warning)
  • cargo test — 162 passed, 1 ignored (a scratch/manual-inspection test), 0 failed
  • elasticsearch-setup.sh: bash -n syntax check clean; both the ingest pipeline body and the honeypot-events-v2 template extracted and validated as well-formed JSON; Painless script brace/paren balance verified around both edit sites
  • docker compose config validates cleanly with the new MAILONEY_MAIL_DIR mount/env on backend-worker-importer
  • Live-cluster verification of the new ES template/pipeline fields (held_ms, user.password) once deployed — new fields only apply to newly-indexed documents, no backfill, same precedent as workstream D

🤖 Generated with Claude Code

Xore and others added 7 commits August 18, 2026 22:39
Ports dashboard/classify.go's ~1040-line per-sensor detail renderer
(all 21 sensors: cowrie, dionaea, multipot, every conpot persona,
endlessh, http-honeypot/api-honeypot, dicompot, sentrypeer,
rdp-honeypot, cisco-asa-honeypot, hellpot, beelzebub, dnp3,
dns-honeypot, citrix-honeypot, mailoney, canarytokens, tanner, galah,
elasticpot, wordpot) into events.rs::row_from_source, shared by the
list endpoint and the SSE live stream. Also adds brand-new suricata-v2-*
detail (alert/anomaly matching classify.go's proven logic exactly;
http/tls/ssh/smtp/dns/fileinfo are new — classify.go's legacy renderer
skipped every suricata event_type except alert/anomaly outright).
flow/netflow/stats stay excluded from the default events view (both
list() and live.rs's stream), matching that same legacy posture --
5700+/hour on a real deployment vs 50 alert/anomaly in the same window.

Also populates EventRow.proto/port from honeypot.proto/honeypot.port/
honeypot.dst_port when network.protocol/destination.port are empty --
multipot/conpot/dnp3 only ever carry them under honeypot.*.

Two corrections found during implementation, not blind translation:
- The issue's own audit table describes beelzebub's honeypot.event as
  an object, but classify.go actually reads flat lowercase fields
  (protocol/username/password/command/path) -- confirmed against
  ip_enrichment/sensors.rs's own beelzebub promotion (ported earlier
  this session), which writes exactly those flat keys. Read the flat
  fields, matching the proven-live legacy code, not the raw upstream
  shape ip-enrichment-worker already normalizes away.
- suricata's http.status is a JSON number in real eve.json, not a
  string -- caught by a unit test that would have silently rendered an
  empty status on every http detail line otherwise.

21 unit tests using literal fixtures shaped like the issue's own live-
sampled field examples.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…1611 workstream C)

honeypot.event ∈ {login, auth_attempt} missed most real auth activity
per #1611's own live-cluster audit -- cowrie logs
cowrie.login.success/failed on honeypot.eventid, never honeypot.event
(1.26M docs, the largest auth source, entirely uncounted before this),
and rdp-honeypot's auth rides on a plain "connect" event with a
non-empty honeypot.username, no dedicated login/auth_attempt kind of
its own.

Added es::logins_filter(), a shared bool.should query fragment (any of:
the original login/auth_attempt terms, cowrie's own eventid terms, the
rdp-honeypot+username combination, or honeypot.canonical_user's mere
existence -- a sensor-agnostic catch-all that improves automatically as
ip-enrichment-worker's canonical_user promotion coverage grows) and
used it in the three places that were duplicating the narrower filter:
aggregates.rs (sources page), dashboard.rs (overview KPIs),
reports_data.rs (report summaries).

Verified search.rs's "Credentials (usernames)" honeypot.username
aggregation does NOT need the same fix: cowrie's raw JSON only ever
carries a username key on login events in the first place, so a plain
terms-agg on that field is already correctly scoped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stream D)

attck.go's own header deliberately deferred T1190 (web exploit) and the
ICS pair (T0886/T1692.001) to "the ES-coverage round" -- this is that
round. promote_attck_technique_fields now also reads raw per-persona
fields directly (path, proto, app_function, request, event-kind), not
just canonical_* fields, since conpot/dnp3 have no canonical-field
promotion case at all and several bespoke sensors only promote a subset.

- T0886/T1692.001: every conpot persona and dnp3 tag T0886 outright;
  T1692.001 escalates on a write/command (dnp3 app_function present,
  conpot non-empty request, or an explicit command/write event kind).
- T1190: a request path combined with a query string, path-traversal/
  wp-/php marker, or an explicit exploit-tagged event (CVE payload
  captures, cisco's method_pri, elasticpot's attack event, a real tanner
  detection past its "index" default).
- T1110 gap: also fires on a login *attempt* regardless of whether
  credentials ended up non-empty (cowrie.login.failed with an empty
  password is the dominant case) -- matches the legacy dashboard's
  IsLogin || HasCredential gate, not credential-presence alone.

Wired into all 7 enrich functions (the generic path plus every bespoke
per-sensor one -- dionaea-incident/beelzebub/hellpot/galah/sentrypeer/
wordpot), not just the ones that already had canonical-field promotion.

kill_chain.rs's three query-time supplemental filter aggregations
(ics/ics_write/web_exploit -- narrower approximations forced by
honeypot.*'s flattened mapping) are removed now that a real promoted tag
covers them; technique_counts is a plain terms aggregation again.
Historical documents predating this change won't carry the new tags
until reindexed/rolled over -- not backfilled here.

10 new unit tests covering every new branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.eml bytes only ever lived on disk; the ES event stream carried
size/body_path but nothing queryable. es_importer.rs's new
mailoney_mail source mirrors .eml files into mailoney-mail-v1 keyed
by sha256(filename) (mailoney's own filenames carry no content-hash
guarantee, unlike cowrie's ttylog convention). New GET
/api/v1/mail/{session_id} does the two-step join: honeypot-v2-*'s
mail-body event for body_path, then mailoney-mail-v1 for the bytes,
parsed via the mail-parser crate rather than hand-rolled MIME/RFC5322
parsing since the input is attacker-controlled. Bodies surface as
plain text only and attachments as metadata (name/type/size/sha256)
without bytes, so the endpoint can't become an HTML-render sink or a
malware distribution point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes the audit's 10 concrete gaps:
- E.2: portbridge-v2-* OS/first-last-seen/ports-touched joined into
  /api/v1/investigate/ip/{ip} (p0f's the only ground truth for which
  ports a tunneled-sensor IP actually knocked on).
- E.3: document that dionaea-incidents-v1-* needs no separate endpoint
  (same incidents already render via honeypot-v2-*, Workstream A).
- E.4: document that network.community_id (the Arkime pivot key) is
  already in the record pane's full-doc clone.
- E.5: new GET /api/v1/ml-health — last retrain outcome per model.
- E.6: new GET /api/v1/gpu-queue — read-only queue visibility (the
  audit's own example: 2 stuck queued jobs, previously invisible).
- E.7: document that /api/v1/reporter-stats already satisfies this.
- E.8: new GET /api/v1/revdeck/{sha} — was entirely missing, so a
  revdeck error state rendered as a blank page.
- E.9: document that the auth-events store passthrough already
  carries every field a frontend column addition would need.
- E.10: document worker-state indices as intentionally unsurfaced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Telnet attackers embed NUL/control bytes in usernames/passwords; these
were only ever cleaned display-side (dashboard.rs, investigate.rs),
so ES terms aggregations, attackers-v1's credentials list, and search
grouping all saw "root" and "root\0\0\0" as distinct buckets. Strip
both raw NUL bytes and the literal "\x00" text form (same two shapes
dashboard.rs's clean() already defends against) in
ip_enrichment/canonical.rs's cowrie login promotion, before the
enriched line is written and before canonical_user/canonical_pass are
derived from it. Display-side cleanup stays in place for documents
written before this fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
honeypot.* is mapped `flattened` for good reason (heterogeneous
per-sensor shapes) but that means no stats/range aggs and no wildcard
queries on its leaves. The geoip-honeypot ingest pipeline already
copies a few hot fields into real typed ECS fields at ingest time
(user.name, process.command_line, url.path); this extends that same
mechanism to the two fields the audit called out:

- held_ms -> a new top-level `held_ms` (long): the endlessh held-time
  chart no longer bucket-counts up to 10k raw hits client-side, it
  runs a real ES `range` aggregation.
- password -> user.password (keyword), alongside the existing
  user.name, for future prefix/wildcard credential hunting.

Proof of the wildcard side: /api/v1/search's "HTTP paths" group moved
off flattened honeypot.path (prefix-only) onto url.path (wildcard,
already pipeline-populated from honeypot.path/url) — genuine substring
path hunting, not just prefix matching.

Template/pipeline changes only affect newly-indexed documents; the
datastream rolls daily so coverage is close to immediate. No backfill,
same precedent as workstream D's technique promotion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails
cargo/hashify 0.2.9 UnknownUnknown
cargo/mail-parser 0.11.6 UnknownUnknown

Scanned Files

  • arcane/home/honeypot-dashboard/backend-service/Cargo.lock

@Xore
Xore merged commit 281b3d3 into port-foundation Aug 18, 2026
88 checks passed
@Xore
Xore deleted the worktree-issue-1611-es-audit branch August 18, 2026 21:43
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