Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
24 changes: 19 additions & 5 deletions worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down Expand Up @@ -339,7 +344,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.
Expand All @@ -365,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,
Expand Down Expand Up @@ -595,13 +607,15 @@ 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) {
const raw = await env.SNARE_KV.get(`webhook:${token}`);
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)
Expand All @@ -619,7 +633,7 @@ async function resolveWebhooks(token, env) {
? [perTokenWebhook]
: (env.WEBHOOK_URLS || "").split(",").filter(Boolean);

return { webhooks, meta };
return { webhooks, meta, registered };
}

// ─── Alert formatting ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -726,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,
}],
};
Expand Down Expand Up @@ -761,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),
}],
};
Expand Down
Loading