From 9df6af317983e6a58458597d1446f3f2c6728f69 Mon Sep 17 00:00:00 2001 From: "clap [bot]" Date: Thu, 19 Mar 2026 21:29:18 +0000 Subject: [PATCH 1/3] fix: replace example AWS account ID with canonical 123456789012 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4381849..feafb78 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The `awsproc` canary uses AWS `credential_process` — a shell command that runs ```ini # ~/.aws/config [profile prod-admin] -role_arn = arn:aws:iam::389844960505:role/OrganizationAccountAccessRole +role_arn = arn:aws:iam::123456789012:role/OrganizationAccountAccessRole source_profile = prod-admin-source [profile prod-admin-source] @@ -184,7 +184,7 @@ The two-profile pattern looks like a real assume-role setup: ```ini # ~/.aws/config [profile prod-admin] -role_arn = arn:aws:iam::389844960505:role/OrganizationAccountAccessRole +role_arn = arn:aws:iam::123456789012:role/OrganizationAccountAccessRole source_profile = prod-admin-source [profile prod-admin-source] From 225ac44926d8aa3f6ebf1751188daeaa44c90019 Mon Sep 17 00:00:00 2001 From: "clap [bot]" Date: Tue, 24 Mar 2026 20:04:34 +0000 Subject: [PATCH 2/3] fix(worker): do not fire webhook for unregistered tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the fix applied to the Go server in #24. When a token hits /c/{token} but has no KV registration, the global WEBHOOK_URLS fallback was still firing — causing false alerts from probe traffic hitting partial URLs like /c/agent-01-. resolveWebhooks() now returns a 'registered' flag; processAlert() returns early for unregistered tokens (except snare-test-* tokens which are exempt by design). --- worker/index.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worker/index.js b/worker/index.js index a8b9321..c5d9857 100644 --- a/worker/index.js +++ b/worker/index.js @@ -339,7 +339,14 @@ async function processAlert(token, metadata, env) { if (await isDuplicate(env, token, metadata.ip)) return; // Resolve webhook + canary metadata (single KV fetch for both filtering and delivery) - const { webhooks, meta } = await resolveWebhooks(token, env); + const { webhooks, meta, registered } = await resolveWebhooks(token, env); + + // Unregistered tokens must never fire webhooks — prevents false alerts from + // probe traffic hitting random/partial token URLs (e.g. snare.sh/c/agent-01-). + if (!registered && !token.startsWith("snare-test-")) { + console.log("UNREGISTERED_TOKEN", token, metadata.ip); + return; + } // Per-type false-positive filtering: drop scanner orgs and requests // that lack expected SDK signatures for high-confidence canary types. @@ -595,6 +602,7 @@ async function handleRotateSecret(request, env) { async function resolveWebhooks(token, env) { let meta = {}; let perTokenWebhook = null; + let registered = false; // Always try to load registration metadata (type, label, device) if (env.SNARE_KV) { @@ -602,6 +610,7 @@ async function resolveWebhooks(token, env) { if (raw) { try { const reg = JSON.parse(raw); + registered = true; meta = { canaryType: reg.canary_type, label: reg.label, deviceId: reg.device_id }; // Use per-token webhook if it's a valid https URL // (fixed: proper parentheses for operator precedence) @@ -619,7 +628,7 @@ async function resolveWebhooks(token, env) { ? [perTokenWebhook] : (env.WEBHOOK_URLS || "").split(",").filter(Boolean); - return { webhooks, meta }; + return { webhooks, meta, registered }; } // ─── Alert formatting ──────────────────────────────────────────────────────── From ecd44dedd72838656cf4b4bb36350144eb20badb Mon Sep 17 00:00:00 2001 From: "clap [bot]" Date: Tue, 24 Mar 2026 21:02:08 +0000 Subject: [PATCH 3/3] fix(worker): polish items for v0.1.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update alert footer to 'IP, UA, timestamp only — no request body' - Log test callbacks as CANARY_TEST vs CANARY_FIRED - Add missing canary types: huggingface, azure, git, terraform - Add v0.1.4 CHANGELOG entry --- CHANGELOG.md | 10 ++++++++++ worker/index.js | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94965e2..33c0660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ Versioning: [Semantic Versioning](https://semver.org/) ## [Unreleased] +## [0.1.4] - 2026-03-24 + +### Fixed +- Worker: unregistered tokens no longer fire the global fallback webhook — probe traffic hitting partial/random token URLs (e.g. `/c/agent-01-`) is silently dropped +- Worker: alert footer text updated to "IP, UA, timestamp only — no request body" (matches website copy) +- Worker: test token callbacks now logged as `CANARY_TEST` instead of `CANARY_FIRED` for cleaner production logs +- Worker: added missing canary types to `CANARY_TYPES` map (huggingface, azure, git, terraform) +- README: replaced example AWS account ID `389844960505` with canonical `123456789012` + + ## [0.1.3] - 2026-03-18 ### Fixed diff --git a/worker/index.js b/worker/index.js index c5d9857..eef3f56 100644 --- a/worker/index.js +++ b/worker/index.js @@ -54,6 +54,11 @@ const CANARY_TYPES = { awsproc: { emoji: "⚙️", color: 0xFF9900, name: "AWS (credential_process)" }, docker: { emoji: "🐳", color: 0x2496ED, name: "Docker" }, generic: { emoji: "🗝️", color: 0x888888, name: "Generic" }, + huggingface: { emoji: "🤗", color: 0xFFD21E, name: "Hugging Face" }, + azure: { emoji: "☁️", color: 0x0078D4, name: "Azure" }, + git: { emoji: "🌿", color: 0xF05033, name: "Git" }, + terraform: { emoji: "🏗️", color: 0x7B42BC, name: "Terraform" }, + stripe: { emoji: "💳", color: 0x6772E5, name: "Stripe" }, }; const DEFAULT_TYPE = { emoji: "🪤", color: 0xB2121A, name: "Canary" }; @@ -372,7 +377,7 @@ async function processAlert(token, metadata, env) { }; // Log metadata only — never body content - console.log("CANARY_FIRED", JSON.stringify({ + console.log(isTest ? "CANARY_TEST" : "CANARY_FIRED", JSON.stringify({ token: event.token, is_test: event.is_test, ip: event.ip, @@ -735,7 +740,7 @@ function buildDiscordPayload(event, meta, type, fromCloud) { title, color: isTest ? 0x888888 : type.color, fields, - footer: { text: "snare.sh · request body was never captured" }, + footer: { text: "snare.sh · IP, UA, timestamp only — no request body" }, timestamp: event.timestamp, }], }; @@ -770,7 +775,7 @@ function buildSlackPayload(event, meta, type, fromCloud) { attachments: [{ color: isTest ? "#888888" : `#${type.color.toString(16).padStart(6, "0")}`, fields, - footer: "snare.sh · request body was never captured", + footer: "snare.sh · IP, UA, timestamp only — no request body", ts: Math.floor(new Date(event.timestamp).getTime() / 1000), }], };