Skip to content

Port #1610's remaining named workers to the Rust tier - #1614

Merged
Xore merged 5 commits into
port-foundationfrom
worktree-issue-1610-worker-migration
Aug 18, 2026
Merged

Port #1610's remaining named workers to the Rust tier#1614
Xore merged 5 commits into
port-foundationfrom
worktree-issue-1610-worker-migration

Conversation

@Xore

@Xore Xore commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

Ports the four workers explicitly named in #1610's inventory (beyond notifyLoop/reportScheduleLoop, already done via #1612/#1613) into the Rust backend-service tier, on top of port-foundation (post-#1613).

  • es-results-importer (Python → Rust) — mirrors ~14 host-side JSON/binary result sources (Ghidra/sandbox/GitHub-analysis/revdeck/CAPE results, Ghidra report/callgraph artifacts, sandbox export PCAPs/diagnostics, cowrie ttylogs, reporter metrics, YARA aggregate results) into ES via bulk indexing, with mtime-based dedup state and horizontal sharding preserved. New backend-worker-importer compose service — kept separate from backend-service-mounted/backend-worker because it genuinely needs root+DAC_READ_SEARCH (root-owned Ghidra/GitHub-analysis result dirs, a real requirement copied from the existing Python service's own compose block) plus a persistent local dedup-state file, both of which conflict with backend-worker's stateless/unprivileged design.
  • attacker-identity-worker (Go → Rust) — the ≥2-of-3-signal (fingerprint/payload-sha256/credential-pair) durable entity-merge algorithm, plus the 4-index verdict join (ghidra/sandbox/github-analysis/revdeck). Pure ES, no host mounts — added to the existing backend-worker. Added a real point-in-time + search_after pagination primitive to es.rs (Elasticsearch's default 10k-result window was a genuine gap — every existing query in this crate was a single bounded search).
  • agent-intrusion-worker (Python → Rust) — union-find campaign correlation + 12 deterministic security-detection rules + a bounded non-executing recursive decoder (base64/gzip/zlib/single-byte-XOR with provenance chain) + severity scoring. Pure ES, no host mounts — added to backend-worker.
  • payload-inventory-worker (Go → Rust) — payload directory discovery/classification/inventory, reusing payload_kind::classify_payload and payload_paths already built in Port backend remainder: submissions, orchestrator, reports generation, host-local signals, settings admin APIs #1612 rather than re-porting them. Slots directly into backend-service's existing WORKER_LOOPS + already-mounted payload directories — no new infrastructure needed.

Also fixed one real pre-existing bug found while researching: stores.rs's generic agent-campaigns passthrough sorted on a last_seen field that doesn't exist in that document shape; now sorts @timestamp, matching dashboard/agent_campaigns.go's own query.

Not done (per #1610's own remaining scope, not touched by this PR): the BFF scalability hard requirements (Node cluster mode/worker_threads, end-to-end backpressure, streaming responses, horizontal-readiness, sustained-SSE-fan-out load-test gate). The old Python/Go workers stay running unchanged — this is a parity port, not a cutover, per #1610's own text ("each migration ships with parity tests... before the old one retires").

Test plan

  • cargo build clean on every commit (no new warnings beyond one pre-existing unrelated canarytokens.rs warning)
  • cargo test — 94/94 passing across all four workers' ported test suites (deterministic algorithms — union-find merge, entity resolution, all 12 detection rules, the bounded decoder, merge-not-overwrite inventory logic — got real unit test coverage, not just a compile check)
  • docker compose --profile next config resolves all new/changed services cleanly
  • Cross-checked ES field names, index names, and document shapes against the existing Go/Python source and this crate's established conventions throughout
  • No live ES/services-adapter/host-mount environment was reachable in this sandbox — live smoke testing against the real stack, and the "parity tests against the current worker's observable outputs" Port follow-up: migrate every worker into the new BFF/service architecture; BFF scalability #1610 itself calls for, are still needed before any old worker retires

🤖 Generated with Claude Code

Xore and others added 5 commits August 18, 2026 21:21
Ports analysis/es-results-importer/importer.py (591 lines Python) into
the Rust backend-worker tier as a new src/es_importer.rs module and a
new "es-results-importer" WORKER_LOOPS entry, faithfully preserving the
full SOURCES table (14 explicit + 9 generated chunked sandbox-export
sources), mtime-based dedup state, sha256(path)%SHARD_COUNT horizontal
sharding, and the correctness-critical advance_state_after_bulk
all-or-nothing-per-key semantics (unit tested). Added a bulk_index
primitive to es.rs (elasticsearch crate's BulkOperation API), returning
per-item failed ids rather than a single pass/fail flag, since a
chunked/aggregate-sample file's multiple bulk operations sharing one
dedup key need per-operation success to decide whether that key's mtime
advances.

Tier decision: a new compose service (backend-worker-importer), not
folded into backend-worker or backend-service-mounted. This worker
needs root + DAC_READ_SEARCH to read root-owned host result directories
(same requirement the Python service's own compose block already has)
and a persistent local dedup-state file, both of which conflict with
backend-worker's stateless/unprivileged-by-design posture. The existing
Python es-results-importer service stays running unchanged — this is a
parity port behind the `next` profile, not a cutover, per #1610's own
text ("each migration ships with parity tests... before the old one
retires").

Note: src/main.rs and src/worker.rs also carry a small, independent
`attacker-identity` WORKER_LOOPS registration from a concurrently
landed port of a different #1610 worker (src/attacker_identity.rs,
not part of this commit) — both additions are non-overlapping,
non-conflicting one-liners/match-arms in the same shared files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports honeypot-attacker-identity-worker (1668 lines Go across
main.go/fetch.go/identity.go/verdicts.go/es.go) into the Rust
backend-service tier as src/attacker_identity.rs, wired as a new
"attacker-identity" WORKER_LOOPS entry.

Faithfully preserves the deterministic entity-merge algorithm: IPs merge
into durable attackers-v1 entities only on >=2 shared signal categories
(fingerprint/payload-sha256/credential-pair), never on one alone; entities
are never deleted for going quiet; same-cycle transitive multi-way merges
are folded together; deterministic sorted-IP iteration order for
reproducible merge results. 7 unit tests port identity_test.go's coverage
(single/double signal-count merge behavior, transitive same-cycle merges,
absorbing a pre-existing entity, untouched entities aren't rewritten,
credential-pair validation, signal intersection). Also ports the
verdicts.go join (ghidra/sandbox/github-analysis/revdeck lookups per
payload hash).

Added a real correctness fix along the way, not just a straight port: the
Go worker's PIT+search_after pagination (openPointInTime/docScrollAll) was
missing from this crate entirely — every existing query here does a single
bounded search, which would hard-fail past Elasticsearch's default 10,000
index.max_result_window once the event window or attackers-v1 exceeds it.
Added Es::search_paginated (PIT-based, verified against the elasticsearch
crate's actual OpenPointInTime/ClosePointInTime API) and used it for both
the event fetch and the existing-entity load.

Tier decision: this worker is pure ES (no host mounts, no local state), so
it runs on the existing backend-worker service (already stateless-by-design)
rather than a new one — unlike es-results-importer (concurrently landed),
which needed its own service for root+DAC_READ_SEARCH and a persistent
dedup state file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nt field

Was sorting the "agent-campaigns" store entry (agent-intrusion-campaigns
index) by "last_seen" -- that field doesn't exist in this index's
document shape (see build_campaign_verdict in the Python worker: @timestamp/
campaign_id/start/end/severity/matched_categories/correlation_identifiers/
event_count/events, no last_seen). dashboard/agent_campaigns.go's own
refreshAgentCampaigns sorts @timestamp:asc; matched that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports honeypot-agent-intrusion-worker's Python pipeline (worker.py/
campaign_correlator.py/criticality_rules.py/decode_correlate.py) into the
Rust backend-service tier as a new `agent-intrusion` WORKER_LOOPS entry
on the existing backend-worker service (pure ES, no host mounts, no
local state -- same shape as attacker-identity).

- decode_correlate.rs: bounded, non-executing recursive decoder
  (base64 -> gzip/zlib -> single-byte-XOR-then-gzip) with a provenance
  chain, plus candidate-blob extraction and chunk-message parsing.
  ChunkCorrelator itself is not ported -- confirmed by grep that nothing
  in the live per-event rule pipeline instantiates it, only the free
  parse_chunk_message function is used live.
- campaign_correlator.rs: union-find campaign correlation over shared
  identifiers (session/src_ip/host/channel), gated by a 72h window.
- criticality_rules.rs: all 12 deterministic detection rules from
  ALL_RULES, campaign_severity scoring, and the two-stage
  campaign_breadcrumb_followed check.
- agent_intrusion.rs: fetch/normalize/correlate/score/write orchestration
  against real ES data, using es.rs's search_paginated (added by the
  sibling attacker-identity-worker port) for the fetch past ES's default
  10k result window.

Every event's `raw` field is the whole raw sensor sub-document
(source.honeypot / source.suricata.eve / bare _source) -- not this
crate's usual flattened honeypot.canonical_* convention -- since the
correlator and every rule read sensor-native field names directly,
ported faithfully from the Python corpus/production fixtures.

New dependencies: flate2 (gzip/zlib decompression, no std equivalent),
data-encoding (base32, for the DNS-label exfil detection path).

89 unit tests total (up from 68), ported from test_campaign_correlator.py,
test_criticality_rules.py (fixture-based cases; corpus.jsonl-dependent
cases deliberately not ported), and test_decode_correlate.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Last of #1610's worker-migration inventory. Reuses payload_kind::classify_payload
and payload_paths::payload_dirs (both already ported for submission/workbench
use in #1612 phase 3a) rather than re-deriving either — the "yara" half of
#1610's "payload/yara pipeline hooks" item was already covered by
es-results-importer's aggregate_samples source, so this worker is the whole
remaining scope.

Every SCAN_INTERVAL (default 5m), walks PAYLOAD_DIRS, classifies each
hash-named file, hex-dumps a preview, cross-directory-dedups by hash, and
writes dashboard-payload-inventory-v1 (merge-onto-existing, never a blind
overwrite -- dashboard-added GitHubAnalysisURL/GitHubAnalysisLabel fields
must survive an unrelated rescan) plus dashboard-payload-bytes-v1 (the
proactive counterpart to payload_bytes.rs's on-demand self-heal mirror).

New WORKER_LOOPS entry "payload-inventory" lands on backend-service, not a
new service -- it already mounts PAYLOAD_DIRS read-only for the bytes
self-heal path, so this is a second consumer of an existing mount, not a
new trust boundary.

MIME detection is a coarse approximation from classify_payload's own
category/magic-byte checks, not a full port of Go's http.DetectContentType
(a WHATWG-sniffing-spec implementation this crate has no dependency for) --
informational-only field, not a security or routing decision.

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

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
See the Details below.

License Issues

arcane/home/honeypot-dashboard/backend-service/Cargo.toml

PackageVersionLicenseIssue Type
flate2>= 1.0.0, < 2.0.0NullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
cargo/data-encoding 2.11.1 🟢 4.3
Details
CheckScoreReason
Code-Review⚠️ 1Found 3/30 approved changesets -- score normalized to 1
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained🟢 34 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 3
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Packaging⚠️ -1packaging workflow not detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
cargo/flate2 >= 1.0.0, < 2.0.0 UnknownUnknown

Scanned Files

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

@Xore
Xore merged commit 7f936bd into port-foundation Aug 18, 2026
88 checks passed
@Xore
Xore deleted the worktree-issue-1610-worker-migration branch August 18, 2026 19:51
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