npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
NIST CSF 2.0
MITRE D3FEND
When to Use
- When proactively searching for compromised endpoints calling back to C2 infrastructure
- After threat intelligence reports indicate active C2 frameworks targeting your sector
- When network logs show periodic outbound connections to unfamiliar destinations
- During purple team exercises validating C2 detection capabilities
- When investigating a potential breach and need to identify active C2 channels
Prerequisites
- Network proxy/firewall logs with timestamps and destination data (minimum 24 hours)
- Zeek conn.log, dns.log, and ssl.log or equivalent NetFlow/IPFIX data
- SIEM platform with statistical analysis capability (Splunk, Elastic, Microsoft Sentinel)
- RITA (Real Intelligence Threat Analytics) or AC-Hunter for automated beacon analysis
- Threat intelligence feeds for domain/IP reputation enrichment
Workflow
- Define Beacon Parameters: Establish detection thresholds -- coefficient of variation (CV) below 0.20 indicates strong periodicity, minimum 50 connections over 24 hours, average interval between 30 seconds and 24 hours.
- Collect Network Telemetry: Aggregate proxy logs, DNS queries, firewall connection logs, and Zeek metadata into the analysis platform.
- Calculate Connection Intervals: For each source-destination pair, compute the time delta between consecutive connections and derive mean interval, standard deviation, and CV.
- Apply Jitter Analysis: Sophisticated C2 frameworks like Cobalt Strike add jitter (randomness) to beacon intervals. The Sunburst backdoor beaconed every 15 minutes plus/minus 90 seconds. Analyze jitter patterns to detect even randomized beaconing.
- Filter Legitimate Periodic Traffic: Exclude known-good beaconing sources including Windows Update, antivirus definition updates, NTP synchronization, SaaS heartbeat services, and CDN health checks.
- Analyze Data Size Consistency: C2 heartbeat packets typically have consistent payload sizes. Calculate the CV of bytes transferred per connection -- low variance suggests automated communication.
- Enrich with Threat Intelligence: Check identified beaconing destinations against VirusTotal, WHOIS registration data (flag domains under 30 days old), certificate transparency logs, and passive DNS history.
- Correlate with Endpoint Telemetry: Map beaconing source IPs to endpoint hostnames via DHCP logs, then correlate with process creation events (Sysmon Event ID 1, 3) to identify the responsible process.
- Score and Prioritize: Assign risk scores based on CV value, domain age, TI matches, data size consistency, and suspicious port usage. Escalate high-confidence findings.
Key Concepts
| Concept | Description |
|---|---|
| T1071.001 | Application Layer Protocol: Web Protocols -- HTTP/HTTPS beaconing |
| T1071.004 | Application Layer Protocol: DNS -- DNS-based C2 tunneling |
| T1573 | Encrypted Channel -- TLS/SSL encrypted C2 communication |
| T1568.002 | Dynamic Resolution: Domain Generation Algorithms |
| Coefficient of Variation | Standard deviation divided by mean; values below 0.20 indicate periodicity |
| Jitter | Random variation added to beacon interval to evade detection |
| RITA Beacon Score | Composite score from connection regularity, data size consistency, and connection count |
| JA3/JA4 Fingerprinting | TLS client fingerprinting to identify C2 framework signatures |
| Fast-Flux DNS | Rapidly changing DNS resolution used to protect C2 infrastructure |
Tools & Systems
| Tool | Purpose |
|---|---|
| RITA (Real Intelligence Threat Analytics) | Automated beacon scoring from Zeek logs |
| AC-Hunter | Commercial threat hunting platform with beacon detection |
| Splunk | SPL-based statistical beacon analysis with streamstats |
| Elastic Security | ML anomaly detection for periodic network behavior |
| Zeek | Network metadata collection (conn.log, dns.log, ssl.log) |
| Suricata | Network IDS with JA3/JA4 TLS fingerprint extraction |
| FLARE | C2 profile and beacon pattern detection |
| VirusTotal | Domain and IP reputation enrichment |
Detection Queries
Splunk -- HTTP/S Beacon Frequency Analysis
index=proxy OR index=firewall
| where NOT match(dest, "(?i)(microsoft|google|amazonaws|cloudflare|akamai)")
| bin _time span=1s
| stats count by src_ip dest _time
| streamstats current=f last(_time) as prev_time by src_ip dest
| eval interval=_time-prev_time
| stats count avg(interval) as avg_interval stdev(interval) as stdev_interval
min(interval) as min_interval max(interval) as max_interval by src_ip dest
| where count > 50
| eval cv=stdev_interval/avg_interval
| where cv < 0.20 AND avg_interval > 30 AND avg_interval < 86400
| sort cv
| table src_ip dest count avg_interval stdev_interval cvKQL -- Microsoft Sentinel Beacon Detection
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| summarize ConnectionTimes=make_list(Timestamp), Count=count() by DeviceName, RemoteIP, RemoteUrl
| where Count > 50
| extend Intervals = array_sort_asc(ConnectionTimes)
| mv-apply Intervals on (
extend NextTime = next(Intervals)
| where isnotempty(NextTime)
| extend IntervalSec = datetime_diff('second', NextTime, Intervals)
| summarize AvgInterval=avg(IntervalSec), StdDev=stdev(IntervalSec)
)
| extend CV = StdDev / AvgInterval
| where CV < 0.2 and AvgInterval > 30
| sort by CV ascSigma Rule -- Beaconing Pattern Detection
title: Potential C2 Beaconing Pattern Detected
status: experimental
logsource:
category: proxy
detection:
selection:
dst_ip|cidr: '!10.0.0.0/8'
timeframe: 24h
condition: selection | count(dst) by src_ip > 50
level: medium
tags:
- attack.command_and_control
- attack.t1071.001Common Scenarios
- Cobalt Strike Beacon: Default 60-second interval with configurable 0-50% jitter over HTTPS. Malleable C2 profiles can mimic legitimate traffic patterns.
- Sunburst/SUNSPOT: 12-14 day dormancy period, then beaconing every 12-14 minutes with randomized jitter, designed to evade frequency analysis.
- DNS Tunneling C2: Encoded data exfiltration via DNS TXT/CNAME queries to attacker-controlled domains, detectable via high subdomain entropy and query volume.
- Sliver C2: Modern C2 framework with HTTPS, mTLS, and WireGuard protocols, configurable beacon intervals with built-in jitter support.
- Legitimate Service Abuse: C2 communication over Slack, Discord, Telegram, or cloud storage APIs, making destination-based filtering ineffective.
Output Format
Hunt ID: TH-BEACON-[DATE]-[SEQ]
Source IP: [Internal IP]
Source Host: [Hostname from DHCP/DNS]
Destination: [Domain/IP]
Protocol: [HTTP/HTTPS/DNS]
Beacon Interval: [Average seconds]
Jitter Estimate: [Percentage]
Coefficient of Variation: [CV value]
Connection Count: [Total connections in window]
Data Size CV: [Payload consistency metric]
Domain Age: [Days since registration]
TI Match: [Yes/No -- source]
Risk Score: [0-100]
Risk Level: [Critical/High/Medium/Low]
Indicators: [List of triggered risk factors]References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md2.6 KB
API Reference: Beaconing Detection via Frequency Analysis
Beaconing Characteristics
| Characteristic | Description |
|---|---|
| Regular intervals | Connections at fixed time periods |
| Low jitter | Small variance in intervals |
| Persistent | Continues over hours/days |
| Consistent size | Similar packet sizes |
Jitter Calculation
Standard Deviation of Intervals
import math
intervals = [t[i+1] - t[i] for i in range(len(t)-1)]
mean = sum(intervals) / len(intervals)
variance = sum((x - mean)**2 for x in intervals) / len(intervals)
jitter = math.sqrt(variance)
jitter_percent = (jitter / mean) * 100Jitter Thresholds
| Jitter % | Confidence | Likely Cause |
|---|---|---|
| < 5% | HIGH | Automated C2 beacon |
| 5-15% | MEDIUM | Possible C2 with sleep jitter |
| 15-30% | LOW | May be legitimate polling |
| > 30% | NONE | Likely human/random |
Zeek conn.log Format
Fields
| Index | Name | Description |
|---|---|---|
| 0 | ts | Unix timestamp |
| 2 | id.orig_h | Source IP |
| 3 | id.orig_p | Source port |
| 4 | id.resp_h | Destination IP |
| 5 | id.resp_p | Destination port |
| 6 | proto | Protocol |
| 9 | orig_bytes | Sent bytes |
| 10 | resp_bytes | Received bytes |
RITA (Real Intelligence Threat Analytics)
Analyze Zeek Logs
rita import /path/to/zeek/logs dataset_name
rita show-beacons dataset_nameOutput Columns
| Column | Description |
|---|---|
| Score | Beacon probability (0-1) |
| Source | Source IP |
| Destination | Destination IP |
| Connections | Total connections |
| Avg Bytes | Average data transfer |
Splunk SPL — Beacon Detection
index=network sourcetype=zeek:conn
| bin _time span=60s
| stats count by src_ip, dest_ip, dest_port, _time
| streamstats window=100 stdev(count) as jitter avg(count) as avg_count by src_ip, dest_ip
| where jitter/avg_count < 0.15
| stats count as beacon_count by src_ip, dest_ip, dest_port
| where beacon_count > 100Elastic SIEM — Beacon Detection
{
"query": {
"bool": {
"must": [
{"range": {"@timestamp": {"gte": "now-24h"}}},
{"exists": {"field": "destination.ip"}}
]
}
},
"aggs": {
"by_flow": {
"composite": {
"sources": [
{"src": {"terms": {"field": "source.ip"}}},
{"dst": {"terms": {"field": "destination.ip"}}}
]
}
}
}
}Common C2 Beacon Intervals
| Framework | Default Interval |
|---|---|
| Cobalt Strike | 60 seconds |
| Metasploit | 5 seconds |
| Empire | 5 seconds |
| Covenant | 10 seconds |
| Sliver | 60 seconds |
standards.md3.5 KB
Standards and References - Beaconing Frequency Analysis
MITRE ATT&CK Command and Control (TA0011)
| Technique | Name | Beacon Indicators |
|---|---|---|
| T1071.001 | Web Protocols | HTTP/HTTPS periodic connections with regular intervals |
| T1071.004 | DNS | DNS query patterns, tunneling with high entropy subdomains |
| T1573.001 | Symmetric Cryptography | Encrypted C2 channels with consistent packet sizes |
| T1573.002 | Asymmetric Cryptography | TLS C2 with custom or self-signed certificates |
| T1572 | Protocol Tunneling | DNS-over-HTTPS, ICMP tunneling with periodic patterns |
| T1568.002 | Domain Generation Algorithms | Algorithmically generated domains with high entropy |
| T1568.001 | Fast Flux DNS | Rapidly rotating IP addresses for C2 infrastructure |
| T1132.001 | Standard Encoding | Base64/hex encoded data in C2 traffic |
| T1095 | Non-Application Layer Protocol | ICMP, raw TCP/UDP for covert C2 |
| T1090 | Proxy | Multi-hop C2 infrastructure obscuring origin |
| T1102 | Web Service | C2 over legitimate cloud services |
Beaconing Detection Thresholds
| Metric | Threshold | Rationale |
|---|---|---|
| Coefficient of Variation (CV) | < 0.20 | Strong indicator of periodic automated communication |
| Minimum Beacon Interval | > 30 seconds | Below this may be streaming or polling traffic |
| Maximum Beacon Interval | < 86400 seconds | Beyond 24 hours reduces statistical significance |
| Minimum Connection Count | > 50 per 24 hours | Needed for reliable statistical analysis |
| Data Size CV | < 0.30 | Consistent payloads suggest automated heartbeats |
| Domain Age | < 30 days | Newly registered domains associated with C2 infrastructure |
Known C2 Framework Default Beacon Profiles
| Framework | Default Interval | Default Jitter | Protocols | Detection Notes |
|---|---|---|---|---|
| Cobalt Strike | 60s | 0-50% | HTTPS, DNS | Malleable C2 profiles can change all defaults |
| Sliver | 60s | 0-30% | HTTPS, mTLS, WireGuard, DNS | Supports domain fronting |
| Brute Ratel C4 | 60s | 10-30% | HTTPS, DNS, SMB | Designed to evade EDR detection |
| Metasploit Meterpreter | 5s | 0% | TCP, HTTP/S | Highly configurable via handlers |
| Havoc | 5s | 0-20% | HTTPS | Demon agent with sleep obfuscation |
| Mythic | Configurable | Configurable | HTTP/S, TCP, WebSocket | Agent-dependent behavior |
| Covenant | 10s | 10% | HTTP/S | .NET-based C2 framework |
| Empire/Starkiller | 5s | 0-20% | HTTP/S | Python-based listeners |
Statistical Methods for Beacon Detection
| Method | Description | Best For |
|---|---|---|
| Coefficient of Variation | stdev/mean of intervals | Regular beacons with low jitter |
| Fast Fourier Transform (FFT) | Frequency domain analysis | Detecting periodic signals in noisy data |
| Autocorrelation | Self-correlation at various lags | Identifying repeating patterns |
| Median Absolute Deviation | Robust measure of variability | Beacon detection resistant to outliers |
| Kullback-Leibler Divergence | Distribution comparison | Comparing observed intervals to uniform distribution |
RITA Beacon Scoring Algorithm
RITA scores beacons on a 0-1 scale using:
- Timestamp score: Regularity of connection intervals
- Data size score: Consistency of bytes transferred
- Connection count: Volume of connections in analysis window
- Duration: How long the beaconing pattern has persisted
Composite score above 0.70 is considered high-confidence beaconing.
workflows.md4.7 KB
Detailed Hunting Workflow - Beaconing Frequency Analysis
Phase 1: Data Collection and Preparation
Step 1.1 - Gather Network Connection Logs
Collect at minimum 24 hours (ideally 7 days) of:
- Proxy/firewall logs with timestamps, source/destination, bytes
- Zeek conn.log for connection metadata
- Zeek dns.log for DNS query analysis
- Zeek ssl.log for TLS certificate and JA3 fingerprinting
- NetFlow/IPFIX for high-level flow data
Step 1.2 - Normalize Timestamps
Ensure all timestamps are in a consistent format (epoch or ISO 8601) and timezone (UTC). Misaligned timestamps will corrupt interval calculations.
Phase 2: Statistical Frequency Analysis
Step 2.1 - Splunk Interval Calculation
index=proxy OR index=firewall
| where NOT match(dest, "(?i)(microsoft|google|amazonaws|cloudflare|akamai|apple|adobe)")
| bin _time span=1s
| stats count by src_ip dest _time
| streamstats current=f last(_time) as prev_time by src_ip dest
| eval interval=_time-prev_time
| stats count avg(interval) as avg_interval stdev(interval) as stdev_interval
min(interval) as min_interval max(interval) as max_interval
dc(interval) as unique_intervals by src_ip dest
| where count > 50
| eval cv=stdev_interval/avg_interval
| eval jitter_pct=round((stdev_interval/avg_interval)*100, 1)
| where cv < 0.25 AND avg_interval > 30 AND avg_interval < 86400
| sort cv
| table src_ip dest count avg_interval stdev_interval cv jitter_pctStep 2.2 - Elastic Query for Beacon Detection
{
"aggs": {
"by_pair": {
"composite": {
"sources": [
{"src": {"terms": {"field": "source.ip"}}},
{"dst": {"terms": {"field": "destination.domain"}}}
]
},
"aggs": {
"timestamps": {
"date_histogram": {"field": "@timestamp", "fixed_interval": "1s"}
},
"stats": {
"extended_stats": {"field": "event.duration"}
}
}
}
}
}Step 2.3 - RITA Automated Analysis
# Import Zeek logs into RITA
rita import /path/to/zeek/logs mydataset
# Analyze beacons
rita show-beacons mydataset
# Export results as CSV
rita show-beacons mydataset --csv > beacon_results.csv
# Show long connections
rita show-long-connections mydatasetPhase 3: Jitter-Aware Detection
Step 3.1 - Detect Beacons with Jitter
Cobalt Strike adds configurable jitter (0-50%) to its sleep timer. A 60-second beacon with 30% jitter produces intervals between 42-78 seconds.
index=proxy
| stats count by src_ip dest _time
| streamstats current=f last(_time) as prev_time by src_ip dest
| eval interval=_time-prev_time
| stats count avg(interval) as avg stdev(interval) as sd
percentile25(interval) as p25 percentile75(interval) as p75 by src_ip dest
| where count > 50
| eval iqr=p75-p25
| eval jitter_ratio=iqr/avg
| where jitter_ratio < 0.50 AND avg > 30
| sort jitter_ratioPhase 4: Data Size Consistency Analysis
Step 4.1 - Payload Size Regularity
index=proxy
| stats count avg(bytes_out) as avg_bytes stdev(bytes_out) as sd_bytes
by src_ip dest
| where count > 50
| eval data_cv=sd_bytes/avg_bytes
| where data_cv < 0.30
| sort data_cvPhase 5: Domain Intelligence Enrichment
Step 5.1 - Check Domain Age via WHOIS
Flag any beaconing destination with domain registration under 30 days. Newly registered domains correlate strongly with C2 infrastructure.
Step 5.2 - JA3/JA4 TLS Fingerprinting
index=zeek sourcetype=bro_ssl
| stats count dc(id.resp_h) as unique_dests values(server_name) as domains by ja3
| lookup ja3_known_c2 ja3 OUTPUT framework
| where isnotnull(framework)
| table ja3 framework count unique_dests domainsPhase 6: Endpoint Correlation
Step 6.1 - Map Network to Process
index=sysmon EventCode=3
| where NOT cidrmatch("10.0.0.0/8", DestinationIp)
AND NOT cidrmatch("172.16.0.0/12", DestinationIp)
AND NOT cidrmatch("192.168.0.0/16", DestinationIp)
| stats count values(DestinationPort) as ports dc(DestinationIp) as unique_ips
by Image Computer DestinationIp
| where count > 50 AND unique_ips < 3
| sort -countPhase 7: Verification and Response
Step 7.1 - Confirm C2 Activity
- Capture packet sample of suspected C2 traffic
- Analyze TLS certificate (self-signed, unusual issuer, short validity)
- Cross-reference domain/IP against multiple TI sources
- Review process tree on source endpoint
- Check for associated lateral movement or tool transfers
Step 7.2 - Containment Actions
- Block C2 domain/IP at firewall, proxy, and DNS sinkhole
- Isolate compromised endpoint via EDR network containment
- Preserve memory dump and disk image for forensics
- Reset credentials used on affected systems
- Sweep environment for additional infections using discovered IOCs
Scripts 2
agent.py5.0 KB
#!/usr/bin/env python3
"""Agent for detecting C2 beaconing through network traffic frequency analysis."""
import argparse
import json
import math
from collections import defaultdict
from datetime import datetime, timezone
def parse_zeek_conn_log(log_path):
"""Parse Zeek conn.log and extract connection timestamps per src-dst pair."""
connections = defaultdict(list)
try:
with open(log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
fields = line.strip().split("\t")
if len(fields) < 7:
continue
ts = float(fields[0])
src, dst = fields[2], fields[4]
dst_port = fields[5]
key = f"{src}->{dst}:{dst_port}"
connections[key].append(ts)
except (FileNotFoundError, ValueError):
pass
return connections
def calculate_jitter(intervals):
"""Calculate jitter (standard deviation of intervals)."""
if len(intervals) < 2:
return 0
mean = sum(intervals) / len(intervals)
variance = sum((x - mean) ** 2 for x in intervals) / len(intervals)
return math.sqrt(variance)
def detect_beaconing(connections, min_connections=10, max_jitter_percent=15):
"""Detect beaconing patterns based on interval regularity."""
beacons = []
for key, timestamps in connections.items():
if len(timestamps) < min_connections:
continue
timestamps.sort()
intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)]
if not intervals:
continue
mean_interval = sum(intervals) / len(intervals)
if mean_interval == 0:
continue
jitter = calculate_jitter(intervals)
jitter_percent = (jitter / mean_interval) * 100
if jitter_percent <= max_jitter_percent:
parts = key.split("->")
src = parts[0]
dst_port = parts[1] if len(parts) > 1 else ""
beacons.append({
"flow": key,
"connection_count": len(timestamps),
"mean_interval_seconds": round(mean_interval, 2),
"jitter_seconds": round(jitter, 2),
"jitter_percent": round(jitter_percent, 2),
"duration_hours": round((timestamps[-1] - timestamps[0]) / 3600, 2),
"confidence": "HIGH" if jitter_percent < 5 else "MEDIUM",
})
return sorted(beacons, key=lambda x: x["jitter_percent"])
def parse_csv_log(csv_path):
"""Parse generic CSV log with timestamp, src, dst, port columns."""
connections = defaultdict(list)
try:
import csv
with open(csv_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
ts = row.get("timestamp") or row.get("ts") or row.get("time")
src = row.get("src") or row.get("source") or row.get("src_ip")
dst = row.get("dst") or row.get("destination") or row.get("dst_ip")
port = row.get("dst_port") or row.get("port") or ""
if ts and src and dst:
try:
ts_float = float(ts)
except ValueError:
from datetime import datetime as dt
try:
ts_float = dt.fromisoformat(ts.replace("Z", "+00:00")).timestamp()
except ValueError:
continue
connections[f"{src}->{dst}:{port}"].append(ts_float)
except (FileNotFoundError, KeyError):
pass
return connections
def main():
parser = argparse.ArgumentParser(
description="Detect C2 beaconing via frequency analysis"
)
parser.add_argument("--conn-log", help="Zeek conn.log path")
parser.add_argument("--csv", help="CSV log with timestamp, src, dst columns")
parser.add_argument("--min-connections", type=int, default=10)
parser.add_argument("--max-jitter", type=float, default=15, help="Max jitter percent")
parser.add_argument("--output", "-o", help="Output JSON report")
args = parser.parse_args()
print("[*] Beaconing Detection via Frequency Analysis")
connections = {}
if args.conn_log:
connections = parse_zeek_conn_log(args.conn_log)
elif args.csv:
connections = parse_csv_log(args.csv)
print(f"[*] Unique flows: {len(connections)}")
beacons = detect_beaconing(connections, args.min_connections, args.max_jitter)
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"flows_analyzed": len(connections),
"beacons_detected": len(beacons),
"beacons": beacons[:50],
"risk_level": "CRITICAL" if beacons else "LOW",
}
print(f"[*] Beacons detected: {len(beacons)}")
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Report saved to {args.output}")
else:
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
process.py9.3 KB
#!/usr/bin/env python3
"""
Beaconing Frequency Analysis Script
Detects C2 beaconing patterns using statistical interval analysis,
jitter detection, and data size consistency scoring.
"""
import json
import csv
import argparse
import datetime
import math
from collections import defaultdict
from pathlib import Path
KNOWN_GOOD_DOMAINS = {
"microsoft.com", "windowsupdate.com", "google.com", "googleapis.com",
"gstatic.com", "amazonaws.com", "cloudflare.com", "akamai.net",
"apple.com", "icloud.com", "adobe.com", "office365.com",
"office.com", "live.com", "outlook.com", "github.com",
"slack-edge.com", "teams.microsoft.com", "symantec.com",
"crowdstrike.com", "sentinelone.com", "mcafee.com",
}
BEACON_THRESHOLDS = {
"min_connections": 20,
"max_cv": 0.25,
"min_interval": 10,
"max_interval": 86400,
"max_data_cv": 0.30,
}
def parse_logs(input_path: str) -> list[dict]:
"""Parse connection logs from JSON, CSV, or Zeek format."""
path = Path(input_path)
events = []
if path.suffix == ".json":
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
events = data if isinstance(data, list) else data.get("events", [])
elif path.suffix == ".csv":
with open(path, "r", encoding="utf-8-sig") as f:
events = [dict(row) for row in csv.DictReader(f)]
elif path.suffix == ".log":
with open(path, "r", encoding="utf-8") as f:
headers = None
for line in f:
if line.startswith("#fields"):
headers = line.strip().split("\t")[1:]
elif line.startswith("#"):
continue
elif headers:
values = line.strip().split("\t")
if len(values) == len(headers):
events.append(dict(zip(headers, values)))
return events
def normalize_event(event: dict) -> dict:
"""Normalize field names across different log formats."""
field_map = {
"timestamp": ["ts", "timestamp", "_time", "@timestamp", "Timestamp"],
"src_ip": ["id.orig_h", "src_ip", "source_ip", "LocalIP"],
"dst_ip": ["id.resp_h", "dst_ip", "dest_ip", "RemoteIP", "DestinationIp"],
"domain": ["query", "domain", "host", "RemoteUrl", "server_name", "dest"],
"bytes_sent": ["orig_bytes", "bytes_out", "SentBytes"],
"bytes_recv": ["resp_bytes", "bytes_in", "ReceivedBytes"],
"dst_port": ["id.resp_p", "dst_port", "dest_port", "RemotePort"],
}
normalized = {}
for target, sources in field_map.items():
for src in sources:
if src in event and event[src] and event[src] != "-":
normalized[target] = str(event[src])
break
if target not in normalized:
normalized[target] = ""
return normalized
def is_known_good(domain: str) -> bool:
"""Check if domain matches known-good allowlist."""
domain_lower = domain.lower()
return any(domain_lower.endswith(good) for good in KNOWN_GOOD_DOMAINS)
def calculate_entropy(text: str) -> float:
"""Calculate Shannon entropy of a string for DGA detection."""
if not text:
return 0.0
freq = defaultdict(int)
for char in text:
freq[char] += 1
length = len(text)
return -sum((count / length) * math.log2(count / length) for count in freq.values())
def analyze_beaconing(connections: list[dict]) -> list[dict]:
"""Perform statistical frequency analysis on connection pairs."""
pairs = defaultdict(list)
for conn in connections:
src = conn.get("src_ip", "")
dst = conn.get("domain", "") or conn.get("dst_ip", "")
if not src or not dst or is_known_good(dst):
continue
try:
ts = float(conn.get("timestamp", 0))
except (ValueError, TypeError):
try:
dt = datetime.datetime.fromisoformat(conn["timestamp"].replace("Z", "+00:00"))
ts = dt.timestamp()
except (ValueError, KeyError):
continue
pairs[(src, dst)].append({
"timestamp": ts,
"bytes_sent": int(conn.get("bytes_sent", 0) or 0),
"bytes_recv": int(conn.get("bytes_recv", 0) or 0),
"dst_port": conn.get("dst_port", ""),
})
findings = []
for (src, dst), conns in pairs.items():
if len(conns) < BEACON_THRESHOLDS["min_connections"]:
continue
conns.sort(key=lambda x: x["timestamp"])
intervals = [conns[i]["timestamp"] - conns[i - 1]["timestamp"]
for i in range(1, len(conns))
if conns[i]["timestamp"] - conns[i - 1]["timestamp"] > 0]
if len(intervals) < 10:
continue
avg_interval = sum(intervals) / len(intervals)
if not (BEACON_THRESHOLDS["min_interval"] <= avg_interval <= BEACON_THRESHOLDS["max_interval"]):
continue
variance = sum((x - avg_interval) ** 2 for x in intervals) / len(intervals)
stdev = math.sqrt(variance)
cv = stdev / avg_interval if avg_interval > 0 else float("inf")
if cv > BEACON_THRESHOLDS["max_cv"]:
continue
bytes_list = [c["bytes_sent"] for c in conns if c["bytes_sent"] > 0]
data_cv = 0.0
if bytes_list:
avg_bytes = sum(bytes_list) / len(bytes_list)
if avg_bytes > 0:
data_var = sum((x - avg_bytes) ** 2 for x in bytes_list) / len(bytes_list)
data_cv = math.sqrt(data_var) / avg_bytes
risk = 0
indicators = []
if cv < 0.05:
risk += 40
indicators.append(f"Very regular interval (CV={cv:.4f})")
elif cv < 0.15:
risk += 30
indicators.append(f"Regular interval (CV={cv:.4f})")
else:
risk += 20
indicators.append(f"Moderately regular interval (CV={cv:.4f})")
if data_cv < 0.10 and bytes_list:
risk += 15
indicators.append(f"Consistent payload size (data_CV={data_cv:.4f})")
if len(conns) > 500:
risk += 10
indicators.append(f"High connection count: {len(conns)}")
domain_parts = dst.split(".")
if domain_parts:
entropy = calculate_entropy(domain_parts[0])
if entropy > 3.5:
risk += 15
indicators.append(f"High domain entropy: {entropy:.2f} (possible DGA)")
risk_level = ("CRITICAL" if risk >= 70 else "HIGH" if risk >= 50
else "MEDIUM" if risk >= 30 else "LOW")
jitter_pct = (stdev / avg_interval * 100) if avg_interval > 0 else 0
findings.append({
"src_ip": src,
"destination": dst,
"connection_count": len(conns),
"avg_interval_sec": round(avg_interval, 2),
"stdev_interval": round(stdev, 2),
"cv": round(cv, 4),
"jitter_pct": round(jitter_pct, 1),
"data_size_cv": round(data_cv, 4),
"first_seen": datetime.datetime.fromtimestamp(conns[0]["timestamp"]).isoformat(),
"last_seen": datetime.datetime.fromtimestamp(conns[-1]["timestamp"]).isoformat(),
"risk_score": risk,
"risk_level": risk_level,
"indicators": indicators,
})
return sorted(findings, key=lambda x: x["risk_score"], reverse=True)
def run_hunt(input_path: str, output_dir: str) -> None:
"""Execute beaconing frequency analysis hunt."""
print(f"[*] Beaconing Frequency Analysis Hunt - {datetime.datetime.now().isoformat()}")
connections = parse_logs(input_path)
normalized = [normalize_event(c) for c in connections]
print(f"[*] Loaded {len(normalized)} connections")
findings = analyze_beaconing(normalized)
print(f"[*] Beacon detections: {len(findings)}")
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
with open(output_path / "beacon_findings.json", "w", encoding="utf-8") as f:
json.dump({
"hunt_id": f"TH-BEACON-{datetime.date.today().isoformat()}",
"total_connections": len(normalized),
"findings_count": len(findings),
"findings": findings,
}, f, indent=2)
with open(output_path / "beacon_report.md", "w", encoding="utf-8") as f:
f.write(f"# Beaconing Frequency Analysis Report\n\n")
f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"**Connections Analyzed**: {len(normalized)}\n")
f.write(f"**Findings**: {len(findings)}\n\n")
for bf in findings[:20]:
f.write(f"## [{bf['risk_level']}] {bf['src_ip']} -> {bf['destination']}\n")
f.write(f"- Interval: {bf['avg_interval_sec']}s (CV: {bf['cv']})\n")
f.write(f"- Jitter: ~{bf['jitter_pct']}%\n")
f.write(f"- Connections: {bf['connection_count']}\n")
f.write(f"- Indicators: {', '.join(bf['indicators'])}\n\n")
print(f"[+] Results written to {output_dir}")
def main():
parser = argparse.ArgumentParser(description="Beaconing Frequency Analysis")
parser.add_argument("--input", "-i", required=True, help="Path to connection logs")
parser.add_argument("--output", "-o", default="./beacon_hunt_output", help="Output directory")
args = parser.parse_args()
run_hunt(args.input, args.output)
if __name__ == "__main__":
main()