Repository navigation
Add the response contract: signed alert webhook and Home Assistant events - #82
Merged
Merged
Conversation
…ents The sensor detects and reports; it never blocks. Until now an operator who wanted a router to act on a decoy trip had only Slack text to parse. This adds one documented outbound event, squirrelops.alert/1, carrying what an automation needs: source IP and (by default) MAC and device id, the decoy and port, alert type, severity, title and summary, campaign intent and stage, and the sensor id and version. Two delivery methods carry it: - alert_methods.webhook: HTTP POST of compact sorted JSON to an operator endpoint, with X-SquirrelOps-Schema and X-SquirrelOps-Event headers and, when a signing secret is set, X-SquirrelOps-Signature as t=<unix>,v1=<HMAC-SHA256(secret, "<t>." + body)>, so a receiver can refuse unsigned or replayed events before it blocks anything. - alert_methods.home_assistant: the same event fired as a squirrelops_alert HA event through the existing LAN-pinned client (new fire_event). Supporting changes: - dispatch() now carries event_type (alert.new or alert.updated) to handlers so the contract can say which it is. - decoy alerts publish device_id, decoy_id, decoy_name, dest_port, and protocol at top level; they were buried in detail. - signing_secret joins webhook_url as a vaulted, redacted alert-method field. webhook_url was already covered by the wildcard rule. - ConfigurableAlertDispatcher learns the sensor id and version at startup. - App Settings: Automation Webhook (URL, signing secret, severity, device identifiers on by default) and Home Assistant Events (severity; requires the HA integration). - docs/ALERT_WEBHOOK.md documents the contract, request, signature recipe, an HA automation example, and the sensor-reports/you-act boundary. USER_GUIDE and SECURITY_MODEL updated. Red first: 17 contract tests, 3 HA fire_event tests, secret redaction and vault tests, and two decoy-handler identifier tests all failed before the implementation. Full sensor suite 2585 passed; swift test 424+121+39.
chrissyrocket
approved these changes
Oct 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item 7 from the 2026-10-17 defense review (recorded internally, not an issue).
Why
The sensor detects and reports; it never blocks a device or touches a router, and that stays true. But an operator who wants their UniFi, OPNsense, or Home Assistant to isolate a device that reached write activity on a decoy had nothing machine-readable to act on: Slack text was the only outbound channel. This adds one documented event with exactly the fields an automation needs, signed so the receiver can trust it.
The contract:
squirrelops.alert/1{"schema":"squirrelops.alert/1","event":"alert.updated","sent_at":"...","sensor":{"id":"...","version":"2.1.3"}, "alert":{"id":42,"type":"decoy.trip","severity":"critical","title":"...","summary":"...","created_at":"..."}, "source":{"ip":"192.168.1.77","mac":"AA:BB:CC:DD:EE:77","device_id":12}, "decoy":{"id":7,"name":"Studio Build Mac","port":445,"protocol":"tcp"}, "campaign":{"intent":"ransomware","stage":4,"stage_label":"write activity"}}decoyandcampaignarenullwhen not applicable.macanddevice_idfollow Include Device Identifiers, which defaults to on for these two methods because the MAC is what a controller blocks on. Free text is bounded (title 256, summary 512, decoy name 128). Nothing internal rides along (no event sequence, read markers, or detail blobs); a test asserts the exact key set. Full spec, request format, signature recipe, HA automation example, and the "sensor reports, you act" boundary:docs/ALERT_WEBHOOK.md.Delivery methods
alert_methods.webhook: POST of compact sorted JSON. HeadersX-SquirrelOps-Schema,X-SquirrelOps-Event,User-Agent: squirrelops-home-sensor/<version>, and with a signing secretX-SquirrelOps-Signature: t=<unix>,v1=<HMAC-SHA256(secret, "<t>." + body)>(timestamp inside the signed material, so replays are detectable). Non-2xx raises the existing credential-safeAlertDeliveryErrorand the dispatcher's backoff applies. Onlyhttp(s)URLs are accepted.alert_methods.home_assistant: firessquirrelops_alertwith the same payload throughHomeAssistantClient.fire_event, which keeps the LAN-only resolution and host pinning of the existing connectivity check. Requires the HA integration enabled with URL and token.Supporting changes
AlertDispatcher.dispatch(payload, event_type=...): handlers receive a copy carryingalert.neworalert.updated. Existing handlers ignore it.device_id,decoy_id,decoy_name,dest_port,protocolat top level on bothalert.newandalert.updated(SwiftAlertSummaryignores unknown keys).signing_secretadded toCONFIG_SECRET_PATHS,ALERT_METHOD_SECRET_PATHS, and the vault's_ALERT_METHOD_SECRET_FIELDS.webhook_urlwas already redacted by the wildcard rule and vaulted.ConfigurableAlertDispatcher(config, sensor_id=, sensor_version=)wired in__main__.Tests (red first)
tests/unit/test_response_event.py(17): exact event shape, opt-in identifiers, null decoy/campaign, bounds, signature algorithm plus the docs' receiver recipe, webhook handler headers/body/signature, unsigned mode, credential-safe HTTP and transport errors,build_methods_from_configfor both methods (disabled, missing URL, non-http, HA not configured), and new-vs-updated event typing through the dispatcher.TestFireEvent(3) in the HA client suite via pytest-httpx.signing_secretredacted, restored, and stripped from persisted YAML.TestOutboundIdentifiers(2):alert.newandalert.updatedcarry device and decoy identifiers.Full sensor suite in a fresh worktree venv: 2585 passed, 6 skipped (opt-in live modules). ruff clean.
swift test: 424 + 121 + 39 passed.Deception integrity
Control surface only. Nothing under
decoys/changes and no attacker-visible behavior changes.Data leaving the network
Two new rows in the USER_GUIDE table: the event fields above go to the endpoint you configured, or to your Home Assistant over the LAN. Off by default.