From 0c1fc68b139f261752fc05573cde60c3f394a13a Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 02:31:59 -0400 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20wire=20attribution=20worker=20?= =?UTF-8?q?=E2=80=94=20CLI=20+=20systemd=20unit=20+=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * decnet attribution — Typer command mirroring decnet reuse-correlate (--multi-actor-tick, --daemon flags). Calls run_attribution_loop with the dependency-injected repo. * deploy/decnet-attribution.service.j2 — systemd unit mirroring decnet-reuse-correlator.service.j2: ExecStart=decnet attribution, same hardening posture (NoNewPrivileges, ProtectSystem=full, ProtectHome=read-only, dedicated /var/log/decnet/decnet.attribution.log). * worker_registry.KNOWN_WORKERS += "attribution" — heartbeat already publishes as system.attribution.health from attribution_worker._WORKER_NAME, so the Workers panel surfaces the row the moment the unit is enabled. * api_start_all_workers preferred-order list + "attribution" between reuse-correlator and enrich so a fresh start-all brings it up alongside its peers. After this commit `systemctl enable --now decnet-attribution` (or the dashboard's start-all) actually launches the engine. --- decnet/cli/workers.py | 64 +++++++++++++++++++ .../router/workers/api_start_all_workers.py | 1 + decnet/web/worker_registry.py | 1 + deploy/decnet-attribution.service.j2 | 45 +++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 deploy/decnet-attribution.service.j2 diff --git a/decnet/cli/workers.py b/decnet/cli/workers.py index fe5c268f..4d4216d5 100644 --- a/decnet/cli/workers.py +++ b/decnet/cli/workers.py @@ -192,6 +192,70 @@ def register(app: typer.Typer) -> None: except KeyboardInterrupt: console.print("\n[yellow]Reuse correlator stopped.[/]") + @app.command(name="attribution") + def attribution( + multi_actor_tick_secs: float = typer.Option( + 60.0, "--multi-actor-tick", "-t", + help=( + "Cross-primitive multi_actor correlator tick interval (seconds). " + "Walks attribution_state for identities flagged on >= 2 " + "primitives and emits attribution.profile.multi_actor_suspected." + ), + ), + daemon: bool = typer.Option( + False, "--daemon", "-d", + help="Detach to background as a daemon process", + ), + ) -> None: + """Attribution engine v0 — per-(identity, primitive) state machine. + + Subscribes to ``attacker.observation.>`` and, for each event, + ensures a stub identity row, runs the merger over the full + per-(identity, primitive) observation series, upserts the + derived state, and publishes + ``attribution.profile.state_changed`` only on transition. + Periodic tick fires + ``attribution.profile.multi_actor_suspected`` when >= 2 + primitives flag the same identity. + + Closes DEBT-051. Bright-line scope: behavioural coherence and + drift only — never persona attribution to natural persons. + """ + import asyncio + from decnet.correlation.attribution_worker import ( + run_attribution_loop, + ) + from decnet.web.dependencies import repo + + if daemon: + log.info( + "attribution worker daemonizing tick=%s", + multi_actor_tick_secs, + ) + _utils._daemonize() + + log.info( + "attribution worker command invoked tick=%s", + multi_actor_tick_secs, + ) + console.print( + f"[bold cyan]Attribution engine starting[/] " + f"multi_actor_tick={multi_actor_tick_secs}s" + ) + console.print("[dim]Press Ctrl+C to stop[/]") + + async def _run() -> None: + await repo.initialize() + await run_attribution_loop( + repo, + multi_actor_tick_secs=multi_actor_tick_secs, + ) + + try: + asyncio.run(_run()) + except KeyboardInterrupt: + console.print("\n[yellow]Attribution engine stopped.[/]") + @app.command(name="clusterer") def clusterer( poll_interval_secs: float = typer.Option( diff --git a/decnet/web/router/workers/api_start_all_workers.py b/decnet/web/router/workers/api_start_all_workers.py index 6db52a1f..e167ebcd 100644 --- a/decnet/web/router/workers/api_start_all_workers.py +++ b/decnet/web/router/workers/api_start_all_workers.py @@ -27,6 +27,7 @@ _PREFERRED_ORDER: tuple[str, ...] = ( "mutator", "reconciler", "reuse-correlator", + "attribution", "enrich", "clusterer", "campaign-clusterer", diff --git a/decnet/web/worker_registry.py b/decnet/web/worker_registry.py index 212497c4..8e0a48b9 100644 --- a/decnet/web/worker_registry.py +++ b/decnet/web/worker_registry.py @@ -40,6 +40,7 @@ KNOWN_WORKERS: tuple[str, ...] = ( "mutator", "reconciler", # host-local fleet convergence — JSON ↔ DB ↔ docker "reuse-correlator", # credential-reuse pass — bus-woken on credential.captured + "attribution", # per-(identity, primitive) state machine — bus-woken on attacker.observation.> "enrich", # threat-intel enrichment — bus-woken on attacker.observed/scored "clusterer", # behavioral clustering — bus-woken on attacker.scored "campaign-clusterer", # campaign assembly — bus-woken on identity.formed diff --git a/deploy/decnet-attribution.service.j2 b/deploy/decnet-attribution.service.j2 new file mode 100644 index 00000000..22afbd97 --- /dev/null +++ b/deploy/decnet-attribution.service.j2 @@ -0,0 +1,45 @@ +[Unit] +Description=DECNET Attribution Engine v0 (per-(identity, primitive) state machine) +Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#attribution +After=network-online.target decnet-bus.service +Wants=network-online.target decnet-bus.service + +[Service] +Type=simple +User={{ user }} +Group={{ group }} +WorkingDirectory={{ install_dir }} +EnvironmentFile=-{{ install_dir }}/.env.local +Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.attribution.log +# Subscribes to attacker.observation.> and, for each event, ensures a +# stub AttackerIdentity row, runs the per-ValueKind merger over the +# full identity-keyed observation series, upserts the derived state in +# attribution_state, and publishes attribution.profile.state_changed +# only on transition. Periodic tick (default 60s) fires +# attribution.profile.multi_actor_suspected when >= 2 primitives flag +# the same identity. Closes DEBT-051. +ExecStart={{ venv_dir }}/bin/decnet attribution +StandardOutput=append:/var/log/decnet/decnet.attribution.log +StandardError=append:/var/log/decnet/decnet.attribution.log + +CapabilityBoundingSet= +AmbientCapabilities= + +# Security Hardening +NoNewPrivileges=yes +ProtectSystem=full +ProtectHome=read-only +PrivateTmp=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +LockPersonality=yes +ReadWritePaths={{ install_dir }} /var/log/decnet + +Restart=on-failure +RestartSec=5 +TimeoutStopSec=15 + +[Install] +WantedBy=multi-user.target