npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
When to Use
- When deploying deception-based tripwires across network infrastructure to detect intrusions
- When building early warning systems that alert on unauthorized access to sensitive resources
- When planting fake AWS credentials, DNS beacons, or HTTP tokens to catch attackers during lateral movement
- When integrating canary token alerts with SOC workflows via Slack, Microsoft Teams, or SIEM webhooks
- When complementing traditional IDS/IPS with zero-false-positive deception technology
Prerequisites
- Python 3.8+ with
requestslibrary installed - Network access to canarytokens.org API (or self-hosted Canarytokens instance)
- Webhook endpoint for alert delivery (Slack, Teams, email, or generic HTTP)
- For Thinkst Canary enterprise: valid console domain and API auth token
- Administrative access to target systems where tokens will be planted
- Appropriate authorization for all deployment activities
Core Concepts
What Are Canary Tokens?
Canary tokens are digital tripwires -- resources that should never be accessed during normal operations. When an attacker interacts with a canary token, it immediately triggers an alert with near-zero false positives. Unlike signature-based detection, canary tokens detect attackers by their behavior (accessing bait resources) rather than matching known patterns.
Token Types for Network Intrusion Detection
| Token Type | Trigger Mechanism | Best Placement | Detection Scenario |
|---|---|---|---|
| DNS Token | DNS resolution of FQDN | Config files, scripts, internal docs | Attacker reads configs during recon |
| HTTP Token | HTTP GET to unique URL | Internal wikis, bookmark files, HTML | Attacker browses internal resources |
| AWS API Key | AWS API call with fake creds | .aws/credentials, env files, repos |
Attacker tests found credentials |
| Cloned Site | Visit to cloned page | Internal portals, admin panels | Attacker accesses cloned services |
| SVN Token | SVN checkout | Repository configs | Attacker clones repositories |
| SQL Server | Database login attempt | Connection strings, config files | Attacker attempts DB access |
Alert Flow Architecture
[Attacker Action] --> [Token Triggered] --> [Canarytokens Server]
|
[Webhook POST]
|
+-------------------------+-------------------------+
| | |
[Slack Alert] [Email Alert] [SIEM Ingestion]
| | |
[SOC Analyst] [On-Call Page] [Correlation Rule]Instructions
Step 1: Generate DNS Canary Tokens
DNS tokens are the most versatile -- they trigger on any DNS resolution, even from air-gapped networks with only DNS egress. The token is an FQDN that, when resolved, alerts the token owner.
import requests
# Create DNS canary token via Canarytokens.org
response = requests.post("https://canarytokens.org/generate", data={
"type": "dns",
"email": "soc@company.com",
"memo": "Production database server - /etc/app/db.conf",
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx"
}, timeout=15)
token_data = response.json()
dns_hostname = token_data["hostname"]
# Example: abc123def456.canarytokens.comPlant DNS tokens in locations attackers commonly inspect:
/etc/hostsentries pointing to the canary FQDN- Application configuration files (
database_host,backup_server) - SSH config files (
~/.ssh/config) with canary hostnames - Internal DNS zone files as decoy A records
- CI/CD pipeline environment variables
Step 2: Deploy HTTP Canary Tokens
HTTP tokens generate a unique URL that triggers on any HTTP request. They reveal the source IP, User-Agent, and other HTTP headers of the requester.
# Create HTTP token
response = requests.post("https://canarytokens.org/generate", data={
"type": "http",
"email": "soc@company.com",
"memo": "Internal wiki - IT admin passwords page",
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx"
}, timeout=15)
http_url = response.json()["url"]
# Embed in internal HTML pages, documents, or bookmark filesPlacement strategies for HTTP tokens:
- Hidden
<img>tags in internal wiki pages with sensitive titles - URL shortener redirects in shared bookmark collections
- Links in internal documentation labeled "admin credentials" or "VPN configs"
.urlor.weblocshortcut files in network shares- Browser bookmark exports in user profile backups
Step 3: Create AWS API Key Tokens
AWS key tokens are among the highest-fidelity canary tokens. They generate real-looking AWS access keys that trigger an alert whenever anyone attempts to use them against any AWS API endpoint.
# Create AWS API key canary token
response = requests.post("https://canarytokens.org/generate", data={
"type": "aws_keys",
"email": "soc@company.com",
"memo": "DevOps jump box - /home/deploy/.aws/credentials",
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx"
}, timeout=15)
aws_token = response.json()
access_key_id = aws_token["access_key_id"]
secret_access_key = aws_token["secret_access_key"]Deploy the fake credentials:
# Place in ~/.aws/credentials on honeypot or jump servers
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region = us-east-1
# Also plant in:
# - .env files in code repositories
# - Docker environment configurations
# - Terraform state files (decoy)
# - Jenkins/CI credential storesStep 4: Configure Webhook Alert Integration
Set up real-time alerting to your SOC through multiple channels:
# Slack webhook integration
def send_slack_alert(webhook_url, alert_data):
"""Forward canary token alert to Slack channel."""
payload = {
"text": f":rotating_light: *Canary Token Triggered*",
"attachments": [{
"color": "#FF0000",
"fields": [
{"title": "Token Memo", "value": alert_data.get("memo", "Unknown"), "short": True},
{"title": "Source IP", "value": alert_data.get("src_ip", "Unknown"), "short": True},
{"title": "Token Type", "value": alert_data.get("channel", "Unknown"), "short": True},
{"title": "Triggered At", "value": alert_data.get("time", "Unknown"), "short": True},
],
"footer": "Canarytokens Alert System",
}]
}
requests.post(webhook_url, json=payload, timeout=10)# Generic webhook receiver (Flask) for SIEM ingestion
from flask import Flask, request, jsonify
import json, logging
app = Flask(__name__)
logging.basicConfig(filename="/var/log/canary_alerts.json", level=logging.INFO)
@app.route("/canary-webhook", methods=["POST"])
def receive_alert():
alert = request.json or request.form.to_dict()
logging.info(json.dumps({
"event_type": "canarytoken_triggered",
"memo": alert.get("memo"),
"src_ip": alert.get("src_ip"),
"token_type": alert.get("channel"),
"time": alert.get("time"),
"manage_url": alert.get("manage_url"),
"additional_data": alert.get("additional_data", {}),
}))
return jsonify({"status": "received"}), 200Step 5: Enterprise Deployment with Thinkst Canary API
For organizations using Thinkst Canary, leverage the API for mass deployment and centralized management:
import canarytools
# Connect to Thinkst Canary console
console = canarytools.Console(
domain="yourcompany",
api_key="your_api_auth_token"
)
# Create tokens programmatically at scale
token_types = {
"dns": "DNS beacon in config files",
"aws-id": "AWS credentials on jump servers",
"http": "Web bug in internal documentation",
"doc-msword": "Word document in finance share",
"slack-api": "Fake Slack bot token in source code",
}
for kind, memo in token_types.items():
result = console.tokens.create(memo=memo, kind=kind)
print(f"[+] Created {kind} token: {result}")
# Monitor for triggered alerts
alerts = console.tokens.alerts()
for alert in alerts:
print(f"[ALERT] {alert.memo} triggered from {alert.src_ip}")Step 6: Token Placement Strategy by Network Zone
DMZ / Public-Facing:
- HTTP tokens in admin panel login pages (hidden image tag)
- DNS tokens in web server configuration files
- AWS keys in
.envfiles on staging servers
Internal Network / Corporate:
- DNS tokens in Active Directory Group Policy scripts
- AWS keys in developer workstation backup directories
- HTTP tokens in internal SharePoint/Confluence pages titled "Emergency Credentials"
- Word document tokens in network shares (
\\fileserver\IT\passwords.docx)
Production / Data Center:
- DNS tokens in database configuration files
- AWS keys in CI/CD environment variables
- SQL Server tokens in connection strings on application servers
- SVN/Git tokens in repository configuration files
Cloud Infrastructure:
- AWS key tokens in S3 bucket policies (decoy)
- DNS tokens in CloudFormation/Terraform templates
- HTTP tokens in Lambda function environment variables
- Cloned-site tokens mimicking cloud admin consoles
Examples
Full Deployment Script
# Deploy a comprehensive canary token network
python scripts/agent.py --action full_deploy \
--email soc@company.com \
--webhook https://hooks.slack.com/services/T.../B.../xxx \
--output deployment_report.jsonMonitor Triggered Tokens
# Check for triggered alerts
python scripts/agent.py --action monitor \
--console-domain yourcompany \
--api-key YOUR_AUTH_TOKENGenerate Token Inventory
# Create inventory of all deployed tokens
python scripts/agent.py --action inventory \
--output token_inventory.jsonValidation Checklist
- DNS tokens resolve correctly and generate alerts within 60 seconds
- HTTP tokens return a valid response and log source IP
- AWS key tokens trigger alerts when used with
aws sts get-caller-identity - Webhook alerts arrive in Slack/Teams/SIEM within acceptable latency
- Token memo fields contain sufficient context for SOC triage
- Deployment locations are documented in token inventory
- Alert escalation procedures are defined and tested
- Tokens do not interfere with legitimate operations
- Self-hosted Canarytokens instance (if used) is hardened and monitored
- Token rotation schedule is established (quarterly recommended)
References
- Canarytokens Documentation: https://docs.canarytokens.org/guide/
- Thinkst Canary Platform: https://canary.tools/
- Thinkst Canary API: https://docs.canary.tools/canarytokens/actions.html
- Canarytokens Open Source: https://github.com/thinkst/canarytokens
- Zeltser Honeytoken Setup Guide: https://zeltser.com/honeytokens-canarytokens-setup/
- Grafana Canary Token Case Study: https://grafana.com/blog/2025/08/25/canary-tokens-learn-all-about-the-unsung-heroes-of-security-at-grafana-labs/
- AWS Infrastructure Canarytoken: https://blog.thinkst.com/2025/09/introducing-the-aws-infrastructure-canarytoken.html
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md5.9 KB
API Reference: Canary Tokens for Network Intrusion Detection
Canarytokens.org Public API
Create Token
POST https://canarytokens.org/generate
Content-Type: application/x-www-form-urlencodedParameters:
| Parameter | Required | Description |
|---|---|---|
type |
Yes | Token type: dns, http, aws_keys, web_image, cloned_web, svn, sql_server, qr_code, slack_api, doc_msword, doc_msexcel, pdf_acrobat_reader |
email |
Yes | Notification email address |
memo |
Yes | Human-readable label for SOC triage |
webhook_url |
No | Webhook URL for real-time POST alerts |
Example - DNS Token:
import requests
resp = requests.post("https://canarytokens.org/generate", data={
"type": "dns",
"email": "soc@company.com",
"memo": "Production DB server /etc/app/db.conf",
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
})
token = resp.json()
# {"hostname": "abc123.canarytokens.com", "url": "https://canarytokens.org/manage?..."}Example - AWS Key Token:
resp = requests.post("https://canarytokens.org/generate", data={
"type": "aws_keys",
"email": "soc@company.com",
"memo": "DevOps jump box /home/deploy/.aws/credentials",
})
token = resp.json()
# {"access_key_id": "AKIA...", "secret_access_key": "...", "url": "..."}Example - HTTP Token:
resp = requests.post("https://canarytokens.org/generate", data={
"type": "http",
"email": "soc@company.com",
"memo": "Internal wiki emergency passwords page",
})
token = resp.json()
# {"url": "http://canarytokens.com/..."}Thinkst Canary Enterprise API
Authentication
All enterprise API calls require auth_token parameter.
Base URL: https://{console_domain}.canary.tools/api/v1/Create Token
POST /api/v1/canarytoken/createParameters:
| Parameter | Required | Description |
|---|---|---|
auth_token |
Yes | API authentication token |
memo |
Yes | Description for the token |
kind |
Yes | Token kind (see below) |
flock_id |
No | Flock ID for grouping |
Supported Kinds: dns, http, aws-id, doc-msword, doc-msexcel, slack-api, svn, cloned-css, cloned-web, qr-code, sql-server
import requests
url = "https://yourcompany.canary.tools/api/v1/canarytoken/create"
resp = requests.post(url, data={
"auth_token": "YOUR_AUTH_TOKEN",
"memo": "Production honeytoken",
"kind": "dns",
})List Tokens
GET /api/v1/canarytokens/fetch?auth_token=YOUR_AUTH_TOKENGet Triggered Alerts
GET /api/v1/canarytokens/alerts?auth_token=YOUR_AUTH_TOKENUsing Python Client Library
import canarytools
console = canarytools.Console(domain="yourcompany", api_key="YOUR_API_KEY")
# Create tokens
dns_token = console.tokens.create(memo="DNS beacon", kind=canarytools.CanaryTokenKinds.DNS)
aws_token = console.tokens.create(memo="AWS keys", kind=canarytools.CanaryTokenKinds.AWS_ID)
# List all tokens
tokens = console.tokens.all()
# Get alerts
alerts = console.tokens.alerts()Webhook Alert Payload Format
When a canary token is triggered, the webhook receives a POST with this payload:
{
"manage_url": "https://canarytokens.org/manage?token=abc123&auth=xyz",
"memo": "Production DB server /etc/app/db.conf",
"additional_data": {
"src_ip": "203.0.113.50",
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"referer": "",
"location": ""
},
"channel": "DNS",
"time": "2026-01-15 14:23:00 (UTC)",
"src_ip": "203.0.113.50"
}Fields:
| Field | Description |
|---|---|
manage_url |
URL to manage/disable the token |
memo |
The description set during creation |
channel |
Token type that triggered (DNS, HTTP, AWS) |
src_ip |
Source IP of the triggering request |
time |
UTC timestamp of the trigger event |
additional_data |
Extra context (User-Agent, referer, etc.) |
Token Placement Matrix
| Token Type | Recommended Location | Trigger Action |
|---|---|---|
| DNS | Config files, /etc/hosts, SSH config |
DNS resolution |
| HTTP | Internal wikis, HTML pages, bookmarks | HTTP GET request |
| AWS Keys | ~/.aws/credentials, .env files, repos |
AWS API call |
| Web Image | HTML pages, email signatures | Image HTTP load |
| Cloned Web | Internal admin portals | Page visit |
| SVN | Repository configs | SVN checkout |
| SQL Server | Connection strings, config files | DB login attempt |
| Slack API | Source code, CI/CD configs | Slack API call |
| QR Code | Physical locations, printed docs | QR scan + URL visit |
MITRE ATT&CK Mapping
| Technique | ID | Canary Token Detection |
|---|---|---|
| Account Discovery | T1087 | AWS key tokens detect credential testing |
| File and Directory Discovery | T1083 | Document/config tokens detect file access |
| Network Service Discovery | T1046 | DNS tokens detect network scanning |
| Valid Accounts: Cloud | T1078.004 | AWS key tokens detect credential abuse |
| Unsecured Credentials: Files | T1552.001 | Credential file tokens detect harvesting |
| Data from Network Shared Drive | T1039 | Document tokens detect share browsing |
References
- Canarytokens Documentation: https://docs.canarytokens.org/guide/
- Canarytokens DNS Tokens: https://docs.canarytokens.org/guide/dns-token.html
- Canarytokens HTTP Tokens: https://docs.canarytokens.org/guide/http-token.html
- Canarytokens AWS Key Tokens: https://docs.canarytokens.org/guide/aws-keys-token.html
- Thinkst Canary API Docs: https://docs.canary.tools/canarytokens/actions.html
- Thinkst Python Client: https://github.com/thinkst/canarytools-python
- Canarytokens Open Source: https://github.com/thinkst/canarytokens
- Zeltser Honeytoken Guide: https://zeltser.com/honeytokens-canarytokens-setup/
Scripts 1
agent.py33.4 KB
#!/usr/bin/env python3
"""
Agent for deploying and managing canary tokens for network intrusion detection.
Supports DNS, HTTP, and AWS API key canary tokens via Canarytokens.org API
and Thinkst Canary enterprise console. Provides webhook alert integration
with Slack, Microsoft Teams, email, and generic HTTP endpoints.
"""
import os
import sys
import json
import uuid
import hashlib
import argparse
import logging
import smtplib
import socket
import re
from datetime import datetime, timezone
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pathlib import Path
from urllib.parse import urlparse
import requests
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
logger = logging.getLogger("canary-token-agent")
_SAFE_NAME_RE = re.compile(r"^[a-zA-Z0-9_.\-]+$")
# ---------------------------------------------------------------------------
# Canarytokens.org API integration
# ---------------------------------------------------------------------------
CANARYTOKENS_API_URL = os.getenv(
"CANARYTOKENS_API_URL", "https://canarytokens.org/generate"
)
SUPPORTED_TOKEN_TYPES = {
"dns": "DNS resolution beacon -- triggers on any DNS lookup of the FQDN",
"http": "HTTP URL token -- triggers on HTTP GET, reveals source IP and User-Agent",
"aws_keys": "AWS API key pair -- triggers when keys are used against any AWS endpoint",
"web_image": "Web bug / image beacon -- triggers when image is loaded in browser",
"cloned_web": "Cloned website token -- triggers when cloned page is visited",
"svn": "SVN repository token -- triggers on SVN checkout",
"sql_server": "SQL Server token -- triggers on database login attempt",
"qr_code": "QR code token -- triggers when QR code is scanned and URL visited",
"slack_api": "Slack API token -- triggers when token is used against Slack API",
}
def generate_token_id():
"""Generate a unique canary token tracking identifier."""
return f"CT-{uuid.uuid4().hex[:12].upper()}"
def create_canarytoken(token_type, email, memo, webhook_url=None):
"""
Create a canary token via Canarytokens.org public API.
Args:
token_type: One of the SUPPORTED_TOKEN_TYPES keys
email: Notification email address
memo: Human-readable description for alert context
webhook_url: Optional webhook URL for real-time alerts
Returns:
dict with token details from the API
"""
if token_type not in SUPPORTED_TOKEN_TYPES:
raise ValueError(
f"Unsupported token type: {token_type}. "
f"Supported: {list(SUPPORTED_TOKEN_TYPES.keys())}"
)
data = {
"type": token_type,
"email": email,
"memo": memo,
}
if webhook_url:
data["webhook_url"] = webhook_url
logger.info("Creating %s canary token: %s", token_type, memo)
resp = requests.post(CANARYTOKENS_API_URL, data=data, timeout=30)
resp.raise_for_status()
result = resp.json()
logger.info("Token created successfully: %s", token_type)
return result
def create_dns_token(email, memo, webhook_url=None):
"""Create a DNS canary token that alerts on any DNS resolution."""
result = create_canarytoken("dns", email, memo, webhook_url)
return {
"type": "dns",
"hostname": result.get("hostname", ""),
"token_id": generate_token_id(),
"memo": memo,
"manage_url": result.get("url", ""),
"created_at": datetime.now(timezone.utc).isoformat(),
}
def create_http_token(email, memo, webhook_url=None):
"""Create an HTTP canary token that alerts on HTTP requests."""
result = create_canarytoken("http", email, memo, webhook_url)
return {
"type": "http",
"url": result.get("url", ""),
"token_id": generate_token_id(),
"memo": memo,
"manage_url": result.get("url", ""),
"created_at": datetime.now(timezone.utc).isoformat(),
}
def create_aws_key_token(email, memo, webhook_url=None):
"""Create an AWS API key canary token that alerts on any AWS API usage."""
result = create_canarytoken("aws_keys", email, memo, webhook_url)
return {
"type": "aws_keys",
"access_key_id": result.get("access_key_id", ""),
"secret_access_key": result.get("secret_access_key", ""),
"token_id": generate_token_id(),
"memo": memo,
"manage_url": result.get("url", ""),
"created_at": datetime.now(timezone.utc).isoformat(),
}
def create_web_image_token(email, memo, webhook_url=None):
"""Create a web image beacon canary token."""
result = create_canarytoken("web_image", email, memo, webhook_url)
return {
"type": "web_image",
"image_url": result.get("url", ""),
"token_id": generate_token_id(),
"memo": memo,
"manage_url": result.get("url", ""),
"created_at": datetime.now(timezone.utc).isoformat(),
}
# ---------------------------------------------------------------------------
# Thinkst Canary Enterprise API integration
# ---------------------------------------------------------------------------
def thinkst_create_token(console_domain, auth_token, kind, memo, flock_id=None):
"""
Create a canary token via Thinkst Canary enterprise console API.
Args:
console_domain: Your Thinkst Canary console domain (e.g., 'yourcompany')
auth_token: API authentication token
kind: Token kind (dns, http, aws-id, doc-msword, etc.)
memo: Description for the token
flock_id: Optional flock identifier for grouping
Returns:
dict with token details from the Thinkst API
"""
url = f"https://{console_domain}.canary.tools/api/v1/canarytoken/create"
payload = {
"auth_token": auth_token,
"memo": memo,
"kind": kind,
}
if flock_id:
payload["flock_id"] = flock_id
logger.info("Creating Thinkst %s token: %s", kind, memo)
resp = requests.post(url, data=payload, timeout=30)
resp.raise_for_status()
return resp.json()
def thinkst_list_tokens(console_domain, auth_token):
"""List all canary tokens from the Thinkst Canary console."""
url = f"https://{console_domain}.canary.tools/api/v1/canarytokens/fetch"
resp = requests.get(url, params={"auth_token": auth_token}, timeout=30)
resp.raise_for_status()
return resp.json().get("tokens", [])
def thinkst_get_alerts(console_domain, auth_token):
"""Retrieve triggered canary token alerts from Thinkst Canary console."""
url = f"https://{console_domain}.canary.tools/api/v1/canarytokens/alerts"
resp = requests.get(url, params={"auth_token": auth_token}, timeout=30)
resp.raise_for_status()
return resp.json().get("alerts", [])
# ---------------------------------------------------------------------------
# Token deployment helpers
# ---------------------------------------------------------------------------
def deploy_aws_credentials_file(target_path, access_key_id, secret_access_key,
profile="default", region="us-east-1"):
"""
Deploy a fake AWS credentials file as a canary token.
Places realistic-looking AWS credentials in the target path. When an attacker
finds and uses these credentials, the canary token triggers an alert.
"""
content = (
f"[{profile}]\n"
f"aws_access_key_id = {access_key_id}\n"
f"aws_secret_access_key = {secret_access_key}\n"
f"region = {region}\n"
)
target = Path(target_path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
logger.info("Deployed AWS credential canary at: %s", target_path)
return {
"type": "aws_credentials_file",
"path": str(target),
"profile": profile,
"deployed_at": datetime.now(timezone.utc).isoformat(),
}
def deploy_dns_token_in_config(config_path, dns_hostname, key_name="backup_server",
comment="Backup replication endpoint"):
"""
Embed a DNS canary token hostname in a configuration file.
The token triggers when anyone or any tool resolves the hostname,
such as during network scanning, config parsing, or manual inspection.
"""
entry = f"\n# {comment}\n{key_name} = {dns_hostname}\n"
config = Path(config_path)
if not config.exists():
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(entry, encoding="utf-8")
else:
with open(config, "a", encoding="utf-8") as f:
f.write(entry)
logger.info("Deployed DNS canary in config: %s (key=%s)", config_path, key_name)
return {
"type": "dns_config_embed",
"config_path": str(config),
"key_name": key_name,
"dns_hostname": dns_hostname,
"deployed_at": datetime.now(timezone.utc).isoformat(),
}
def deploy_http_token_in_html(html_path, http_token_url, page_title="IT Admin Portal"):
"""
Embed an HTTP canary token as a hidden image tag in an HTML page.
The token triggers when the page is rendered in a browser and the
hidden image is loaded, revealing the attacker's IP and User-Agent.
"""
html_content = f"""<!DOCTYPE html>
<html>
<head><title>{page_title}</title></head>
<body>
<h1>{page_title}</h1>
<p>Access restricted. Contact IT for credentials.</p>
<!-- Canary token beacon -->
<img src="{http_token_url}" style="display:none" alt="" width="1" height="1" />
</body>
</html>"""
target = Path(html_path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(html_content, encoding="utf-8")
logger.info("Deployed HTTP canary in HTML: %s", html_path)
return {
"type": "http_html_beacon",
"html_path": str(target),
"token_url": http_token_url,
"deployed_at": datetime.now(timezone.utc).isoformat(),
}
def deploy_ssh_config_token(ssh_config_path, dns_hostname,
host_alias="backup-gateway"):
"""
Plant a DNS canary token in an SSH config file.
Attackers performing recon on SSH configurations will trigger the token
when they attempt to resolve or connect to the canary hostname.
"""
entry = (
f"\n# Legacy backup gateway\n"
f"Host {host_alias}\n"
f" HostName {dns_hostname}\n"
f" User backup\n"
f" Port 22\n"
f" IdentityFile ~/.ssh/backup_key\n"
)
config = Path(ssh_config_path)
config.parent.mkdir(parents=True, exist_ok=True)
if config.exists():
with open(config, "a", encoding="utf-8") as f:
f.write(entry)
else:
config.write_text(entry, encoding="utf-8")
logger.info("Deployed DNS canary in SSH config: %s", ssh_config_path)
return {
"type": "ssh_config_canary",
"ssh_config_path": str(config),
"host_alias": host_alias,
"dns_hostname": dns_hostname,
"deployed_at": datetime.now(timezone.utc).isoformat(),
}
def deploy_env_file_token(env_path, access_key_id, secret_access_key,
additional_vars=None):
"""
Deploy a fake .env file containing canary AWS credentials and optional extras.
Attackers harvesting environment files from repos or servers will trigger
the token when they attempt to use the credentials.
"""
lines = [
"# Application configuration",
f"AWS_ACCESS_KEY_ID={access_key_id}",
f"AWS_SECRET_ACCESS_KEY={secret_access_key}",
"AWS_DEFAULT_REGION=us-east-1",
"DATABASE_URL=postgresql://readonly:readonly@db.internal:5432/app",
]
if additional_vars:
for k, v in additional_vars.items():
lines.append(f"{k}={v}")
target = Path(env_path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
logger.info("Deployed canary .env file: %s", env_path)
return {
"type": "env_file_canary",
"path": str(target),
"deployed_at": datetime.now(timezone.utc).isoformat(),
}
# ---------------------------------------------------------------------------
# Webhook alert processing and forwarding
# ---------------------------------------------------------------------------
def send_slack_alert(webhook_url, alert_data):
"""
Forward a canary token alert to a Slack channel via incoming webhook.
Args:
webhook_url: Slack incoming webhook URL
alert_data: Dict with alert details (memo, src_ip, channel, time, etc.)
"""
payload = {
"text": ":rotating_light: *Canary Token Triggered -- Possible Intrusion*",
"attachments": [
{
"color": "#FF0000",
"fields": [
{
"title": "Token Description",
"value": alert_data.get("memo", "Unknown token"),
"short": True,
},
{
"title": "Source IP",
"value": alert_data.get("src_ip", "Unknown"),
"short": True,
},
{
"title": "Token Type",
"value": alert_data.get("channel", alert_data.get("token_type", "Unknown")),
"short": True,
},
{
"title": "Triggered At",
"value": alert_data.get("time", datetime.now(timezone.utc).isoformat()),
"short": True,
},
{
"title": "User Agent",
"value": alert_data.get("additional_data", {}).get("useragent", "N/A"),
"short": False,
},
{
"title": "Management URL",
"value": alert_data.get("manage_url", "N/A"),
"short": False,
},
],
"footer": "Canary Token Intrusion Detection System",
"ts": int(datetime.now(timezone.utc).timestamp()),
}
],
}
resp = requests.post(webhook_url, json=payload, timeout=10)
resp.raise_for_status()
logger.info("Slack alert sent for: %s", alert_data.get("memo", ""))
def send_teams_alert(webhook_url, alert_data):
"""
Forward a canary token alert to Microsoft Teams via incoming webhook.
Args:
webhook_url: Teams incoming webhook URL
alert_data: Dict with alert details
"""
payload = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "FF0000",
"summary": "Canary Token Triggered",
"sections": [
{
"activityTitle": "Canary Token Triggered -- Possible Intrusion",
"facts": [
{"name": "Token", "value": alert_data.get("memo", "Unknown")},
{"name": "Source IP", "value": alert_data.get("src_ip", "Unknown")},
{"name": "Type", "value": alert_data.get("channel", "Unknown")},
{"name": "Time", "value": alert_data.get("time", "Unknown")},
],
"markdown": True,
}
],
}
resp = requests.post(webhook_url, json=payload, timeout=10)
resp.raise_for_status()
logger.info("Teams alert sent for: %s", alert_data.get("memo", ""))
def send_email_alert(smtp_config, alert_data):
"""
Send a canary token alert via email.
Args:
smtp_config: Dict with server, port, username, password, from_addr, to_addr
alert_data: Dict with alert details
"""
msg = MIMEMultipart("alternative")
msg["Subject"] = f"[CANARY ALERT] Token Triggered: {alert_data.get('memo', 'Unknown')}"
msg["From"] = smtp_config["from_addr"]
msg["To"] = smtp_config["to_addr"]
text_body = (
f"CANARY TOKEN ALERT\n"
f"{'=' * 50}\n"
f"Token: {alert_data.get('memo', 'Unknown')}\n"
f"Type: {alert_data.get('channel', 'Unknown')}\n"
f"Source IP: {alert_data.get('src_ip', 'Unknown')}\n"
f"Time: {alert_data.get('time', 'Unknown')}\n"
f"Management: {alert_data.get('manage_url', 'N/A')}\n"
f"{'=' * 50}\n"
f"This alert was generated by the Canary Token Intrusion Detection System.\n"
)
html_body = f"""<html>
<body>
<h2 style="color:red;">Canary Token Triggered</h2>
<table border="1" cellpadding="8">
<tr><td><b>Token</b></td><td>{alert_data.get('memo', 'Unknown')}</td></tr>
<tr><td><b>Type</b></td><td>{alert_data.get('channel', 'Unknown')}</td></tr>
<tr><td><b>Source IP</b></td><td>{alert_data.get('src_ip', 'Unknown')}</td></tr>
<tr><td><b>Time</b></td><td>{alert_data.get('time', 'Unknown')}</td></tr>
<tr><td><b>Management</b></td><td><a href="{alert_data.get('manage_url', '#')}">{alert_data.get('manage_url', 'N/A')}</a></td></tr>
</table>
</body>
</html>"""
msg.attach(MIMEText(text_body, "plain"))
msg.attach(MIMEText(html_body, "html"))
with smtplib.SMTP(smtp_config["server"], smtp_config.get("port", 587)) as server:
server.starttls()
server.login(smtp_config["username"], smtp_config["password"])
server.send_message(msg)
logger.info("Email alert sent to %s for: %s", smtp_config["to_addr"], alert_data.get("memo", ""))
def forward_to_siem(siem_url, alert_data, api_key=None):
"""
Forward canary token alert to a SIEM system via HTTP API.
Formats the alert as a structured security event suitable for
ingestion by Splunk HEC, Elastic, or similar SIEM platforms.
"""
siem_event = {
"event_type": "canarytoken_triggered",
"severity": "high",
"source": "canary_token_ids",
"timestamp": alert_data.get("time", datetime.now(timezone.utc).isoformat()),
"details": {
"memo": alert_data.get("memo"),
"token_type": alert_data.get("channel"),
"source_ip": alert_data.get("src_ip"),
"user_agent": alert_data.get("additional_data", {}).get("useragent"),
"manage_url": alert_data.get("manage_url"),
},
"mitre_attack": {
"tactic": "Discovery",
"technique": "T1083",
"description": "File and Directory Discovery -- attacker accessed canary resource",
},
}
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
resp = requests.post(siem_url, json=siem_event, headers=headers, timeout=15)
resp.raise_for_status()
logger.info("SIEM event forwarded for: %s", alert_data.get("memo", ""))
# ---------------------------------------------------------------------------
# Token inventory and monitoring
# ---------------------------------------------------------------------------
def create_deployment_plan(environment, zones=None):
"""
Generate a comprehensive canary token deployment plan for an environment.
Args:
environment: Target environment name (production, staging, corporate)
zones: Optional list of network zones to include
Returns:
Deployment plan with recommended token placements
"""
default_zones = {
"dmz": [
{"type": "http", "location": "/var/www/admin/index.html",
"memo": f"DMZ admin panel -- {environment}",
"description": "Hidden image beacon in web server admin page"},
{"type": "dns", "location": "/etc/nginx/conf.d/upstream.conf",
"memo": f"DMZ nginx upstream -- {environment}",
"description": "DNS canary in nginx upstream config"},
],
"internal": [
{"type": "aws_keys", "location": "/home/deploy/.aws/credentials",
"memo": f"Internal deploy creds -- {environment}",
"description": "Fake AWS credentials on deployment server"},
{"type": "dns", "location": "/etc/app/database.yml",
"memo": f"Internal DB config -- {environment}",
"description": "DNS canary in database configuration"},
{"type": "http", "location": "/opt/wiki/pages/emergency-passwords.html",
"memo": f"Internal wiki passwords page -- {environment}",
"description": "HTTP beacon in internal wiki sensitive page"},
],
"production": [
{"type": "aws_keys", "location": "/opt/app/.env",
"memo": f"Production .env file -- {environment}",
"description": "Canary AWS keys in production env file"},
{"type": "dns", "location": "/etc/ssh/ssh_config",
"memo": f"Production SSH config -- {environment}",
"description": "DNS canary in SSH configuration"},
{"type": "dns", "location": "/opt/backup/config.ini",
"memo": f"Production backup config -- {environment}",
"description": "DNS canary in backup server config"},
],
"cloud": [
{"type": "aws_keys", "location": "s3://config-bucket/.env.backup",
"memo": f"Cloud S3 env backup -- {environment}",
"description": "Canary AWS keys in S3 configuration bucket"},
{"type": "dns", "location": "terraform/modules/networking/vars.tf",
"memo": f"Cloud Terraform vars -- {environment}",
"description": "DNS canary in Terraform variable definitions"},
],
}
selected_zones = zones or list(default_zones.keys())
plan_tokens = []
for zone in selected_zones:
if zone in default_zones:
for token_spec in default_zones[zone]:
token_spec["zone"] = zone
plan_tokens.append(token_spec)
return {
"environment": environment,
"zones": selected_zones,
"total_tokens": len(plan_tokens),
"tokens": plan_tokens,
"generated_at": datetime.now(timezone.utc).isoformat(),
}
def build_token_inventory(report_dir):
"""
Build an inventory of all deployed canary tokens from report files.
Scans the report directory for deployment reports and consolidates
them into a single inventory.
"""
inventory = {"tokens": [], "total": 0, "by_type": {}, "by_zone": {}}
report_path = Path(report_dir)
if not report_path.exists():
logger.warning("Report directory not found: %s", report_dir)
return inventory
for report_file in report_path.glob("*.json"):
with open(report_file, encoding="utf-8") as f:
report = json.load(f)
for token_key, token_data in report.get("tokens", {}).items():
if isinstance(token_data, dict):
inventory["tokens"].append(token_data)
token_type = token_data.get("type", "unknown")
inventory["by_type"][token_type] = (
inventory["by_type"].get(token_type, 0) + 1
)
if "deployment_plan" in report:
for token_spec in report["deployment_plan"].get("tokens", []):
zone = token_spec.get("zone", "unknown")
inventory["by_zone"][zone] = (
inventory["by_zone"].get(zone, 0) + 1
)
inventory["total"] = len(inventory["tokens"])
inventory["generated_at"] = datetime.now(timezone.utc).isoformat()
return inventory
def check_token_alerts(webhook_log_path):
"""
Parse webhook logs to identify triggered canary token alerts.
Args:
webhook_log_path: Path to the JSON log file from webhook receiver
Returns:
List of alert dicts with token details and trigger information
"""
log_path = Path(webhook_log_path)
if not log_path.exists():
logger.warning("Webhook log not found: %s", webhook_log_path)
return []
alerts = []
with open(log_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("event_type") == "canarytoken_triggered":
alerts.append({
"token_memo": entry.get("memo", ""),
"token_type": entry.get("token_type", ""),
"source_ip": entry.get("src_ip", ""),
"triggered_at": entry.get("time", ""),
"user_agent": entry.get("additional_data", {}).get("useragent", ""),
"manage_url": entry.get("manage_url", ""),
"severity": "high",
})
logger.info("Found %d triggered alerts in %s", len(alerts), webhook_log_path)
return alerts
def test_token_connectivity(token_hostname=None, token_url=None):
"""
Validate that a canary token is reachable and can trigger alerts.
WARNING: This will trigger the actual canary token alert.
Only use during initial deployment validation.
"""
results = {"dns": None, "http": None}
if token_hostname:
try:
resolved = socket.getaddrinfo(token_hostname, None)
results["dns"] = {
"status": "resolved",
"hostname": token_hostname,
"addresses": [r[4][0] for r in resolved],
}
logger.info("DNS token test: %s resolved successfully", token_hostname)
except socket.gaierror as e:
results["dns"] = {
"status": "resolution_failed",
"hostname": token_hostname,
"error": str(e),
}
logger.warning("DNS token test failed for %s: %s", token_hostname, e)
if token_url:
try:
resp = requests.get(token_url, timeout=10, allow_redirects=True)
results["http"] = {
"status": "reachable",
"url": token_url,
"http_status": resp.status_code,
}
logger.info("HTTP token test: %s returned %d", token_url, resp.status_code)
except requests.RequestException as e:
results["http"] = {
"status": "unreachable",
"url": token_url,
"error": str(e),
}
logger.warning("HTTP token test failed for %s: %s", token_url, e)
return results
# ---------------------------------------------------------------------------
# Main CLI
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Canary Token Network Intrusion Detection Agent",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Actions:
create_dns Create a DNS canary token via Canarytokens.org
create_http Create an HTTP canary token
create_aws Create an AWS API key canary token
create_web_img Create a web image beacon canary token
plan Generate a deployment plan for an environment
full_deploy Create all token types and generate deployment plan
monitor Check for triggered alerts in webhook logs
inventory Build inventory from deployment reports
test Test connectivity to deployed tokens (triggers alerts!)
Examples:
python agent.py --action plan --environment production
python agent.py --action create_dns --email soc@company.com --webhook https://hooks.slack.com/...
python agent.py --action full_deploy --email soc@company.com --output deploy_report.json
python agent.py --action monitor --webhook-log /var/log/canary_alerts.json
""",
)
parser.add_argument("--action", required=True, choices=[
"create_dns", "create_http", "create_aws", "create_web_img",
"plan", "full_deploy", "monitor", "inventory", "test",
])
parser.add_argument("--email", default=os.getenv("CANARY_EMAIL", "soc@company.com"),
help="Notification email for token alerts")
parser.add_argument("--webhook", default=os.getenv("CANARY_WEBHOOK"),
help="Webhook URL for real-time alerts (Slack/Teams/generic)")
parser.add_argument("--memo", default=None,
help="Human-readable description for the token")
parser.add_argument("--environment", default="production",
help="Target environment for deployment plan")
parser.add_argument("--zones", nargs="*", default=None,
help="Network zones to include in deployment plan")
parser.add_argument("--output", default="canary_token_report.json",
help="Output file path for report")
parser.add_argument("--webhook-log", default="/var/log/canary_alerts.json",
help="Path to webhook alert log for monitoring")
parser.add_argument("--report-dir", default="./reports",
help="Directory containing deployment reports for inventory")
parser.add_argument("--console-domain", default=os.getenv("THINKST_DOMAIN"),
help="Thinkst Canary console domain (enterprise)")
parser.add_argument("--api-key", default=os.getenv("THINKST_API_KEY"),
help="Thinkst Canary API auth token (enterprise)")
parser.add_argument("--test-hostname", default=None,
help="DNS hostname to test connectivity")
parser.add_argument("--test-url", default=None,
help="HTTP URL to test connectivity")
args = parser.parse_args()
report = {
"agent": "canary-token-intrusion-detection",
"generated_at": datetime.now(timezone.utc).isoformat(),
"action": args.action,
"tokens": {},
}
# --- Deployment Plan ---
if args.action == "plan":
plan = create_deployment_plan(args.environment, args.zones)
report["deployment_plan"] = plan
print(f"[+] Deployment plan generated: {plan['total_tokens']} tokens across "
f"{len(plan['zones'])} zones")
for token in plan["tokens"]:
print(f" [{token['zone']}] {token['type']:10s} -> {token['location']}")
# --- DNS Token ---
if args.action in ("create_dns", "full_deploy"):
memo = args.memo or f"DNS canary -- {args.environment}"
token = create_dns_token(args.email, memo, args.webhook)
report["tokens"]["dns"] = token
print(f"[+] DNS canary token created: {token.get('hostname', 'N/A')}")
# --- HTTP Token ---
if args.action in ("create_http", "full_deploy"):
memo = args.memo or f"HTTP canary -- {args.environment}"
token = create_http_token(args.email, memo, args.webhook)
report["tokens"]["http"] = token
print(f"[+] HTTP canary token created: {token.get('url', 'N/A')}")
# --- AWS Key Token ---
if args.action in ("create_aws", "full_deploy"):
memo = args.memo or f"AWS key canary -- {args.environment}"
token = create_aws_key_token(args.email, memo, args.webhook)
report["tokens"]["aws_keys"] = token
print(f"[+] AWS key canary token created: {token.get('access_key_id', 'N/A')}")
# --- Web Image Token ---
if args.action in ("create_web_img", "full_deploy"):
memo = args.memo or f"Web beacon canary -- {args.environment}"
token = create_web_image_token(args.email, memo, args.webhook)
report["tokens"]["web_image"] = token
print(f"[+] Web image canary token created: {token.get('image_url', 'N/A')}")
# --- Full Deploy also generates plan ---
if args.action == "full_deploy":
plan = create_deployment_plan(args.environment, args.zones)
report["deployment_plan"] = plan
print(f"[+] Deployment plan: {plan['total_tokens']} tokens planned")
# --- Monitor ---
if args.action == "monitor":
if args.console_domain and args.api_key:
alerts = thinkst_get_alerts(args.console_domain, args.api_key)
report["enterprise_alerts"] = alerts
print(f"[+] Thinkst Canary: {len(alerts)} triggered alerts found")
for alert in alerts:
print(f" [ALERT] {alert}")
else:
alerts = check_token_alerts(args.webhook_log)
report["webhook_alerts"] = alerts
print(f"[+] Webhook log: {len(alerts)} triggered alerts found")
for alert in alerts:
print(f" [ALERT] {alert.get('token_memo', 'Unknown')} "
f"from {alert.get('source_ip', 'Unknown')} "
f"at {alert.get('triggered_at', 'Unknown')}")
# --- Inventory ---
if args.action == "inventory":
inventory = build_token_inventory(args.report_dir)
report["inventory"] = inventory
print(f"[+] Token inventory: {inventory['total']} tokens")
for token_type, count in inventory.get("by_type", {}).items():
print(f" {token_type}: {count}")
# --- Test ---
if args.action == "test":
print("[!] WARNING: Testing tokens will trigger real alerts!")
results = test_token_connectivity(args.test_hostname, args.test_url)
report["test_results"] = results
for test_type, result in results.items():
if result:
print(f" [{test_type}] {result.get('status', 'unknown')}")
# --- Write report ---
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()