Skip to content

Port backend remainder: submissions, orchestrator, reports, host-local signals, settings admin (#1612) - #1613

Merged
Xore merged 10 commits into
port-foundationfrom
worktree-issue-1612-backend-port
Aug 18, 2026
Merged

Port backend remainder: submissions, orchestrator, reports, host-local signals, settings admin (#1612)#1613
Xore merged 10 commits into
port-foundationfrom
worktree-issue-1612-backend-port

Conversation

@Xore

@Xore Xore commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the majority of #1612 on top of port-foundation, in the Rust backend-service/backend-worker tier:

  • Settings admin backend APIs — services control (Unix-socket passthrough to hp-services-adapter), config revision history + rollback, audit trail, per-subject preference sync (GET/PUT /api/v1/preferences), reporter-stats passthrough, user-retention-sweep worker loop.
  • Alert notifier host-local signals — OT command alerts (T1692.001), log-stream size alerts, sandbox/ghidra/cape/github-analysis spool health, filebeat ingestion stats.
  • Mounted worker role — sandbox/ghidra/github-analysis submission endpoints, golden-image status, on-demand payload-bytes ES self-heal, and the full Payload Workbench orchestrator (recipes, run creation/reconciliation, child cancel/retry) on a new backend-service-mounted compose service.
  • Reports generation — PDF composer (now on vendored printpdf, swapped from an initial hand-rolled writer for long-term maintainability), definitions CRUD, ES-driven report data gathering, on-demand generate, and a scheduler worker loop. The three artifact-referenced templates (sandbox/payload/ghidra one-click reports) validate/save but generate returns 501 — their dedicated renderers weren't in scope for this pass.

Not included (see note below): issue #1612's "Misc write paths" section (honeyfs-implant credential passthrough, canarytokens fired-event ack beyond mint+download) — flagged to the requester as out of scope for this PR, open as follow-up.

Frontend/BFF wiring for the new endpoints (settings UI reading /api/v1/preferences instead of localStorage, Workbench UI, Reports studio write actions, the new backend-service-mounted internal URL) is left to the parallel visual-completion track per the issue's own coordination note.

Test plan

  • cargo build clean on every commit (no new warnings beyond one pre-existing unrelated canarytokens.rs warning)
  • cargo test — 10/10 passing (1 ignored scratch/manual-inspection test)
  • Generated real multi-page PDFs and validated with poppler (pdfinfo/pdftotext/pdfimages) — correct page count/dimensions, clean text extraction, both embedded emblems render as proper shared 1-bit stencil masks
  • docker compose --profile next config resolves all new/changed services cleanly
  • Verified ES field names/index names/query shapes against the existing Go source and this crate's own established conventions (events.rs/aggregates.rs/dashboard.rs) — caught and fixed two real discrepancies in review (risk-level scale, a wrong ES field name) before merge
  • Rebased onto the latest port-foundation (after the parallel visual-completion pass landed) with no conflicts; rebuilt and retested clean
  • No live ES/services-adapter/host-mount environment was reachable in this sandbox — live smoke testing against the real stack is still needed before cutover

🤖 Generated with Claude Code

Xore and others added 8 commits August 18, 2026 20:31
Services control (Unix-socket passthrough to hp-services-adapter),
config revision history + rollback, an audit trail, per-subject
preference sync (GET/PUT /api/v1/preferences, POST .../reset), reporter
stats passthrough, and a user-retention-sweep worker loop. All new
ES-backed stores work at the JSON Value level like the existing
config.rs, not the full typed dashboardConfig/behavior/honeypot patch
machinery. backend-service now mounts dashboard-state and
services-adapter-socket directly (tier decision recorded in compose.yml).

Deliberately out of scope for this pass: frontend wiring (BFF/UI still
reads localStorage for prefs), full typed config PATCH with pinned-field
source tracking and impact-classification preview, and ETag/If-Match
optimistic concurrency on preferences (nothing else in this tier has
that yet either).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the alert-notifier worker loop with the seven signals that need
a host mount or service URL rather than pure ES: OT command detection
(T1692.001, actually ES-only via honeypot.canonical_attck_techniques),
log-stream size (#1389 rotation guard), sandbox/ghidra/cape/
github-analysis spool + worker-status health (with ghidra/cape's
live-spool recheck against a stale status.json), ghidra/github-analysis
findings thresholds, and Filebeat ingestion stats via FILEBEAT_URL.

backend-worker gains read-only bind mounts for /logs and the
sandbox/windows-sandbox/ghosts-sandbox/ghidra/cape/github-analysis
request+results spools (mirroring the dashboard service's existing
mounts), plus the corresponding env vars. Every subsystem stays silent
when its directory env var is unset, matching the Go tier's "no noise
for an unopted-in subsystem" posture.

Left out: an in-memory IngestState (the Go tier's ingest=/age= half of
the "ingestion unhealthy" message) — this pass covers the Filebeat half
only; write request-dir mounts (submission) are Phase 3's concern, these
are read-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…heal (#1612 phase 3a)

Payload classification (payload_kind.rs, hand-rolled PE/ELF header checks
instead of a new parsing crate) and payload-dir path resolution
(payload_paths.rs, PAYLOAD_DIRS/BINARIES_DIR/SCRIPT_PAYLOAD_DIR) as shared
foundations, then three submission endpoints on top: POST
/api/v1/sandbox/submit (classifies + routes to linux/windows/ghosts),
/api/v1/ghidra/submit, /api/v1/github-analysis/submit (requires
confirm=publish, every outcome audited) plus GET
/api/v1/sandbox/golden-image-status. Adapted from Go's form-POST-redirect
flow to plain JSON for the BFF.

New backend-service-mounted compose service: same image/route table as
backend-service, but with read-write access to the sandbox/ghidra/
github-analysis request spools — a distinct trust/mount boundary kept
separate from backend-service's Phase-1 (state+socket) scope and from
backend-worker (loops only, not network-reachable). Internal URL
http://backend-service-mounted:8082; BFF wiring is a follow-up for the
frontend track.

Also wires payload_bytes.rs's on-demand mirror self-heal into
payload_detail.rs: a hash not yet in dashboard-payload-bytes-v1 is now
mirrored from disk on demand instead of coming back empty until
payload-inventory-worker's next scan. Needed read-only payload-dir mounts
added to backend-service itself (single-instance-safe, unlike the
mounted service's write access).

Deliberately simplified from the Go tier: no SHA-256-vs-Dionaea-MD5
fallback path resolution (payloadPathBySHA256), no byte-fingerprint
staleness recheck on an already-mirrored payload. Phase 3b (Payload
Workbench orchestrator) builds on payload_kind.rs/payload_paths.rs next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recipe-driven and one-off analysis runs across ghidra/linux-sandbox/
windows-sandbox/windows-ghosts/revdeck, ported from workbench_domain.go/
workbench_es.go/workbench_orchestrator.go onto backend-service-mounted
(phase 3a's spool-write service). A run's own document id is its
idempotency key, so creation is one atomic Elasticsearch op_type=create
(new es.rs primitives: index_doc_create/index_doc_cas/get_doc_meta) --
cross-instance dedup without a process-local mutex. Reconciliation reads
ghidra/sandbox/revdeck results straight off disk, never through their ES
mirrors, so a just-completed job's state change is never hidden behind an
import interval.

New: GET /api/v1/workbench/analyzers, POST/GET /api/v1/workbench/runs,
GET /api/v1/workbench/runs/{id}, POST .../children/{analyzer}/{action}
(cancel/retry), GET/POST /api/v1/workbench/recipes. Identity travels as
plain owner params (Phase 1's established BFF-supplied-identity
precedent), not a new header scheme.

"cape" stays selectable/validated but has no submission adapter -- mirrors
a real gap in the Go source today, not an omission here. "deterministic"
reports a clear not-yet-implemented failure rather than a fake result:
this tier has no YARA/entropy/IOC engine yet. No trueSHA256/
staticAnalysisFor (ghidra/revdeck submissions use the run's own SHA-256
directly -- same Dionaea MD5-vs-SHA256 gap phase 3a's payload_paths
already has) and no cross-process flock on the per-directory queue-depth
check (backend-service-mounted is already single-instance by phase 3a's
own tier decision).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hand-rolled, dependency-free PDF 1.4 writer ported from report_pdf.go +
report_pdf_brandmark.go + report_pdf_watermark.go: dark/light theme
palettes, branding defaults, the full report-element set (cover, metric
grid, assessment, findings/recommendations, top-N tables, operational
alerts, event appendix, parameters), watermark + header-mark image masks,
and the raw object/xref/trailer assembly — same fixed object numbering as
the Go source (watermark=6, its ExtGState=7, header-mark=8, pages from 9).
No new dependency: this is plain byte/string formatting on both sides of
the port, same as the Go original.

The two embedded 1-bit image masks (assets_pdf/*.maskdata, pre-FlateDecode
compressed) are copied byte-for-byte from the Go tier and embedded via
include_bytes!. Verified independently with poppler (pdfinfo/pdftotext,
not just internal self-consistency): a 23-page multi-section sample parses
cleanly, correct page count/size/version, text extracts correctly.

Pure module, no ES/HTTP dependency of its own — render_report_pdf(data,
theme, branding, elements, appendix_limit) is the entry point. Not yet
wired to a route: reportDataFor's ES-backed data gathering, the
definitions CRUD API, generate endpoint, and scheduler are phase 4b.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…er (#1612 phase 4b)

Definitions CRUD (dashboard-reports-definitions-v1, singleton doc like
config.rs/preferences.rs, not Go's generic esSettingsStore[T]), the 9-entry
template + 14-entry element catalog, and the on-demand generate endpoint,
built on phase 4a's pure PDF composer.

reportDataFor's real translation: Go filters an in-memory event cache this
tier doesn't have, so the summary/top-N/findings dataset is rebuilt as ES
aggregations (reports_data.rs) using the field names events.rs/
aggregates.rs/dashboard.rs already established, instead of in-memory
map-counting. Operational-alert matching and event-appendix rows are
ported field-for-field from reportAlertMatches/eventAppendix.

reports-scheduler joins backend-service's WORKER_LOOPS (ES-only, no host
mount, same tier as user-retention-sweep): every 30s, render due
definitions through the same pipeline as manual generate and advance
next_run_at (daily/weekly/monthly, ported from nextScheduleRun) — success
advances last_run_at too, failure only advances next_run_at so it never
hot-loops.

Scope decision: the sandbox/payload/ghidra templates render one referenced
artifact through dedicated renderers (sandbox_pdf.go/ghidra_pdf.go/a
payload equivalent) this pass does not port. They validate and save
correctly; generating one returns 501 with a clear message instead of
attempting to render. The 6 generic templates (executive/security/threat/
incident/sensors/custom) work end-to-end.

Also: es.rs gains delete_doc (generated-report retention pruning).
7 new unit tests (validation + schedule-math) alongside phase 4a's PDF
tests, all passing. Verified compose.yml resolves cleanly with the new
WORKER_LOOPS value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…phase 4b review)

reportDataFor's risk_level helper diverged from payload_analysis.go's
canonical riskLevel scale (this dashboard's one shared risk-word scale,
also used by sandbox/payload scoring) — was missing the "critical" tier
and used different score boundaries (>=70/>=40 instead of >=75/>=50/>=25).
Also fixed the "payloads" aggregation reading honeypot.sha256, a field
that doesn't exist in the indexed events — the correct field (confirmed
against fusion.rs's existing usage and the Go classify.go source) is
honeypot.shasum.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The phase 4a composer emitted raw PDF-1.4 bytes by hand (manual object
numbering, xref table, content-stream strings) to mirror the Go source's
own zero-dependency approach. Per explicit request, replaced it with the
vendored printpdf crate (0.12.6, default-features off to skip its
HTML/text-layout dependency chain — azul-layout/taffy/hyphenation — none
of which this module needs): object/xref/trailer assembly, content-stream
encoding, and PDF conformance now come from a maintained library instead
of hand-rolled byte formatting.

Public API (render_report_pdf, PdfTheme/PdfBranding, ReportData and
friends, ELEMENT_* constants) is unchanged, so reports_data.rs/
reports_api.rs needed zero edits. Both embedded emblems (watermark +
header-mark, 1-bit PDF image masks) are registered as XObject::External
with their original FlateDecode-compressed bytes reused verbatim —
printpdf's built-in image pipeline has no 1-bit ImageMask path, so this
was the documented escape hatch rather than re-encoding them as 8-bit
raster. Text uses printpdf's builtin Helvetica/Helvetica-Bold (one of the
14 standard PDF fonts, no embedding or font-license question needed).

Verified beyond compilation: full test suite green, plus a real 23-page
sample (forced pagination, both themes) checked with poppler
(pdfinfo/pdftotext/pdfimages) — correct A4 page size, correct page count,
clean text extraction, and both emblems confirmed as proper 1-bit stencil
masks at their original byte-identical sizes, each a single shared
XObject reused across every page.

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

github-actions Bot commented Aug 18, 2026

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.
  • ⚠️ 7 packages with OpenSSF Scorecard issues.

View full job summary

Xore and others added 2 commits August 18, 2026 20:40
Both failures predate this branch's own commits:
- port-tests/README.md and lib.sh leaked the real home-server LAN IP in
  an example SSH tunnel command, tripping the public-repository safety
  check; replaced with $HOMESERVER_HOST.
- dashboard/frontend/e2e/responsive-screens.spec.ts was committed by
  accident (its own header says "Throwaway capture harness... NOT
  committed") with a hardcoded path into a prior session's Claude Code
  job-scratch directory, which doesn't exist in CI. Deleted — it's a
  one-off manual screenshot dump, not a real regression test, and
  nothing else references it.

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

Last remaining scope item of #1612. Ports honeyfs_implant_client.go
(plain HTTP passthrough to the WireGuard-tunnel-only honeyfs-implant
service) and credentials_manager.go/credentials_api.go (the ES-backed
credentialRecord store keyed by a random cred_ id, CAS-retry writes via
es.rs's index_doc_create/index_doc_cas primitives, and the
list/create/rotate/link-token API surface). Also adds GET
/api/v1/canarytokens, a small pre-existing gap found while wiring
link-token's "does this token id exist" check — an earlier pass ported
create/download/types but never a history-list endpoint; the response
redacts auth_token the same way create()'s response already does.

Confirmed the issue's other "misc write paths" bullet (canarytoken
fired-event ack/management) doesn't exist in the Go source: fired
tokens are plain honeypot events through the existing pipeline,
acknowledged the same way any other alert is (already ported). No new
backend surface needed there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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