Add folder anatomy (scripts/agent.py + references/api-reference.md) for 648 cybersecurity skills

Complete skill folder anatomy across all cybersecurity skills:
- scripts/agent.py: 80-150 line Python agents using real libraries (impacket,
  boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.)
- references/api-reference.md: real API documentation with method signatures
- LICENSE: MIT license for all skill folders
This commit is contained in:
mukul975
2026-03-10 21:02:12 +01:00
parent c74d52fa30
commit 27c6414ca5
1390 changed files with 106806 additions and 0 deletions
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,99 @@
# API Reference: dd and dcfldd Disk Imaging
## dd - Standard Unix Disk Duplication
### Basic Syntax
```bash
dd if=<source> of=<destination> [options]
```
### Key Options
| Flag | Description | Example |
|------|-------------|---------|
| `if=` | Input file (source device) | `if=/dev/sdb` |
| `of=` | Output file (destination image) | `of=evidence.dd` |
| `bs=` | Block size for read/write | `bs=4096` (forensic standard) |
| `count=` | Number of blocks to copy | `count=1024` |
| `skip=` | Skip N blocks from input start | `skip=2048` |
| `conv=` | Conversion options | `conv=noerror,sync` |
| `status=` | Transfer statistics level | `status=progress` |
### conv= Values
- `noerror` - Continue on read errors (do not abort)
- `sync` - Pad input blocks with zeros on error (preserves offset alignment)
- `notrunc` - Do not truncate output file
### Output Format
```
500107862016 bytes (500 GB, 466 GiB) copied, 8132.45 s, 61.5 MB/s
976773168+0 records in
976773168+0 records out
```
## dcfldd - DoD Forensic dd
### Basic Syntax
```bash
dcfldd if=<source> of=<destination> [options]
```
### Extended Options
| Flag | Description | Example |
|------|-------------|---------|
| `hash=` | Hash algorithm(s) | `hash=sha256,md5` |
| `hashlog=` | File for hash output | `hashlog=hashes.txt` |
| `hashwindow=` | Hash every N bytes | `hashwindow=1G` |
| `hashconv=` | Hash before or after conversion | `hashconv=after` |
| `errlog=` | Error log file | `errlog=errors.log` |
| `split=` | Split output into chunks | `split=2G` |
| `splitformat=` | Suffix format for split files | `splitformat=aa` |
| `vf=` | Verification file | `vf=evidence.dd` |
| `verifylog=` | Verification result log | `verifylog=verify.log` |
### Output Format
```
Total (sha256): a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5...
1024+0 records in
1024+0 records out
```
## sha256sum - Hash Verification
### Syntax
```bash
sha256sum <file_or_device>
sha256sum -c <checksum_file>
```
### Output Format
```
a3f2b8c9d4e5f6... /dev/sdb
a3f2b8c9d4e5f6... evidence.dd
```
## blockdev - Write Protection
### Syntax
```bash
blockdev --setro <device> # Set read-only
blockdev --setrw <device> # Set read-write
blockdev --getro <device> # Check: 1=RO, 0=RW
blockdev --getsize64 <device> # Size in bytes
```
## lsblk - Block Device Enumeration
### Syntax
```bash
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,RO
lsblk -J # JSON output
lsblk -p # Full device paths
```
## hdparm - Drive Identification
### Syntax
```bash
hdparm -I <device> # Detailed drive info
hdparm -i <device> # Summary identification
```
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Forensic disk image acquisition agent using dd and dcfldd with hash verification."""
import subprocess
import hashlib
import os
import sys
import datetime
import json
def run_cmd(cmd, capture=True):
"""Execute a shell command and return output."""
result = subprocess.run(cmd, shell=True, capture_output=capture, text=True)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def list_block_devices():
"""Enumerate connected block devices."""
stdout, _, rc = run_cmd("lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,RO")
if rc == 0 and stdout:
return json.loads(stdout)
return {"blockdevices": []}
def check_write_protection(device):
"""Verify a device is set to read-only mode."""
stdout, _, rc = run_cmd(f"blockdev --getro {device}")
if rc == 0:
return stdout.strip() == "1"
return False
def enable_write_protection(device):
"""Enable software write-blocking on the target device."""
_, _, rc = run_cmd(f"blockdev --setro {device}")
if rc != 0:
print(f"[ERROR] Failed to set {device} read-only. Run as root.")
return False
if check_write_protection(device):
print(f"[OK] Write protection enabled on {device}")
return True
print(f"[ERROR] Write protection verification failed for {device}")
return False
def compute_hash(path, algorithm="sha256", block_size=65536):
"""Compute the SHA-256 or MD5 hash of a file or device."""
h = hashlib.new(algorithm)
try:
with open(path, "rb") as f:
while True:
block = f.read(block_size)
if not block:
break
h.update(block)
except PermissionError:
print(f"[ERROR] Permission denied reading {path}. Run as root.")
return None
except FileNotFoundError:
print(f"[ERROR] Path not found: {path}")
return None
return h.hexdigest()
def acquire_with_dd(source, destination, block_size=4096, log_file=None):
"""Acquire a forensic image using dd with error handling."""
cmd = (
f"dd if={source} of={destination} bs={block_size} "
f"conv=noerror,sync status=progress"
)
if log_file:
cmd += f" 2>&1 | tee {log_file}"
print(f"[*] Starting dd acquisition: {source} -> {destination}")
print(f"[*] Block size: {block_size}")
start = datetime.datetime.utcnow()
_, stderr, rc = run_cmd(cmd, capture=False)
elapsed = (datetime.datetime.utcnow() - start).total_seconds()
print(f"[*] Acquisition completed in {elapsed:.1f} seconds (rc={rc})")
return rc == 0
def acquire_with_dcfldd(source, destination, hash_alg="sha256", hash_log=None,
error_log=None, block_size=4096, split_size=None):
"""Acquire a forensic image using dcfldd with built-in hashing."""
cmd = f"dcfldd if={source} of={destination} bs={block_size} conv=noerror,sync"
cmd += f" hash={hash_alg}"
if hash_log:
cmd += f" hashlog={hash_log}"
cmd += " hashwindow=1G"
if error_log:
cmd += f" errlog={error_log}"
if split_size:
cmd += f" split={split_size} splitformat=aa"
print(f"[*] Starting dcfldd acquisition: {source} -> {destination}")
start = datetime.datetime.utcnow()
_, stderr, rc = run_cmd(cmd, capture=False)
elapsed = (datetime.datetime.utcnow() - start).total_seconds()
print(f"[*] dcfldd completed in {elapsed:.1f} seconds (rc={rc})")
return rc == 0
def verify_image(source, image_path, algorithm="sha256"):
"""Verify image integrity by comparing hashes of source and acquired image."""
print(f"[*] Computing {algorithm} hash of source: {source}")
source_hash = compute_hash(source, algorithm)
print(f" Source hash: {source_hash}")
print(f"[*] Computing {algorithm} hash of image: {image_path}")
image_hash = compute_hash(image_path, algorithm)
print(f" Image hash: {image_hash}")
if source_hash and image_hash:
match = source_hash == image_hash
status = "PASSED" if match else "FAILED"
print(f"[{'OK' if match else 'FAIL'}] Verification: {status}")
return match, source_hash, image_hash
return False, source_hash, image_hash
def generate_report(case_dir, source_device, image_path, tool_used,
source_hash, image_hash, verified, elapsed_seconds=0):
"""Generate a forensic acquisition report."""
report = {
"report_type": "Disk Image Acquisition",
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
"case_directory": case_dir,
"source_device": source_device,
"image_file": image_path,
"acquisition_tool": tool_used,
"block_size": 4096,
"source_hash_sha256": source_hash,
"image_hash_sha256": image_hash,
"hash_verified": verified,
"duration_seconds": elapsed_seconds,
}
report_path = os.path.join(case_dir, "acquisition_report.json")
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Report saved to {report_path}")
return report
if __name__ == "__main__":
print("=" * 60)
print("Forensic Disk Image Acquisition Agent")
print("Tools: dd / dcfldd with SHA-256 verification")
print("=" * 60)
# Demo: list block devices
print("\n[*] Enumerating block devices...")
devices = list_block_devices()
for dev in devices.get("blockdevices", []):
name = dev.get("name", "?")
size = dev.get("size", "?")
dtype = dev.get("type", "?")
model = dev.get("model", "N/A")
ro = "RO" if dev.get("ro") else "RW"
print(f" /dev/{name} {size} {dtype} {model} [{ro}]")
# Demo workflow (dry run)
demo_source = "/dev/sdb"
demo_case = "/cases/demo-case/images"
demo_image = os.path.join(demo_case, "evidence.dd")
print(f"\n[DEMO] Acquisition workflow for {demo_source}:")
print(f" 1. Enable write protection: blockdev --setro {demo_source}")
print(f" 2. Acquire with dcfldd: dcfldd if={demo_source} of={demo_image} "
f"hash=sha256 hashwindow=1G bs=4096 conv=noerror,sync")
print(f" 3. Verify: compare SHA-256 of {demo_source} and {demo_image}")
print(f" 4. Generate acquisition report with chain-of-custody metadata")
print("\n[*] Agent ready. Provide a source device and case directory to begin.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,41 @@
---
name: analyzing-api-gateway-access-logs
description: >
Parses API Gateway access logs (AWS API Gateway, Kong, Nginx) to detect BOLA/IDOR
attacks, rate limit bypass, credential scanning, and injection attempts. Uses pandas
for statistical analysis of request patterns and anomaly detection. Use when
investigating API abuse or building API-specific threat detection rules.
---
# Analyzing API Gateway Access Logs
## Instructions
Parse API gateway access logs to identify attack patterns including broken object
level authorization (BOLA), excessive data exposure, and injection attempts.
```python
import pandas as pd
df = pd.read_json("api_gateway_logs.json", lines=True)
# Detect BOLA: same user accessing many different resource IDs
bola = df.groupby(["user_id", "endpoint"]).agg(
unique_ids=("resource_id", "nunique")).reset_index()
suspicious = bola[bola["unique_ids"] > 50]
```
Key detection patterns:
1. BOLA/IDOR: sequential resource ID enumeration
2. Rate limit bypass via header manipulation
3. Credential scanning (401 surges from single source)
4. SQL/NoSQL injection in query parameters
5. Unusual HTTP methods (DELETE, PATCH) on read-only endpoints
## Examples
```python
# Detect 401 surges indicating credential scanning
auth_failures = df[df["status_code"] == 401]
scanner_ips = auth_failures.groupby("source_ip").size()
scanners = scanner_ips[scanner_ips > 100]
```
@@ -0,0 +1,58 @@
# API Reference: Analyzing API Gateway Access Logs
## AWS API Gateway Log Fields
```json
{
"requestId": "abc-123",
"ip": "203.0.113.50",
"httpMethod": "GET",
"resourcePath": "/api/users/{id}",
"status": 200,
"requestTime": "2025-03-15T14:00:00Z",
"responseLength": 1024
}
```
## Pandas Log Analysis
```python
import pandas as pd
df = pd.read_json("access_logs.json", lines=True)
# BOLA detection
df.groupby("user_id")["resource_id"].nunique()
# Auth failure surge
df[df["status_code"] == 401].groupby("source_ip").size()
# Request velocity
df.set_index("timestamp").resample("1min").size()
```
## OWASP API Top 10 Patterns
| Risk | Detection Pattern |
|------|-------------------|
| BOLA (API1) | User accessing > 50 unique resource IDs |
| Broken Auth (API2) | > 100 401/403 from single IP |
| Excessive Data (API3) | Response size > 10x average |
| Rate Limit (API4) | > 100 req/min from single IP |
| BFLA (API5) | DELETE/PUT on read-only endpoints |
| Injection (API8) | SQL/NoSQL patterns in params |
## Injection Regex Patterns
```python
sql = r"union\s+select|drop\s+table|'\s*or\s+'1'"
nosql = r"\$ne|\$gt|\$regex|\$where"
xss = r"<script|javascript:|onerror="
path_traversal = r"\.\./\.\./|/etc/passwd"
```
### References
- OWASP API Security Top 10: https://owasp.org/API-Security/
- AWS API Gateway logging: https://docs.aws.amazon.com/apigateway/latest/developerguide/
- pandas: https://pandas.pydata.org/docs/
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Agent for analyzing API Gateway access logs for security threats."""
import os
import re
import json
import argparse
from datetime import datetime
from collections import defaultdict
import pandas as pd
import numpy as np
def load_api_logs(log_path):
"""Load API gateway logs from JSON lines or CSV."""
if log_path.endswith(".csv"):
return pd.read_csv(log_path, parse_dates=["timestamp"])
return pd.read_json(log_path, lines=True)
def detect_bola_attacks(df, threshold=50):
"""Detect Broken Object Level Authorization (BOLA/IDOR) attacks."""
findings = []
if "resource_id" not in df.columns:
path_col = "request_path" if "request_path" in df.columns else "path"
df["resource_id"] = df[path_col].str.extract(r'/(\d+)(?:/|$|\?)')
df_with_ids = df.dropna(subset=["resource_id"])
if df_with_ids.empty:
return findings
user_col = "user_id" if "user_id" in df.columns else "source_ip"
grouped = df_with_ids.groupby([user_col]).agg(
unique_resources=("resource_id", "nunique"),
total_requests=("resource_id", "count"),
).reset_index()
bola_suspects = grouped[grouped["unique_resources"] >= threshold]
for _, row in bola_suspects.iterrows():
findings.append({
"user": row[user_col],
"unique_resources_accessed": int(row["unique_resources"]),
"total_requests": int(row["total_requests"]),
"type": "BOLA/IDOR",
"severity": "CRITICAL",
})
return findings
def detect_auth_scanning(df, threshold=100):
"""Detect credential scanning via 401/403 response surges."""
findings = []
auth_failures = df[df["status_code"].isin([401, 403])]
if auth_failures.empty:
return findings
ip_col = "source_ip" if "source_ip" in df.columns else "client_ip"
ip_failures = auth_failures.groupby(ip_col).agg(
failure_count=("status_code", "count"),
unique_endpoints=("request_path", "nunique") if "request_path" in df.columns
else ("path", "nunique"),
).reset_index()
scanners = ip_failures[ip_failures["failure_count"] >= threshold]
for _, row in scanners.iterrows():
findings.append({
"source_ip": row[ip_col],
"auth_failures": int(row["failure_count"]),
"endpoints_probed": int(row["unique_endpoints"]),
"type": "credential_scanning",
"severity": "HIGH",
})
return findings
def detect_injection_attempts(df):
"""Detect SQL/NoSQL injection attempts in request parameters."""
injection_patterns = [
r"(?:union\s+select|select\s+.*\s+from|drop\s+table|insert\s+into)",
r"(?:'\s*or\s+'1'\s*=\s*'1|'\s*or\s+1\s*=\s*1)",
r'(?:\$ne|\$gt|\$lt|\$regex|\$where)',
r'(?:<script|javascript:|onerror=|onload=)',
r'(?:\.\./\.\./|/etc/passwd|/proc/self)',
]
findings = []
path_col = "request_path" if "request_path" in df.columns else "path"
query_col = "query_string" if "query_string" in df.columns else path_col
for _, row in df.iterrows():
request_str = str(row.get(query_col, "")) + str(row.get("request_body", ""))
for pattern in injection_patterns:
if re.search(pattern, request_str, re.IGNORECASE):
findings.append({
"source_ip": row.get("source_ip", row.get("client_ip", "")),
"path": row.get(path_col, ""),
"pattern_matched": pattern,
"type": "injection_attempt",
"severity": "HIGH",
})
break
return findings[:500]
def detect_rate_limit_bypass(df, window="1min", threshold=100):
"""Detect rate limit bypass attempts."""
findings = []
ip_col = "source_ip" if "source_ip" in df.columns else "client_ip"
df_copy = df.copy()
df_copy["timestamp"] = pd.to_datetime(df_copy["timestamp"])
df_copy = df_copy.set_index("timestamp")
for ip, group in df_copy.groupby(ip_col):
resampled = group.resample(window).size()
bursts = resampled[resampled > threshold]
if len(bursts) > 0:
findings.append({
"source_ip": ip,
"max_requests_per_min": int(resampled.max()),
"burst_periods": len(bursts),
"type": "rate_limit_bypass",
"severity": "MEDIUM",
})
return sorted(findings, key=lambda x: x["max_requests_per_min"], reverse=True)[:50]
def detect_unusual_methods(df):
"""Detect unusual HTTP methods on typically read-only endpoints."""
findings = []
dangerous_methods = {"DELETE", "PUT", "PATCH"}
method_col = "method" if "method" in df.columns else "http_method"
path_col = "request_path" if "request_path" in df.columns else "path"
unusual = df[df[method_col].str.upper().isin(dangerous_methods)]
for _, row in unusual.iterrows():
findings.append({
"source_ip": row.get("source_ip", row.get("client_ip", "")),
"method": row[method_col],
"path": row[path_col],
"status_code": int(row.get("status_code", 0)),
"type": "unusual_method",
"severity": "MEDIUM",
})
return findings[:200]
def main():
parser = argparse.ArgumentParser(description="API Gateway Log Analysis Agent")
parser.add_argument("--log-file", required=True, help="API gateway log file")
parser.add_argument("--output", default="api_gateway_report.json")
parser.add_argument("--action", choices=[
"bola", "auth_scan", "injection", "rate_limit", "full_analysis"
], default="full_analysis")
args = parser.parse_args()
df = load_api_logs(args.log_file)
report = {"generated_at": datetime.utcnow().isoformat(), "total_requests": len(df),
"findings": {}}
print(f"[+] Loaded {len(df)} API requests")
if args.action in ("bola", "full_analysis"):
findings = detect_bola_attacks(df)
report["findings"]["bola"] = findings
print(f"[+] BOLA suspects: {len(findings)}")
if args.action in ("auth_scan", "full_analysis"):
findings = detect_auth_scanning(df)
report["findings"]["auth_scanning"] = findings
print(f"[+] Auth scanners: {len(findings)}")
if args.action in ("injection", "full_analysis"):
findings = detect_injection_attempts(df)
report["findings"]["injection_attempts"] = findings
print(f"[+] Injection attempts: {len(findings)}")
if args.action in ("rate_limit", "full_analysis"):
findings = detect_rate_limit_bypass(df)
report["findings"]["rate_limit_bypass"] = findings
print(f"[+] Rate limit bypasses: {len(findings)}")
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,97 @@
# API Reference: MITRE ATT&CK Navigator APT Analysis
## ATT&CK Navigator Layer Format
### Layer JSON Structure
```json
{
"name": "APT29 - TTPs",
"versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"},
"domain": "enterprise-attack",
"techniques": [
{
"techniqueID": "T1566.001",
"tactic": "initial-access",
"color": "#ff6666",
"score": 100,
"comment": "Used by APT29",
"enabled": true
}
],
"gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100}
}
```
## ATT&CK STIX Data Access
### Download Enterprise ATT&CK Bundle
```bash
curl -o enterprise-attack.json \
https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
```
### STIX Object Types
| Type | Description |
|------|-------------|
| `intrusion-set` | APT groups / threat actors |
| `attack-pattern` | Techniques and sub-techniques |
| `relationship` | Links groups to techniques (`uses`) |
| `malware` | Malware families |
| `tool` | Legitimate tools used by adversaries |
## mitreattack-python Library
### Installation
```bash
pip install mitreattack-python
```
### Query Group Techniques
```python
from mitreattack.stix20 import MitreAttackData
attack = MitreAttackData("enterprise-attack.json")
groups = attack.get_groups()
for g in groups:
techs = attack.get_techniques_used_by_group(g)
print(f"{g.name}: {len(techs)} techniques")
```
### Get Technique Details
```python
technique = attack.get_object_by_attack_id("T1566.001", "attack-pattern")
print(technique.name) # Spearphishing Attachment
print(technique.x_mitre_platforms) # ['Windows', 'macOS', 'Linux']
```
## Navigator CLI (attack-navigator)
### Export Layer to SVG
```bash
npx attack-navigator-export \
--layer layer.json \
--output output.svg \
--theme dark
```
## ATT&CK API (TAXII)
```python
from stix2 import TAXIICollectionSource, Filter
from taxii2client.v20 import Collection
collection = Collection(
"https://cti-taxii.mitre.org/stix/collections/95ecc380-afe9-11e4-9b6c-751b66dd541e/"
)
tc_source = TAXIICollectionSource(collection)
groups = tc_source.query([Filter("type", "=", "intrusion-set")])
```
## Key APT Groups Reference
| ID | Name | Known Aliases |
|----|------|--------------|
| G0016 | APT29 | Cozy Bear, The Dukes, NOBELIUM |
| G0007 | APT28 | Fancy Bear, Sofacy, Strontium |
| G0022 | APT3 | Gothic Panda, UPS |
| G0032 | Lazarus Group | HIDDEN COBRA, Zinc |
| G0074 | Dragonfly 2.0 | Energetic Bear, Berserk Bear |
| G0010 | Turla | Waterbug, Venomous Bear |
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""APT group analysis agent using MITRE ATT&CK Navigator layers.
Queries ATT&CK data, maps APT techniques to Navigator layers,
performs detection gap analysis, and generates threat-informed reports.
"""
import json
import os
import sys
import hashlib
from collections import Counter
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
ATTACK_ENTERPRISE_URL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json"
NAVIGATOR_LAYER_TEMPLATE = {
"name": "",
"versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"},
"domain": "enterprise-attack",
"description": "",
"filters": {"platforms": ["Windows", "Linux", "macOS", "Cloud"]},
"sorting": 0,
"layout": {"layout": "side", "aggregateFunction": "average", "showID": False,
"showName": True, "showAggregateScores": False, "countUnscored": False},
"hideDisabled": False,
"techniques": [],
"gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100},
"legendItems": [],
"metadata": [],
"links": [],
"showTacticRowBackground": False,
"tacticRowBackground": "#dddddd",
"selectTechniquesAcrossTactics": True,
"selectSubtechniquesWithParent": False,
"selectVisibleTechniques": False,
}
def load_attack_data(filepath=None):
"""Load ATT&CK STIX bundle from file or download."""
if filepath and os.path.exists(filepath):
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
if HAS_REQUESTS:
print("[*] Downloading ATT&CK Enterprise data...")
resp = requests.get(ATTACK_ENTERPRISE_URL, timeout=60)
resp.raise_for_status()
return resp.json()
return None
def extract_groups(bundle):
"""Extract intrusion-set (APT group) objects from STIX bundle."""
groups = {}
for obj in bundle.get("objects", []):
if obj.get("type") == "intrusion-set":
name = obj.get("name", "Unknown")
aliases = obj.get("aliases", [])
ext_refs = obj.get("external_references", [])
attack_id = ""
for ref in ext_refs:
if ref.get("source_name") == "mitre-attack":
attack_id = ref.get("external_id", "")
break
groups[obj["id"]] = {
"name": name, "id": attack_id, "aliases": aliases,
"description": obj.get("description", "")[:200],
}
return groups
def extract_techniques(bundle):
"""Extract attack-pattern (technique) objects from STIX bundle."""
techniques = {}
for obj in bundle.get("objects", []):
if obj.get("type") == "attack-pattern" and not obj.get("revoked", False):
ext_refs = obj.get("external_references", [])
attack_id = ""
for ref in ext_refs:
if ref.get("source_name") == "mitre-attack":
attack_id = ref.get("external_id", "")
break
if attack_id:
tactics = [p["phase_name"] for p in obj.get("kill_chain_phases", [])]
techniques[obj["id"]] = {
"id": attack_id, "name": obj.get("name", ""),
"tactics": tactics, "platforms": obj.get("x_mitre_platforms", []),
}
return techniques
def map_group_techniques(bundle, group_stix_id, techniques):
"""Map techniques used by a specific group via relationship objects."""
group_techniques = []
for obj in bundle.get("objects", []):
if (obj.get("type") == "relationship" and
obj.get("relationship_type") == "uses" and
obj.get("source_ref") == group_stix_id and
obj.get("target_ref", "").startswith("attack-pattern--")):
tech_id = obj["target_ref"]
if tech_id in techniques:
group_techniques.append(techniques[tech_id])
return group_techniques
def build_navigator_layer(group_name, group_techniques, color="#ff6666", score=100):
"""Build ATT&CK Navigator JSON layer for a group's techniques."""
layer = json.loads(json.dumps(NAVIGATOR_LAYER_TEMPLATE))
layer["name"] = f"{group_name} - TTPs"
layer["description"] = f"ATT&CK techniques attributed to {group_name}"
for tech in group_techniques:
entry = {
"techniqueID": tech["id"],
"tactic": tech["tactics"][0] if tech["tactics"] else "",
"color": color,
"comment": f"Used by {group_name}",
"enabled": True,
"metadata": [],
"links": [],
"showSubtechniques": False,
"score": score,
}
layer["techniques"].append(entry)
return layer
def detection_gap_analysis(group_techniques, detection_rules):
"""Compare group TTPs against existing detection rules to find gaps."""
covered = set()
for rule in detection_rules:
tech_id = rule.get("technique_id", "")
if tech_id:
covered.add(tech_id)
gaps = []
for tech in group_techniques:
if tech["id"] not in covered:
gaps.append({
"technique_id": tech["id"],
"technique_name": tech["name"],
"tactics": tech["tactics"],
"status": "NO DETECTION",
})
coverage_pct = (len(covered & {t["id"] for t in group_techniques}) /
len(group_techniques) * 100) if group_techniques else 0
return gaps, round(coverage_pct, 1)
def tactic_heatmap(group_techniques):
"""Generate tactic-level heatmap showing technique distribution."""
tactic_counts = Counter()
for tech in group_techniques:
for tactic in tech["tactics"]:
tactic_counts[tactic] += 1
return dict(tactic_counts.most_common())
def compare_groups(group_a_techs, group_b_techs):
"""Compare two groups' technique sets for overlap analysis."""
set_a = {t["id"] for t in group_a_techs}
set_b = {t["id"] for t in group_b_techs}
overlap = set_a & set_b
only_a = set_a - set_b
only_b = set_b - set_a
jaccard = len(overlap) / len(set_a | set_b) if (set_a | set_b) else 0
return {
"overlap_count": len(overlap), "overlap_ids": sorted(overlap),
"only_group_a": len(only_a), "only_group_b": len(only_b),
"jaccard_similarity": round(jaccard, 4),
}
def save_layer(layer, output_path):
"""Save Navigator layer to JSON file."""
with open(output_path, "w", encoding="utf-8") as f:
json.dump(layer, f, indent=2)
print(f"[+] Layer saved: {output_path}")
if __name__ == "__main__":
print("=" * 60)
print("APT Group Analysis Agent - MITRE ATT&CK Navigator")
print("TTP mapping, detection gap analysis, group comparison")
print("=" * 60)
group_name = sys.argv[1] if len(sys.argv) > 1 else None
attack_file = sys.argv[2] if len(sys.argv) > 2 else None
bundle = load_attack_data(attack_file)
if not bundle:
print("\n[!] Cannot load ATT&CK data. Provide STIX bundle path or install requests.")
print("[DEMO] Usage:")
print(" python agent.py APT29 enterprise-attack.json")
print(" python agent.py APT28 # downloads from GitHub")
sys.exit(1)
groups = extract_groups(bundle)
techniques = extract_techniques(bundle)
print(f"[*] Loaded {len(groups)} groups, {len(techniques)} techniques")
if not group_name:
print("\n--- Available APT Groups (sample) ---")
for gid, g in list(groups.items())[:20]:
print(f" {g['id']:8s} {g['name']:30s} aliases={g['aliases'][:3]}")
sys.exit(0)
target_group = None
for gid, g in groups.items():
if (g["name"].lower() == group_name.lower() or
g["id"].lower() == group_name.lower() or
group_name.lower() in [a.lower() for a in g["aliases"]]):
target_group = (gid, g)
break
if not target_group:
print(f"[!] Group '{group_name}' not found")
sys.exit(1)
gid, ginfo = target_group
print(f"\n[*] Group: {ginfo['name']} ({ginfo['id']})")
print(f" Aliases: {', '.join(ginfo['aliases'][:5])}")
group_techs = map_group_techniques(bundle, gid, techniques)
print(f" Techniques: {len(group_techs)}")
heatmap = tactic_heatmap(group_techs)
print("\n--- Tactic Heatmap ---")
for tactic, count in heatmap.items():
bar = "#" * count
print(f" {tactic:35s} {count:3d} {bar}")
layer = build_navigator_layer(ginfo["name"], group_techs)
out_file = f"{ginfo['name'].replace(' ', '_')}_layer.json"
save_layer(layer, out_file)
sample_rules = [{"technique_id": t["id"]} for t in group_techs[:len(group_techs)//2]]
gaps, coverage = detection_gap_analysis(group_techs, sample_rules)
print(f"\n--- Detection Gap Analysis (demo: {coverage}% coverage) ---")
for gap in gaps[:10]:
print(f" [GAP] {gap['technique_id']:12s} {gap['technique_name']}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,48 @@
---
name: analyzing-azure-activity-logs-for-threats
description: >
Queries Azure Monitor activity logs and sign-in logs via azure-monitor-query to
detect suspicious administrative operations, impossible travel, privilege escalation,
and resource modifications. Builds KQL queries for threat hunting in Azure environments.
Use when investigating suspicious Azure tenant activity or building cloud SIEM detections.
---
# Analyzing Azure Activity Logs for Threats
## Instructions
Use azure-monitor-query to execute KQL queries against Azure Log Analytics workspaces,
detecting suspicious admin operations and sign-in anomalies.
```python
from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient
from datetime import timedelta
credential = DefaultAzureCredential()
client = LogsQueryClient(credential)
response = client.query_workspace(
workspace_id="WORKSPACE_ID",
query="AzureActivity | where OperationNameValue has 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE' | take 10",
timespan=timedelta(hours=24),
)
```
Key detection queries:
1. Role assignment changes (privilege escalation)
2. Resource group and subscription modifications
3. Key vault secret access from new IPs
4. Network security group rule changes
5. Conditional access policy modifications
## Examples
```python
# Detect new Global Admin role assignments
query = '''
AuditLogs
| where OperationName == "Add member to role"
| where TargetResources[0].modifiedProperties[0].newValue has "Global Administrator"
'''
```
@@ -0,0 +1,54 @@
# API Reference: Analyzing Azure Activity Logs for Threats
## azure-monitor-query
```python
from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
from datetime import timedelta
credential = DefaultAzureCredential()
client = LogsQueryClient(credential)
response = client.query_workspace(
workspace_id="WORKSPACE_ID",
query="AzureActivity | take 10",
timespan=timedelta(hours=24),
)
if response.status == LogsQueryStatus.SUCCESS:
for table in response.tables:
columns = [col.name for col in table.columns]
for row in table.rows:
print(dict(zip(columns, row)))
```
## Key Azure Log Tables
| Table | Content |
|-------|---------|
| `AzureActivity` | Control plane operations (ARM) |
| `SigninLogs` | Azure AD sign-in events |
| `AuditLogs` | Azure AD audit trail |
| `AzureDiagnostics` | Resource diagnostics (Key Vault, NSG) |
| `SecurityAlert` | Defender for Cloud alerts |
## Threat Detection KQL Patterns
```kql
// Privilege escalation
AzureActivity | where OperationNameValue has "ROLEASSIGNMENTS/WRITE"
// Impossible travel
SigninLogs | where ResultType == 0
| extend Distance = geo_distance_2points(...)
// Mass deletion
AzureActivity | where OperationNameValue endswith "/DELETE"
| summarize count() by Caller, bin(TimeGenerated, 1h)
```
### References
- azure-monitor-query: https://pypi.org/project/azure-monitor-query/
- KQL reference: https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/
- Azure Activity Log schema: https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log-schema
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""Agent for analyzing Azure activity logs for threat detection."""
import os
import json
import argparse
from datetime import datetime, timedelta
from azure.identity import DefaultAzureCredential, ClientSecretCredential
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
def get_credential(tenant_id=None, client_id=None, client_secret=None):
"""Get Azure credential."""
if client_id and client_secret and tenant_id:
return ClientSecretCredential(tenant_id, client_id, client_secret)
return DefaultAzureCredential()
def run_kql(credential, workspace_id, query, hours=24):
"""Execute KQL query against Log Analytics workspace."""
client = LogsQueryClient(credential)
response = client.query_workspace(
workspace_id, query, timespan=timedelta(hours=hours)
)
rows = []
if response.status == LogsQueryStatus.SUCCESS:
for table in response.tables:
columns = [col.name for col in table.columns]
for row in table.rows:
rows.append(dict(zip(columns, row)))
return rows
def detect_privilege_escalation(credential, workspace_id):
"""Detect role assignment changes indicating privilege escalation."""
query = """
AzureActivity
| where OperationNameValue has_any (
"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE",
"MICROSOFT.AUTHORIZATION/ROLEDEFINITIONS/WRITE"
)
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, CallerIpAddress,
OperationNameValue, ResourceGroup, Properties_d
| order by TimeGenerated desc
"""
return run_kql(credential, workspace_id, query)
def detect_nsg_changes(credential, workspace_id):
"""Detect Network Security Group rule modifications."""
query = """
AzureActivity
| where OperationNameValue has_any (
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE"
)
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, CallerIpAddress,
OperationNameValue, ResourceGroup
| order by TimeGenerated desc
"""
return run_kql(credential, workspace_id, query)
def detect_keyvault_access(credential, workspace_id):
"""Detect Key Vault secret access from unusual sources."""
query = """
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName in ("SecretGet", "SecretList", "SecretSet")
| summarize AccessCount = count(), DistinctIPs = dcount(CallerIPAddress),
IPList = make_set(CallerIPAddress, 10)
by identity_claim_upn_s, OperationName, Resource
| where DistinctIPs > 2 or AccessCount > 50
| order by AccessCount desc
"""
return run_kql(credential, workspace_id, query)
def detect_impossible_travel(credential, workspace_id):
"""Detect sign-ins from geographically distant locations in short time."""
query = """
SigninLogs
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, IPAddress,
Lat = toreal(LocationDetails.geoCoordinates.latitude),
Lon = toreal(LocationDetails.geoCoordinates.longitude)
| sort by UserPrincipalName asc, TimeGenerated asc
| extend PrevLat = prev(Lat), PrevLon = prev(Lon),
PrevTime = prev(TimeGenerated), PrevUser = prev(UserPrincipalName)
| where UserPrincipalName == PrevUser
| extend TimeDiffMin = datetime_diff('minute', TimeGenerated, PrevTime)
| where TimeDiffMin < 60 and TimeDiffMin > 0
| extend DistKm = geo_distance_2points(Lon, Lat, PrevLon, PrevLat) / 1000
| where DistKm > 500
| project TimeGenerated, UserPrincipalName, IPAddress, DistKm, TimeDiffMin
"""
return run_kql(credential, workspace_id, query)
def detect_resource_deletion(credential, workspace_id):
"""Detect mass resource deletion events."""
query = """
AzureActivity
| where OperationNameValue endswith "/DELETE"
| where ActivityStatusValue == "Success"
| summarize DeleteCount = count(), Resources = make_set(Resource, 20)
by Caller, bin(TimeGenerated, 1h)
| where DeleteCount > 10
| order by DeleteCount desc
"""
return run_kql(credential, workspace_id, query)
def detect_conditional_access_changes(credential, workspace_id):
"""Detect modifications to Conditional Access policies."""
query = """
AuditLogs
| where OperationName has_any (
"Update conditional access policy",
"Delete conditional access policy"
)
| project TimeGenerated, InitiatedBy, OperationName,
TargetResources, Result
| order by TimeGenerated desc
"""
return run_kql(credential, workspace_id, query)
def main():
parser = argparse.ArgumentParser(description="Azure Activity Log Threat Detection Agent")
parser.add_argument("--workspace-id", default=os.getenv("AZURE_WORKSPACE_ID"))
parser.add_argument("--tenant-id", default=os.getenv("AZURE_TENANT_ID"))
parser.add_argument("--client-id", default=os.getenv("AZURE_CLIENT_ID"))
parser.add_argument("--client-secret", default=os.getenv("AZURE_CLIENT_SECRET"))
parser.add_argument("--output", default="azure_threat_report.json")
parser.add_argument("--action", choices=[
"privesc", "nsg", "keyvault", "travel", "deletion", "full_hunt"
], default="full_hunt")
args = parser.parse_args()
cred = get_credential(args.tenant_id, args.client_id, args.client_secret)
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
if args.action in ("privesc", "full_hunt"):
results = detect_privilege_escalation(cred, args.workspace_id)
report["findings"]["privilege_escalation"] = results
print(f"[+] Privilege escalation events: {len(results)}")
if args.action in ("nsg", "full_hunt"):
results = detect_nsg_changes(cred, args.workspace_id)
report["findings"]["nsg_changes"] = results
print(f"[+] NSG changes: {len(results)}")
if args.action in ("keyvault", "full_hunt"):
results = detect_keyvault_access(cred, args.workspace_id)
report["findings"]["keyvault_anomalies"] = results
print(f"[+] Key Vault anomalies: {len(results)}")
if args.action in ("travel", "full_hunt"):
results = detect_impossible_travel(cred, args.workspace_id)
report["findings"]["impossible_travel"] = results
print(f"[+] Impossible travel: {len(results)}")
if args.action in ("deletion", "full_hunt"):
results = detect_resource_deletion(cred, args.workspace_id)
report["findings"]["mass_deletion"] = results
print(f"[+] Mass deletion events: {len(results)}")
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,97 @@
# API Reference: Bootkit and Rootkit Analysis Tools
## dd - Boot Sector Extraction
### Syntax
```bash
dd if=/dev/sda of=mbr.bin bs=512 count=1 # MBR
dd if=/dev/sda of=first_track.bin bs=512 count=63 # First track
dd if=/dev/sda1 of=vbr.bin bs=512 count=1 # VBR
```
## ndisasm - 16-bit Disassembly
### Syntax
```bash
ndisasm -b16 mbr.bin > mbr_disasm.txt
ndisasm -b16 -o 0x7C00 mbr.bin # Set origin to MBR load address
```
### Key Flags
| Flag | Description |
|------|-------------|
| `-b16` | 16-bit real-mode disassembly |
| `-b32` | 32-bit protected-mode |
| `-o` | Origin address offset |
## UEFITool - Firmware Analysis
### CLI Syntax
```bash
UEFIExtract firmware.rom all # Extract all modules
UEFIExtract firmware.rom <GUID> body # Extract specific module body
```
### Output
Extracts firmware volumes into a directory tree with each DXE driver, PEI module, and option ROM as separate files identified by GUID.
## chipsec - Hardware Security Assessment
### Syntax
```bash
python chipsec_main.py -m common.secureboot.variables # Check Secure Boot
python chipsec_main.py -m common.bios_wp # SPI write protection
python chipsec_main.py -m common.spi_lock # SPI lock status
python chipsec_util.py spi dump firmware.rom # Dump SPI flash
```
### Key Modules
| Module | Purpose |
|--------|---------|
| `common.secureboot.variables` | Verify Secure Boot configuration |
| `common.bios_wp` | Check BIOS write protection |
| `common.spi_lock` | Verify SPI flash lock bits |
| `common.smm` | SMM protection verification |
## Volatility 3 - Rootkit Detection Plugins
### Syntax
```bash
vol3 -f memory.dmp <plugin>
```
### Rootkit Detection Plugins
| Plugin | Purpose |
|--------|---------|
| `windows.ssdt` | System Service Descriptor Table hooks |
| `windows.callbacks` | Kernel callback registrations |
| `windows.driverscan` | Scan for driver objects |
| `windows.modules` | List loaded kernel modules |
| `windows.psscan` | Pool-tag scan for processes (finds hidden) |
| `windows.pslist` | Active process list (DKOM-affected) |
| `windows.idt` | Interrupt Descriptor Table hooks |
### Output Format
```
Offset Order Module Section Owner
------- ----- ------ ------- -----
0x... 0 ntoskrnl.exe .text ntoskrnl.exe
0x... 73 UNKNOWN - rootkit.sys ← suspicious
```
## flashrom - SPI Flash Dumping
### Syntax
```bash
flashrom -p internal -r firmware.rom # Read/dump
flashrom -p internal -w clean.rom # Write/reflash
flashrom -p internal --verify clean.rom # Verify flash contents
```
## YARA - Firmware Pattern Scanning
### Syntax
```bash
yara -r uefi_malware.yar firmware.rom
yara -s -r rules.yar firmware.rom # Show matching strings
```
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""Bootkit and rootkit analysis agent for MBR/VBR/UEFI inspection and rootkit detection."""
import struct
import hashlib
import os
import sys
import subprocess
import math
from collections import Counter
def read_mbr(disk_path_or_file):
"""Read and parse the first 512 bytes (MBR) from a disk image or device."""
with open(disk_path_or_file, "rb") as f:
mbr = f.read(512)
return mbr
def validate_mbr_signature(mbr_data):
"""Check the MBR boot signature at bytes 510-511 (should be 0x55AA)."""
sig = mbr_data[510:512]
valid = sig == b"\x55\xAA"
return valid, sig.hex()
def parse_partition_table(mbr_data):
"""Parse the four 16-byte partition table entries starting at offset 446."""
partitions = []
for i in range(4):
offset = 446 + (i * 16)
entry = mbr_data[offset:offset + 16]
if entry == b"\x00" * 16:
continue
boot_flag = entry[0]
part_type = entry[4]
start_lba = struct.unpack_from("<I", entry, 8)[0]
size_lba = struct.unpack_from("<I", entry, 12)[0]
partitions.append({
"index": i + 1,
"active": boot_flag == 0x80,
"type_id": f"0x{part_type:02X}",
"start_lba": start_lba,
"size_sectors": size_lba,
"size_mb": round(size_lba * 512 / (1024 * 1024), 1),
})
return partitions
BOOTKIT_SIGNATURES = {
b"\xE8\x00\x00\x5E\x81\xEE": "TDL4/Alureon bootkit",
b"\xFA\x33\xC0\x8E\xD0\xBC\x00\x7C\x8B\xF4\x50\x07": "Standard Windows MBR (clean)",
b"\xEB\x5A\x90\x4E\x54\x46\x53": "Standard NTFS VBR (clean)",
b"\xEB\x52\x90\x4E\x54\x46\x53": "NTFS VBR variant (clean)",
b"\x33\xC0\x8E\xD0\xBC\x00\x7C": "Windows 10 MBR (clean)",
}
def scan_bootkit_signatures(data):
"""Scan boot sector data against known bootkit signatures."""
matches = []
for sig, name in BOOTKIT_SIGNATURES.items():
if sig in data:
offset = data.find(sig)
matches.append({"signature": name, "offset": offset, "clean": "clean" in name})
return matches
def calculate_entropy(data):
"""Calculate Shannon entropy of binary data."""
if not data:
return 0.0
counter = Counter(data)
length = len(data)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
def read_first_track(disk_path, num_sectors=63):
"""Read the first track (typically 63 sectors) for extended bootkit code."""
with open(disk_path, "rb") as f:
data = f.read(num_sectors * 512)
return data
def analyze_boot_code(mbr_data):
"""Analyze MBR bootstrap code (bytes 0-445) for suspicious patterns."""
boot_code = mbr_data[:446]
entropy = calculate_entropy(boot_code)
sha256 = hashlib.sha256(boot_code).hexdigest()
suspicious_patterns = []
# Check for INT 13h hooking (common bootkit technique)
if b"\xCD\x13" in boot_code:
count = boot_code.count(b"\xCD\x13")
suspicious_patterns.append(f"INT 13h calls: {count}")
# Check for far jumps to unusual addresses
if b"\xEA" in boot_code:
suspicious_patterns.append("Far JMP instruction found")
# Check for self-modifying code patterns
if b"\xF3\xA4" in boot_code or b"\xF3\xA5" in boot_code:
suspicious_patterns.append("REP MOVSB/MOVSW (memory copy, possible code relocation)")
return {
"entropy": entropy,
"sha256": sha256,
"high_entropy": entropy > 6.5,
"suspicious_patterns": suspicious_patterns,
}
def run_volatility_rootkit_scan(memory_dump, plugin):
"""Run a Volatility 3 plugin for rootkit detection via subprocess."""
cmd = f"vol3 -f {memory_dump} {plugin}"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout, result.stderr, result.returncode
def detect_kernel_rootkit(memory_dump):
"""Run multiple Volatility plugins to detect kernel-level rootkit artifacts."""
plugins = [
"windows.ssdt",
"windows.callbacks",
"windows.driverscan",
"windows.modules",
"windows.psscan",
"windows.pslist",
]
results = {}
for plugin in plugins:
stdout, stderr, rc = run_volatility_rootkit_scan(memory_dump, plugin)
results[plugin] = {"output": stdout, "error": stderr, "return_code": rc}
return results
def compare_process_lists(pslist_output, psscan_output):
"""Compare pslist and psscan output to find hidden processes (DKOM)."""
pslist_pids = set()
psscan_pids = set()
for line in pslist_output.splitlines():
parts = line.split()
if len(parts) >= 2 and parts[1].isdigit():
pslist_pids.add(int(parts[1]))
for line in psscan_output.splitlines():
parts = line.split()
if len(parts) >= 2 and parts[1].isdigit():
psscan_pids.add(int(parts[1]))
hidden = psscan_pids - pslist_pids
return hidden
if __name__ == "__main__":
print("=" * 60)
print("Bootkit & Rootkit Analysis Agent")
print("MBR/VBR inspection, UEFI firmware analysis, rootkit detection")
print("=" * 60)
# Demo with a sample MBR file if available
demo_mbr = "mbr.bin"
if len(sys.argv) > 1:
demo_mbr = sys.argv[1]
if os.path.exists(demo_mbr):
print(f"\n[*] Analyzing: {demo_mbr}")
mbr = read_mbr(demo_mbr)
valid, sig_hex = validate_mbr_signature(mbr)
print(f"[*] MBR Signature: 0x{sig_hex.upper()} ({'Valid' if valid else 'INVALID'})")
partitions = parse_partition_table(mbr)
print(f"[*] Partition entries: {len(partitions)}")
for p in partitions:
active = "Active" if p["active"] else "Inactive"
print(f" Part {p['index']}: Type={p['type_id']} {active} "
f"Start=LBA {p['start_lba']} Size={p['size_mb']} MB")
sigs = scan_bootkit_signatures(mbr)
for s in sigs:
tag = "[*]" if s["clean"] else "[!]"
print(f"{tag} Signature match: {s['signature']} at offset {s['offset']}")
analysis = analyze_boot_code(mbr)
print(f"[*] Boot code entropy: {analysis['entropy']}"
f" ({'HIGH - possible encryption' if analysis['high_entropy'] else 'Normal'})")
print(f"[*] Boot code SHA-256: {analysis['sha256']}")
for pat in analysis["suspicious_patterns"]:
print(f"[!] {pat}")
else:
print(f"\n[DEMO] No MBR file provided. Usage: {sys.argv[0]} <mbr.bin | /dev/sda>")
print("[DEMO] Provide a 512-byte MBR dump or disk device for analysis.")
print("\n[*] Supported analysis:")
print(" - MBR/VBR signature validation and bootkit detection")
print(" - Partition table parsing and anomaly detection")
print(" - Boot code entropy and pattern analysis")
print(" - Volatility-based kernel rootkit detection (SSDT, callbacks, DKOM)")
print(" - UEFI firmware module inspection via chipsec subprocess")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,92 @@
# API Reference: Browser Forensics with Hindsight
## Hindsight CLI
### Syntax
```bash
hindsight.py -i <profile_path> # Analyze Chrome profile
hindsight.py -i <path> -o <output_dir> # Save results
hindsight.py -i <path> -f xlsx # Export as Excel
hindsight.py -i <path> -f sqlite # Export as SQLite
hindsight.py -i <path> -b <browser_type> # Specify browser type
```
### Browser Types
| Flag | Browser |
|------|---------|
| `Chrome` | Google Chrome |
| `Edge` | Microsoft Edge (Chromium) |
| `Brave` | Brave Browser |
| `Opera` | Opera (Chromium) |
### Output Artifacts
| Table | Description |
|-------|-------------|
| `urls` | Browsing history with visit counts |
| `downloads` | File downloads with source URLs |
| `cookies` | Cookie values, domains, expiry |
| `autofill` | Form autofill entries |
| `bookmarks` | Saved bookmarks |
| `preferences` | Browser configuration |
| `local_storage` | Site local storage data |
| `login_data` | Saved credential metadata |
| `extensions` | Installed extensions with permissions |
## Chrome SQLite Databases
### History Database
```sql
-- Browsing history
SELECT u.url, u.title, v.visit_time, v.transition
FROM visits v JOIN urls u ON v.url = u.id
ORDER BY v.visit_time DESC;
-- Downloads
SELECT target_path, tab_url, total_bytes, start_time, danger_type, mime_type
FROM downloads ORDER BY start_time DESC;
```
### Cookies Database
```sql
SELECT host_key, name, value, creation_utc, expires_utc, is_secure, is_httponly
FROM cookies ORDER BY creation_utc DESC;
```
### Web Data Database (Autofill)
```sql
SELECT name, value, count, date_created, date_last_used
FROM autofill ORDER BY date_last_used DESC;
```
## Chrome Timestamp Conversion
### Format
Microseconds since January 1, 1601 (Windows FILETIME base)
### Python Conversion
```python
import datetime
def chrome_to_datetime(chrome_time):
epoch = datetime.datetime(1601, 1, 1)
return epoch + datetime.timedelta(microseconds=chrome_time)
```
## Browser Profile Paths
| OS | Browser | Default Path |
|----|---------|-------------|
| Windows | Chrome | `%LOCALAPPDATA%\Google\Chrome\User Data\Default` |
| Windows | Edge | `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default` |
| Linux | Chrome | `~/.config/google-chrome/Default` |
| macOS | Chrome | `~/Library/Application Support/Google/Chrome/Default` |
## Transition Types (visit_transition & 0xFF)
| Value | Type | Description |
|-------|------|-------------|
| 0 | LINK | Clicked a link |
| 1 | TYPED | Typed URL in address bar |
| 2 | AUTO_BOOKMARK | Via bookmark |
| 3 | AUTO_SUBFRAME | Subframe navigation |
| 5 | GENERATED | Generated (e.g., search) |
| 7 | FORM_SUBMIT | Form submission |
| 8 | RELOAD | Page reload |
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Browser forensics analysis agent using Hindsight concepts.
Parses Chromium-based browser artifacts (Chrome, Edge, Brave) including
history, downloads, cookies, autofill, and extensions from SQLite databases.
"""
import os
import sys
import json
import sqlite3
import datetime
import hashlib
from collections import defaultdict
def chrome_time_to_datetime(chrome_time):
"""Convert Chrome timestamp (microseconds since 1601-01-01) to datetime."""
if not chrome_time or chrome_time == 0:
return None
try:
epoch = datetime.datetime(1601, 1, 1)
delta = datetime.timedelta(microseconds=chrome_time)
return (epoch + delta).isoformat() + "Z"
except (OverflowError, OSError):
return None
def find_browser_profiles(base_path=None):
"""Locate Chromium-based browser profile directories."""
if base_path and os.path.isdir(base_path):
return [base_path]
profiles = []
home = os.path.expanduser("~")
candidates = [
os.path.join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default"),
os.path.join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default"),
os.path.join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default"),
os.path.join(home, ".config", "google-chrome", "Default"),
os.path.join(home, ".config", "chromium", "Default"),
os.path.join(home, ".config", "microsoft-edge", "Default"),
]
for path in candidates:
if os.path.isdir(path):
profiles.append(path)
return profiles
def parse_history(profile_path):
"""Parse browsing history from History SQLite database."""
db_path = os.path.join(profile_path, "History")
if not os.path.exists(db_path):
return []
entries = []
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
cursor = conn.cursor()
cursor.execute("""
SELECT u.url, u.title, v.visit_time, v.transition, u.visit_count
FROM visits v JOIN urls u ON v.url = u.id
ORDER BY v.visit_time DESC LIMIT 5000
""")
for url, title, visit_time, transition, count in cursor.fetchall():
entries.append({
"url": url, "title": title or "",
"visit_time": chrome_time_to_datetime(visit_time),
"transition": transition & 0xFF,
"visit_count": count,
})
conn.close()
except sqlite3.Error as e:
entries.append({"error": str(e)})
return entries
def parse_downloads(profile_path):
"""Parse download history from History database."""
db_path = os.path.join(profile_path, "History")
if not os.path.exists(db_path):
return []
downloads = []
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
cursor = conn.cursor()
cursor.execute("""
SELECT target_path, tab_url, total_bytes, start_time, end_time,
danger_type, interrupt_reason, mime_type
FROM downloads ORDER BY start_time DESC LIMIT 1000
""")
for row in cursor.fetchall():
downloads.append({
"target_path": row[0], "source_url": row[1],
"total_bytes": row[2],
"start_time": chrome_time_to_datetime(row[3]),
"end_time": chrome_time_to_datetime(row[4]),
"danger_type": row[5], "interrupt_reason": row[6],
"mime_type": row[7],
})
conn.close()
except sqlite3.Error as e:
downloads.append({"error": str(e)})
return downloads
def parse_cookies(profile_path):
"""Parse cookies from Cookies database."""
db_path = os.path.join(profile_path, "Cookies")
if not os.path.exists(db_path):
db_path = os.path.join(profile_path, "Network", "Cookies")
if not os.path.exists(db_path):
return []
cookies = []
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
cursor = conn.cursor()
cursor.execute("""
SELECT host_key, name, path, creation_utc, expires_utc,
is_secure, is_httponly, samesite
FROM cookies ORDER BY creation_utc DESC LIMIT 2000
""")
for row in cursor.fetchall():
cookies.append({
"host": row[0], "name": row[1], "path": row[2],
"created": chrome_time_to_datetime(row[3]),
"expires": chrome_time_to_datetime(row[4]),
"secure": bool(row[5]), "httponly": bool(row[6]),
"samesite": row[7],
})
conn.close()
except sqlite3.Error as e:
cookies.append({"error": str(e)})
return cookies
def parse_autofill(profile_path):
"""Parse autofill data from Web Data database."""
db_path = os.path.join(profile_path, "Web Data")
if not os.path.exists(db_path):
return []
entries = []
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
cursor = conn.cursor()
cursor.execute("""
SELECT name, value, count, date_created, date_last_used
FROM autofill ORDER BY date_last_used DESC LIMIT 500
""")
for row in cursor.fetchall():
entries.append({
"field_name": row[0], "value": row[1][:50] + "..." if len(row[1]) > 50 else row[1],
"usage_count": row[2],
"created": chrome_time_to_datetime(row[3] * 1000000 if row[3] else 0),
"last_used": chrome_time_to_datetime(row[4] * 1000000 if row[4] else 0),
})
conn.close()
except sqlite3.Error as e:
entries.append({"error": str(e)})
return entries
def parse_extensions(profile_path):
"""Parse installed browser extensions."""
ext_dir = os.path.join(profile_path, "Extensions")
extensions = []
if not os.path.isdir(ext_dir):
return extensions
for ext_id in os.listdir(ext_dir):
ext_path = os.path.join(ext_dir, ext_id)
if os.path.isdir(ext_path):
versions = sorted(os.listdir(ext_path))
manifest_path = os.path.join(ext_path, versions[-1], "manifest.json") if versions else None
name = ext_id
if manifest_path and os.path.exists(manifest_path):
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = json.load(f)
name = manifest.get("name", ext_id)
extensions.append({
"id": ext_id, "name": name,
"version": manifest.get("version", "?"),
"permissions": manifest.get("permissions", [])[:10],
})
except (json.JSONDecodeError, IOError):
extensions.append({"id": ext_id, "name": name, "version": "unknown"})
return extensions
def detect_suspicious_activity(history, downloads):
"""Flag suspicious browsing and download patterns."""
findings = []
suspicious_domains = ["pastebin.com", "ngrok.io", "raw.githubusercontent.com",
"transfer.sh", "file.io", "temp.sh", "anonfiles.com"]
for entry in history:
url = entry.get("url", "").lower()
for domain in suspicious_domains:
if domain in url:
findings.append({
"type": "suspicious_url", "url": entry["url"],
"domain": domain, "time": entry.get("visit_time"),
})
dangerous_mimes = ["application/x-msdownload", "application/x-msdos-program",
"application/x-executable", "application/vnd.ms-excel.sheet.macroEnabled"]
for dl in downloads:
if dl.get("danger_type", 0) > 0:
findings.append({
"type": "dangerous_download", "path": dl.get("target_path"),
"source": dl.get("source_url"), "danger_type": dl.get("danger_type"),
})
if dl.get("mime_type", "") in dangerous_mimes:
findings.append({
"type": "suspicious_mime", "mime": dl.get("mime_type"),
"path": dl.get("target_path"),
})
return findings
if __name__ == "__main__":
print("=" * 60)
print("Browser Forensics Analysis Agent")
print("Chromium history, downloads, cookies, extensions")
print("=" * 60)
target = sys.argv[1] if len(sys.argv) > 1 else None
profiles = find_browser_profiles(target)
if not profiles:
print("\n[!] No browser profiles found.")
print("[DEMO] Usage: python agent.py <profile_path>")
print(" e.g. python agent.py ~/AppData/Local/Google/Chrome/User\\ Data/Default")
sys.exit(0)
for profile in profiles:
print(f"\n[*] Profile: {profile}")
history = parse_history(profile)
print(f" History entries: {len(history)}")
for h in history[:5]:
print(f" {h.get('visit_time', '?')} | {h.get('title', '')[:50]} | {h.get('url', '')[:60]}")
downloads = parse_downloads(profile)
print(f" Downloads: {len(downloads)}")
for d in downloads[:5]:
print(f" {d.get('start_time', '?')} | {d.get('mime_type', '?')} | {os.path.basename(d.get('target_path', ''))}")
cookies = parse_cookies(profile)
print(f" Cookies: {len(cookies)}")
extensions = parse_extensions(profile)
print(f" Extensions: {len(extensions)}")
for ext in extensions[:5]:
print(f" {ext.get('name', '?')} v{ext.get('version', '?')} [{ext.get('id', '')[:20]}]")
findings = detect_suspicious_activity(history, downloads)
print(f"\n --- Suspicious Activity: {len(findings)} findings ---")
for f in findings[:10]:
print(f" [{f['type']}] {f.get('url', f.get('path', ''))}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,110 @@
# API Reference: Campaign Attribution Evidence Analysis
## Diamond Model of Intrusion Analysis
### Four Core Features
| Feature | Description | Attribution Value |
|---------|-------------|-------------------|
| Adversary | Threat actor identity | Direct attribution |
| Capability | Malware, exploits, tools | Indirect - shared tooling |
| Infrastructure | C2, domains, IPs | Strong - operational overlap |
| Victim | Targets, sectors, regions | Contextual - targeting pattern |
### Pivot Analysis
```
Adversary ←→ Capability ←→ Infrastructure ←→ Victim
↕ ↕ ↕ ↕
(HUMINT) (Malware DB) (WHOIS/DNS) (Victimology)
```
## Analysis of Competing Hypotheses (ACH)
### Matrix Format
```
Evidence \ Hypothesis | APT28 | APT29 | Lazarus | Unknown
-----------------------------------------------------------------
Infrastructure overlap | ++ | - | - | N
TTP consistency | ++ | ++ | - | N
Malware similarity | + | - | - | N
Timing (UTC+3) | ++ | ++ | - | N
Language (Russian) | ++ | ++ | - | N
```
### Scoring
| Symbol | Meaning | Weight |
|--------|---------|--------|
| `++` | Strongly consistent | +2 |
| `+` | Consistent | +1 |
| `N` | Neutral | 0 |
| `-` | Inconsistent | -1 |
| `--` | Strongly inconsistent | -2 |
## MITRE ATT&CK Group Queries
### Python (mitreattack-python)
```python
from mitreattack.stix20 import MitreAttackData
attack = MitreAttackData("enterprise-attack.json")
group = attack.get_group_by_alias("APT29")
techniques = attack.get_techniques_used_by_group(group.id)
```
### STIX2 Relationship Query
```python
from stix2 import Filter
relationships = src.query([
Filter("type", "=", "relationship"),
Filter("source_ref", "=", group_id),
Filter("relationship_type", "=", "uses"),
])
```
## Infrastructure Overlap Tools
### PassiveTotal / RiskIQ
```bash
# WHOIS history
curl -u user:key "https://api.passivetotal.org/v2/whois?query=domain.com"
# Passive DNS
curl -u user:key "https://api.passivetotal.org/v2/dns/passive?query=1.2.3.4"
```
### VirusTotal Relations
```bash
curl -H "x-apikey: KEY" \
"https://www.virustotal.com/api/v3/domains/example.com/communicating_files"
```
## Confidence Assessment Framework
| Level | Score Range | Criteria |
|-------|------------|---------|
| HIGH | 0.8-1.0 | Multiple independent evidence types converge |
| MEDIUM | 0.5-0.8 | Significant evidence with some gaps |
| LOW | 0.2-0.5 | Limited evidence, alternative hypotheses remain |
| NEGLIGIBLE | 0.0-0.2 | Insufficient evidence for attribution |
## STIX Attribution Objects
### Campaign Object
```json
{
"type": "campaign",
"name": "Operation DarkShadow",
"first_seen": "2024-01-15T00:00:00Z",
"last_seen": "2024-03-20T00:00:00Z",
"objective": "Espionage targeting defense sector"
}
```
### Attribution Relationship
```json
{
"type": "relationship",
"relationship_type": "attributed-to",
"source_ref": "campaign--abc123",
"target_ref": "intrusion-set--def456",
"confidence": 75
}
```
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Campaign attribution analysis agent using Diamond Model and ACH methodology.
Evaluates attribution evidence including infrastructure overlaps, TTP consistency,
malware code similarity, timing patterns, and language artifacts.
"""
import json
import os
import sys
import hashlib
import re
from collections import defaultdict
from datetime import datetime
DIAMOND_DIMENSIONS = {
"adversary": "Threat actor identity, group attribution",
"capability": "Malware, exploits, tools used",
"infrastructure": "C2 servers, domains, IP addresses",
"victim": "Targeted sectors, regions, organizations",
}
EVIDENCE_WEIGHTS = {
"infrastructure_overlap": 0.25,
"ttp_consistency": 0.30,
"malware_code_similarity": 0.25,
"timing_pattern": 0.10,
"language_artifact": 0.10,
}
CONFIDENCE_LEVELS = {
(0.8, 1.0): "HIGH - Strong attribution confidence",
(0.5, 0.8): "MEDIUM - Moderate attribution, further analysis recommended",
(0.2, 0.5): "LOW - Weak attribution, insufficient evidence",
(0.0, 0.2): "NEGLIGIBLE - No meaningful attribution possible",
}
def diamond_model_analysis(adversary=None, capability=None, infrastructure=None, victim=None):
"""Structure evidence using the Diamond Model of Intrusion Analysis."""
model = {
"adversary": {
"identified": adversary is not None,
"details": adversary or "Unknown",
},
"capability": {
"tools": capability.get("tools", []) if capability else [],
"exploits": capability.get("exploits", []) if capability else [],
"malware": capability.get("malware", []) if capability else [],
},
"infrastructure": {
"c2_servers": infrastructure.get("c2", []) if infrastructure else [],
"domains": infrastructure.get("domains", []) if infrastructure else [],
"ip_addresses": infrastructure.get("ips", []) if infrastructure else [],
},
"victim": {
"sectors": victim.get("sectors", []) if victim else [],
"regions": victim.get("regions", []) if victim else [],
},
"pivot_opportunities": [],
}
if infrastructure and infrastructure.get("c2"):
model["pivot_opportunities"].append("Pivot from C2 infrastructure to related campaigns")
if capability and capability.get("malware"):
model["pivot_opportunities"].append("Pivot from malware samples to shared infrastructure")
return model
def evaluate_infrastructure_overlap(campaign_infra, known_actor_infra):
"""Score infrastructure overlap between campaign and known actor."""
campaign_set = set(campaign_infra)
known_set = set(known_actor_infra)
if not campaign_set or not known_set:
return 0.0, []
overlap = campaign_set & known_set
score = len(overlap) / max(len(campaign_set), len(known_set))
return round(score, 4), sorted(overlap)
def evaluate_ttp_consistency(campaign_ttps, actor_ttps):
"""Score TTP consistency using MITRE ATT&CK technique overlap."""
campaign_set = set(campaign_ttps)
actor_set = set(actor_ttps)
if not campaign_set or not actor_set:
return 0.0, []
overlap = campaign_set & actor_set
jaccard = len(overlap) / len(campaign_set | actor_set)
return round(jaccard, 4), sorted(overlap)
def evaluate_malware_similarity(sample_features, known_features):
"""Score malware code similarity based on feature comparison."""
if not sample_features or not known_features:
return 0.0
matches = 0
total = max(len(sample_features), len(known_features))
for feature in sample_features:
if feature in known_features:
matches += 1
return round(matches / total, 4) if total > 0 else 0.0
def evaluate_timing_pattern(campaign_timestamps, actor_timezone_offset=None):
"""Analyze operational timing to infer timezone/working hours."""
if not campaign_timestamps:
return {"score": 0.0, "working_hours": None, "timezone_guess": None}
hours = []
for ts in campaign_timestamps:
try:
if isinstance(ts, str):
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
else:
dt = ts
adjusted = dt.hour + (actor_timezone_offset or 0)
hours.append(adjusted % 24)
except (ValueError, TypeError):
continue
if not hours:
return {"score": 0.0}
work_hours = sum(1 for h in hours if 8 <= h <= 18)
work_ratio = work_hours / len(hours)
avg_hour = sum(hours) / len(hours)
return {
"score": round(work_ratio, 4),
"average_hour_utc": round(avg_hour, 1),
"work_hour_ratio": round(work_ratio, 4),
"sample_size": len(hours),
}
def evaluate_language_artifacts(strings_list):
"""Detect language artifacts in malware strings or documents."""
language_indicators = {
"Russian": [r"[а-яА-Я]{3,}", r"codepage.*1251", r"locale.*ru"],
"Chinese": [r"[\u4e00-\u9fff]{2,}", r"codepage.*936", r"GB2312"],
"Korean": [r"[\uac00-\ud7af]{2,}", r"codepage.*949", r"EUC-KR"],
"Farsi": [r"[\u0600-\u06ff]{3,}", r"codepage.*1256"],
"English": [r"\b(the|and|for|with)\b"],
}
detections = defaultdict(int)
for s in strings_list:
for lang, patterns in language_indicators.items():
for pattern in patterns:
if re.search(pattern, s, re.IGNORECASE):
detections[lang] += 1
total = sum(detections.values()) or 1
scored = {lang: round(count / total, 4) for lang, count in detections.items()}
return scored
def ach_analysis(hypotheses, evidence_items):
"""Analysis of Competing Hypotheses (ACH) for attribution."""
matrix = {}
for hyp in hypotheses:
hyp_name = hyp["name"]
matrix[hyp_name] = {"consistent": 0, "inconsistent": 0, "neutral": 0, "score": 0}
for evidence in evidence_items:
ev_name = evidence["name"]
consistency = evidence.get("hypotheses", {}).get(hyp_name, "neutral")
if consistency == "consistent":
matrix[hyp_name]["consistent"] += evidence.get("weight", 1)
elif consistency == "inconsistent":
matrix[hyp_name]["inconsistent"] += evidence.get("weight", 1)
else:
matrix[hyp_name]["neutral"] += evidence.get("weight", 1)
c = matrix[hyp_name]["consistent"]
i = matrix[hyp_name]["inconsistent"]
matrix[hyp_name]["score"] = round((c - i) / (c + i + 0.01), 4)
return matrix
def compute_attribution_score(scores):
"""Compute weighted attribution confidence score."""
total = 0.0
for evidence_type, weight in EVIDENCE_WEIGHTS.items():
score = scores.get(evidence_type, 0.0)
total += score * weight
confidence = "UNKNOWN"
for (low, high), label in CONFIDENCE_LEVELS.items():
if low <= total < high:
confidence = label
break
return round(total, 4), confidence
def generate_attribution_report(campaign_name, candidate_actor, evidence):
"""Generate structured attribution assessment report."""
scores = {}
details = {}
infra_score, infra_overlap = evaluate_infrastructure_overlap(
evidence.get("campaign_infra", []), evidence.get("actor_infra", []))
scores["infrastructure_overlap"] = infra_score
details["infrastructure_overlap"] = infra_overlap
ttp_score, ttp_overlap = evaluate_ttp_consistency(
evidence.get("campaign_ttps", []), evidence.get("actor_ttps", []))
scores["ttp_consistency"] = ttp_score
details["ttp_consistency"] = ttp_overlap
malware_score = evaluate_malware_similarity(
evidence.get("sample_features", []), evidence.get("known_features", []))
scores["malware_code_similarity"] = malware_score
timing = evaluate_timing_pattern(
evidence.get("timestamps", []), evidence.get("tz_offset"))
scores["timing_pattern"] = timing.get("score", 0.0)
details["timing"] = timing
lang = evaluate_language_artifacts(evidence.get("strings", []))
scores["language_artifact"] = max(lang.values()) if lang else 0.0
details["language_artifacts"] = lang
total_score, confidence = compute_attribution_score(scores)
return {
"campaign": campaign_name,
"candidate_actor": candidate_actor,
"attribution_score": total_score,
"confidence_level": confidence,
"evidence_scores": scores,
"evidence_details": details,
}
if __name__ == "__main__":
print("=" * 60)
print("Campaign Attribution Evidence Analysis Agent")
print("Diamond Model, ACH, TTP/infrastructure/malware scoring")
print("=" * 60)
demo_evidence = {
"campaign_infra": ["185.220.101.1", "evil-domain.com", "c2.attacker.net"],
"actor_infra": ["185.220.101.1", "c2.attacker.net", "other-domain.org"],
"campaign_ttps": ["T1566.001", "T1059.001", "T1053.005", "T1071.001", "T1041"],
"actor_ttps": ["T1566.001", "T1059.001", "T1053.005", "T1071.001", "T1021.001", "T1003.001"],
"sample_features": ["xor_0x55", "mutex_Global\\QWE", "ua_Mozilla5", "rc4_key"],
"known_features": ["xor_0x55", "mutex_Global\\QWE", "ua_Mozilla5", "aes_cbc"],
"timestamps": ["2024-03-15T06:30:00Z", "2024-03-15T07:15:00Z",
"2024-03-16T08:00:00Z", "2024-03-16T09:45:00Z"],
"tz_offset": 3,
"strings": ["Привет мир", "connect to server", "upload file"],
}
report = generate_attribution_report("Operation DarkShadow", "APT29", demo_evidence)
print(f"\n[*] Campaign: {report['campaign']}")
print(f"[*] Candidate: {report['candidate_actor']}")
print(f"[*] Attribution Score: {report['attribution_score']}")
print(f"[*] Confidence: {report['confidence_level']}")
print("\n--- Evidence Scores ---")
for ev, score in report["evidence_scores"].items():
weight = EVIDENCE_WEIGHTS.get(ev, 0)
print(f" {ev:30s} score={score:.4f} weight={weight}")
print(f"\n[*] Full report:\n{json.dumps(report, indent=2, default=str)}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,97 @@
# API Reference: Certificate Transparency Phishing Detection
## crt.sh API
### Search Certificates
```bash
# JSON output
curl "https://crt.sh/?q=%.example.com&output=json"
# Exclude expired
curl "https://crt.sh/?q=%.example.com&output=json&exclude=expired"
# Exact match
curl "https://crt.sh/?q=example.com&output=json"
```
### Response Fields
| Field | Description |
|-------|-------------|
| `id` | Certificate ID in crt.sh database |
| `common_name` | Certificate CN |
| `name_value` | All SANs (newline-separated) |
| `issuer_name` | Certificate Authority |
| `not_before` | Validity start |
| `not_after` | Validity end |
| `serial_number` | Certificate serial |
## Certstream - Real-time CT Monitoring
### Python Client
```python
import certstream
def callback(message, context):
if message["message_type"] == "certificate_update":
data = message["data"]
domains = data["leaf_cert"]["all_domains"]
for domain in domains:
if "example" in domain:
print(f"[ALERT] {domain}")
certstream.listen_for_events(callback, url="wss://certstream.calidog.io/")
```
### Message Fields
| Field | Path |
|-------|------|
| Domains | `data.leaf_cert.all_domains` |
| Issuer | `data.leaf_cert.issuer.O` |
| Subject | `data.leaf_cert.subject.CN` |
| Fingerprint | `data.leaf_cert.fingerprint` |
| Source | `data.source.name` |
## CT Log Servers
| Log | Operator | URL |
|-----|----------|-----|
| Argon | Google | `ct.googleapis.com/logs/argon2024` |
| Xenon | Google | `ct.googleapis.com/logs/xenon2024` |
| Nimbus | Cloudflare | `ct.cloudflare.com/logs/nimbus2024` |
| Oak | Let's Encrypt | `oak.ct.letsencrypt.org/2024h1` |
| Yeti | DigiCert | `yeti2024.ct.digicert.com/log` |
## Phishing Detection Techniques
### Homoglyph / IDN Attacks
| Original | Lookalike | Technique |
|----------|-----------|-----------|
| example.com | examp1e.com | Character substitution (l→1) |
| google.com | gооgle.com | Cyrillic о (U+043E) |
| paypal.com | paypa1.com | l→1 substitution |
| microsoft.com | mіcrosoft.com | Cyrillic і (U+0456) |
### dnstwist Integration
```bash
dnstwist -r -f json example.com # Generate and resolve permutations
dnstwist -w wordlist.txt example.com # Dictionary-based
```
## Certificate Details Lookup
```bash
# Get full certificate from crt.sh
curl "https://crt.sh/?d=<cert_id>"
# OpenSSL inspection
openssl s_client -connect domain.com:443 -servername domain.com </dev/null 2>/dev/null | \
openssl x509 -noout -text
```
## Suspicious Indicators
| Pattern | Risk Level |
|---------|-----------|
| Free CA + new domain + brand keyword | HIGH |
| Wildcard cert on recently registered domain | HIGH |
| Multiple certs for slight domain variants | MEDIUM |
| IDN/punycode domain mimicking brand | HIGH |
| Cert issued same day as domain registration | MEDIUM |
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""Certificate Transparency monitoring agent for phishing detection.
Queries crt.sh for certificates matching target domains, detects lookalike
certificates, and identifies potential phishing infrastructure.
"""
import json
import os
import sys
import re
from datetime import datetime
from collections import defaultdict
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
def query_crtsh(domain, wildcard=True, expired=False):
"""Query crt.sh for certificates matching a domain."""
if not HAS_REQUESTS:
return []
query = f"%.{domain}" if wildcard else domain
params = {"q": query, "output": "json"}
if not expired:
params["exclude"] = "expired"
try:
resp = requests.get("https://crt.sh/", params=params, timeout=30)
resp.raise_for_status()
return resp.json()
except (requests.RequestException, json.JSONDecodeError) as e:
return [{"error": str(e)}]
def find_lookalike_domains(target_domain, ct_results):
"""Identify certificates for domains that look similar to the target."""
base = target_domain.split(".")[0].lower()
lookalikes = []
for cert in ct_results:
cn = cert.get("common_name", "").lower()
names = cert.get("name_value", "").lower().split("\n")
for name in [cn] + names:
name = name.strip()
if not name or name == target_domain:
continue
similarity = calculate_similarity(base, name.split(".")[0])
if similarity > 0.6 and name != target_domain:
lookalikes.append({
"domain": name,
"similarity": round(similarity, 3),
"issuer": cert.get("issuer_name", ""),
"not_before": cert.get("not_before", ""),
"not_after": cert.get("not_after", ""),
"cert_id": cert.get("id"),
})
seen = set()
unique = []
for l in sorted(lookalikes, key=lambda x: -x["similarity"]):
if l["domain"] not in seen:
seen.add(l["domain"])
unique.append(l)
return unique
def calculate_similarity(s1, s2):
"""Calculate string similarity using Levenshtein-like ratio."""
if s1 == s2:
return 1.0
len1, len2 = len(s1), len(s2)
if len1 == 0 or len2 == 0:
return 0.0
matrix = [[0] * (len2 + 1) for _ in range(len1 + 1)]
for i in range(len1 + 1):
matrix[i][0] = i
for j in range(len2 + 1):
matrix[0][j] = j
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
cost = 0 if s1[i-1] == s2[j-1] else 1
matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1,
matrix[i-1][j-1] + cost)
distance = matrix[len1][len2]
return 1.0 - distance / max(len1, len2)
HOMOGLYPH_MAP = {
"a": ["а", "@", "4"], "e": ["е", "3"], "o": ["о", "0"],
"i": ["і", "1", "l"], "l": ["1", "i", "I"],
"s": ["5", "$"], "t": ["7"], "g": ["9", "q"],
}
def detect_homoglyph_domains(target_domain, ct_results):
"""Detect domains using homoglyph/IDN attacks against target."""
findings = []
base = target_domain.split(".")[0].lower()
for cert in ct_results:
names = cert.get("name_value", "").lower().split("\n")
for name in names:
name = name.strip()
if not name or name == target_domain:
continue
name_base = name.split(".")[0]
if len(name_base) == len(base):
diffs = sum(1 for a, b in zip(base, name_base) if a != b)
if 0 < diffs <= 2:
findings.append({
"domain": name,
"char_differences": diffs,
"cert_id": cert.get("id"),
"issuer": cert.get("issuer_name", ""),
})
return findings
def analyze_issuer_patterns(ct_results):
"""Analyze certificate issuer patterns for anomalies."""
issuer_counts = defaultdict(int)
free_cas = ["Let's Encrypt", "ZeroSSL", "Buypass"]
for cert in ct_results:
issuer = cert.get("issuer_name", "Unknown")
issuer_counts[issuer] += 1
free_ca_certs = sum(
count for issuer, count in issuer_counts.items()
if any(ca.lower() in issuer.lower() for ca in free_cas)
)
return {
"issuers": dict(issuer_counts),
"total_certs": len(ct_results),
"free_ca_count": free_ca_certs,
"free_ca_ratio": round(free_ca_certs / max(len(ct_results), 1), 3),
}
def detect_wildcard_abuse(ct_results):
"""Detect suspicious wildcard certificate patterns."""
wildcards = []
for cert in ct_results:
cn = cert.get("common_name", "")
if cn.startswith("*."):
wildcards.append({
"domain": cn,
"issuer": cert.get("issuer_name", ""),
"not_before": cert.get("not_before", ""),
})
return wildcards
def generate_report(target_domain, ct_results):
"""Generate comprehensive CT monitoring report."""
lookalikes = find_lookalike_domains(target_domain, ct_results)
homoglyphs = detect_homoglyph_domains(target_domain, ct_results)
issuer_analysis = analyze_issuer_patterns(ct_results)
wildcards = detect_wildcard_abuse(ct_results)
risk_score = 0
risk_score += min(len(lookalikes) * 10, 40)
risk_score += min(len(homoglyphs) * 15, 30)
risk_score += 20 if issuer_analysis["free_ca_ratio"] > 0.8 else 0
risk_score = min(risk_score, 100)
return {
"target_domain": target_domain,
"total_certificates": len(ct_results),
"lookalike_domains": lookalikes[:20],
"homoglyph_domains": homoglyphs[:20],
"issuer_analysis": issuer_analysis,
"wildcard_certs": wildcards[:10],
"risk_score": risk_score,
"risk_level": "HIGH" if risk_score >= 60 else "MEDIUM" if risk_score >= 30 else "LOW",
}
if __name__ == "__main__":
print("=" * 60)
print("Certificate Transparency Phishing Detection Agent")
print("crt.sh queries, lookalike detection, homoglyph analysis")
print("=" * 60)
domain = sys.argv[1] if len(sys.argv) > 1 else None
if not domain:
print("\n[DEMO] Usage: python agent.py <target_domain>")
print(" e.g. python agent.py example.com")
sys.exit(0)
if not HAS_REQUESTS:
print("[!] Install requests: pip install requests")
sys.exit(1)
print(f"\n[*] Querying crt.sh for: {domain}")
results = query_crtsh(domain)
print(f"[*] Found {len(results)} certificates")
report = generate_report(domain, results)
print(f"\n--- Lookalike Domains ({len(report['lookalike_domains'])}) ---")
for l in report["lookalike_domains"][:10]:
print(f" [{l['similarity']:.3f}] {l['domain']} (issuer: {l['issuer'][:40]})")
print(f"\n--- Homoglyph Domains ({len(report['homoglyph_domains'])}) ---")
for h in report["homoglyph_domains"][:10]:
print(f" [diff={h['char_differences']}] {h['domain']}")
print(f"\n--- Issuer Analysis ---")
for issuer, count in sorted(report["issuer_analysis"]["issuers"].items(),
key=lambda x: -x[1])[:5]:
print(f" {count:4d} | {issuer[:60]}")
print(f"\n[*] Risk Score: {report['risk_score']}/100 ({report['risk_level']})")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,32 @@
---
name: analyzing-cloud-storage-access-patterns
description: >-
Detect abnormal access patterns in AWS S3, GCS, and Azure Blob Storage by analyzing CloudTrail
Data Events, GCS audit logs, and Azure Storage Analytics. Identifies after-hours bulk downloads,
access from new IP addresses, unusual API calls (GetObject spikes), and potential data exfiltration
using statistical baselines and time-series anomaly detection.
---
## Instructions
1. Install dependencies: `pip install boto3 requests`
2. Query CloudTrail for S3 Data Events using AWS CLI or boto3.
3. Build access baselines: hourly request volume, per-user object counts, source IP history.
4. Detect anomalies:
- After-hours access (outside 8am-6pm local time)
- Bulk downloads: >100 GetObject calls from single principal in 1 hour
- New source IPs not seen in the prior 30 days
- ListBucket enumeration spikes (reconnaissance indicator)
5. Generate prioritized findings report.
```bash
python scripts/agent.py --bucket my-sensitive-data --hours-back 24 --output s3_access_report.json
```
## Examples
### CloudTrail S3 Data Event
```json
{"eventName": "GetObject", "requestParameters": {"bucketName": "sensitive-data", "key": "financials/q4.xlsx"},
"sourceIPAddress": "203.0.113.50", "userIdentity": {"arn": "arn:aws:iam::123456789012:user/analyst"}}
```
@@ -0,0 +1,49 @@
# API Reference: Cloud Storage Access Pattern Analysis
## AWS CLI - CloudTrail Lookup
```bash
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ResourceType,AttributeValue=AWS::S3::Object \
--start-time 2024-01-15T00:00:00Z \
--output json
```
## CloudTrail S3 Data Event Structure
```json
{
"EventTime": "2024-01-15T10:30:00Z",
"EventName": "GetObject",
"Username": "analyst",
"CloudTrailEvent": "{\"sourceIPAddress\":\"10.0.0.1\",\"userAgent\":\"aws-cli\",\"requestParameters\":{\"bucketName\":\"data\",\"key\":\"file.csv\"},\"userIdentity\":{\"arn\":\"arn:aws:iam::123:user/analyst\"}}"
}
```
## Key S3 Event Names
| Event | Meaning |
|-------|---------|
| GetObject | Object download |
| PutObject | Object upload |
| DeleteObject | Object deletion |
| ListBucket / ListObjectsV2 | Bucket enumeration |
| GetBucketPolicy | Policy read |
| PutBucketPolicy | Policy modification |
## Detection Thresholds
| Anomaly | Threshold | Severity |
|---------|-----------|----------|
| Bulk download | >100 GetObject/hr per user | Critical |
| After-hours | Access outside 08:00-18:00 UTC | Medium |
| New source IP | IP not in 30-day baseline | High |
| Enumeration | >20 ListBucket per user | High |
## boto3 CloudTrail Client (alternative)
```python
import boto3
client = boto3.client("cloudtrail")
response = client.lookup_events(
LookupAttributes=[{"AttributeKey":"ResourceType","AttributeValue":"AWS::S3::Object"}],
StartTime=datetime(2024,1,15),
MaxResults=50
)
events = response["Events"]
```
@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""Cloud Storage Access Pattern Analyzer - Detects abnormal S3/GCS/Azure Blob access via CloudTrail."""
import json
import logging
import argparse
import subprocess
from collections import defaultdict
from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def query_cloudtrail_s3_events(bucket_name, hours_back=24):
"""Query CloudTrail for S3 data events on a specific bucket."""
start_time = (datetime.utcnow() - timedelta(hours=hours_back)).strftime("%Y-%m-%dT%H:%M:%SZ")
cmd = [
"aws", "cloudtrail", "lookup-events",
"--lookup-attributes", f"AttributeKey=ResourceType,AttributeValue=AWS::S3::Object",
"--start-time", start_time,
"--output", "json",
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
logger.error("CloudTrail query failed: %s", result.stderr[:200])
return []
events = json.loads(result.stdout).get("Events", [])
s3_events = []
for event in events:
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
req_params = ct_event.get("requestParameters", {})
if req_params.get("bucketName") == bucket_name or not bucket_name:
s3_events.append({
"timestamp": event.get("EventTime", ""),
"event_name": event.get("EventName", ""),
"username": event.get("Username", ""),
"source_ip": ct_event.get("sourceIPAddress", ""),
"user_agent": ct_event.get("userAgent", ""),
"bucket": req_params.get("bucketName", ""),
"key": req_params.get("key", ""),
"user_arn": ct_event.get("userIdentity", {}).get("arn", ""),
})
logger.info("Retrieved %d S3 events for bucket '%s'", len(s3_events), bucket_name or "all")
return s3_events
def detect_bulk_downloads(events, threshold=100):
"""Detect bulk GetObject operations from a single principal."""
user_downloads = defaultdict(list)
for event in events:
if event["event_name"] == "GetObject":
user_downloads[event["user_arn"]].append(event)
alerts = []
for user_arn, downloads in user_downloads.items():
if len(downloads) >= threshold:
keys = [d["key"] for d in downloads]
alerts.append({
"user_arn": user_arn,
"download_count": len(downloads),
"unique_keys": len(set(keys)),
"source_ips": list({d["source_ip"] for d in downloads}),
"first_access": downloads[0]["timestamp"],
"last_access": downloads[-1]["timestamp"],
"severity": "critical",
"indicator": "Bulk download (potential exfiltration)",
})
logger.info("Found %d bulk download alerts", len(alerts))
return alerts
def detect_after_hours_access(events, business_start=8, business_end=18):
"""Detect access outside business hours."""
after_hours = []
for event in events:
try:
ts = event["timestamp"]
if isinstance(ts, str):
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
else:
dt = ts
hour = dt.hour
if hour < business_start or hour >= business_end:
event["indicator"] = f"After-hours access at {hour:02d}:00 UTC"
event["severity"] = "medium"
after_hours.append(event)
except (ValueError, AttributeError):
continue
logger.info("Found %d after-hours access events", len(after_hours))
return after_hours
def detect_new_source_ips(events, known_ips=None):
"""Detect access from IP addresses not in the known baseline."""
if known_ips is None:
known_ips = set()
new_ip_events = []
for event in events:
ip = event["source_ip"]
if ip and ip not in known_ips and not ip.startswith("AWS Internal"):
event["indicator"] = f"New source IP: {ip}"
event["severity"] = "high"
new_ip_events.append(event)
unique_new = len({e["source_ip"] for e in new_ip_events})
logger.info("Found %d events from %d new source IPs", len(new_ip_events), unique_new)
return new_ip_events
def detect_enumeration(events, threshold=20):
"""Detect ListBucket/ListObjects enumeration patterns."""
user_listings = defaultdict(int)
for event in events:
if event["event_name"] in ("ListBucket", "ListObjects", "ListObjectsV2"):
user_listings[event["user_arn"]] += 1
alerts = []
for user_arn, count in user_listings.items():
if count >= threshold:
alerts.append({
"user_arn": user_arn,
"list_count": count,
"severity": "high",
"indicator": "Bucket enumeration spike (reconnaissance)",
})
return alerts
def build_access_baseline(events):
"""Build statistical baseline of normal access patterns."""
hourly_counts = defaultdict(int)
user_counts = defaultdict(int)
ip_set = set()
for event in events:
try:
ts = event["timestamp"]
if isinstance(ts, str):
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
hourly_counts[dt.hour] += 1
except (ValueError, AttributeError):
pass
user_counts[event["user_arn"]] += 1
if event["source_ip"]:
ip_set.add(event["source_ip"])
return {
"hourly_distribution": dict(hourly_counts),
"user_request_counts": dict(user_counts),
"known_ips": list(ip_set),
"total_events": len(events),
}
def generate_report(events, bulk_alerts, after_hours, new_ips, enum_alerts, baseline):
"""Generate cloud storage access analysis report."""
report = {
"timestamp": datetime.utcnow().isoformat(),
"total_events_analyzed": len(events),
"bulk_download_alerts": bulk_alerts,
"after_hours_access": len(after_hours),
"new_source_ip_events": len(new_ips),
"enumeration_alerts": enum_alerts,
"baseline_summary": {
"known_ips": len(baseline.get("known_ips", [])),
"total_baseline_events": baseline.get("total_events", 0),
},
"sample_after_hours": after_hours[:10],
"sample_new_ips": new_ips[:10],
}
total_alerts = len(bulk_alerts) + len(enum_alerts) + (1 if new_ips else 0)
print(f"CLOUD STORAGE REPORT: {len(events)} events, {total_alerts} alerts")
return report
def main():
parser = argparse.ArgumentParser(description="Cloud Storage Access Pattern Analyzer")
parser.add_argument("--bucket", default="", help="S3 bucket name to analyze")
parser.add_argument("--hours-back", type=int, default=24)
parser.add_argument("--bulk-threshold", type=int, default=100)
parser.add_argument("--known-ips-file", help="File with known IP baselines")
parser.add_argument("--output", default="s3_access_report.json")
args = parser.parse_args()
events = query_cloudtrail_s3_events(args.bucket, args.hours_back)
baseline = build_access_baseline(events)
known_ips = set(baseline.get("known_ips", []))
if args.known_ips_file:
with open(args.known_ips_file) as f:
known_ips.update(line.strip() for line in f if line.strip())
bulk_alerts = detect_bulk_downloads(events, args.bulk_threshold)
after_hours = detect_after_hours_access(events)
new_ips = detect_new_source_ips(events, known_ips)
enum_alerts = detect_enumeration(events)
report = generate_report(events, bulk_alerts, after_hours, new_ips, enum_alerts, baseline)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,112 @@
# API Reference: Cobalt Strike Beacon Configuration Analysis
## Beacon Config TLV Format
### Structure
```
[Field ID: 2 bytes][Type: 2 bytes][Value: variable]
Type 1 = short (2 bytes), Type 2 = int (4 bytes), Type 3 = string/blob (2-byte length + data)
```
### XOR Encoding
| Version | XOR Key |
|---------|---------|
| CS 3.x | `0x69` |
| CS 4.x | `0x2E` |
### Key Configuration Fields
| ID | Name | Description |
|----|------|-------------|
| 1 | BeaconType | 0=HTTP, 1=Hybrid, 2=SMB, 8=HTTPS |
| 2 | Port | C2 communication port |
| 3 | SleepTime | Beacon interval (ms) |
| 5 | Jitter | Random sleep variation (%) |
| 7 | PublicKey | RSA public key for encryption |
| 8 | C2Server | Command and control server(s) |
| 9 | UserAgent | HTTP User-Agent string |
| 10 | PostURI | POST callback URI |
| 37 | Watermark | License watermark (operator ID) |
| 54 | PipeName | Named pipe for SMB beacons |
## 1768.py (Didier Stevens) - Config Extractor
### Syntax
```bash
python 1768.py <beacon_file> # Extract config
python 1768.py -j <beacon_file> # JSON output
python 1768.py -r <beacon_file> # Raw config dump
```
## CobaltStrikeParser (SentinelOne)
### Syntax
```bash
python parse_beacon_config.py <file>
python parse_beacon_config.py --json <file>
```
### Output Fields
```
BeaconType: HTTPS
Port: 443
SleepTime: 60000
Jitter: 37
C2Server: update.microsoft-cdn.com,/api/v2
UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Watermark: 305419896
SpawnToX86: %windir%\syswow64\dllhost.exe
SpawnToX64: %windir%\sysnative\dllhost.exe
```
## JARM Fingerprinting
### Cobalt Strike Default JARM
```bash
# Default CS JARM hash (pre-4.7)
07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1
# Scan with JARM
python jarm.py <target_ip> -p 443
```
## Known Watermark Values
| Watermark | Attribution |
|-----------|------------|
| 0 | Trial/cracked version |
| 305419896 | Common cracked version |
| 1359593325 | Known threat actor toolkit |
| 1580103824 | Known APT usage |
## Detection Signatures
### Suricata
```
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"ET MALWARE Cobalt Strike Beacon";
content:"/submit.php"; http_uri;
content:"Cookie:"; http_header;
pcre:"/Cookie:\s[A-Za-z0-9+/=]{60,}/H";
sid:2028591; rev:1;)
```
### YARA
```yara
rule CobaltStrike_Beacon {
strings:
$config_v3 = { 00 01 00 01 00 02 ?? ?? 00 01 00 02 }
$magic = "MSSE-%d-server"
$pipe = "\\\\.\\pipe\\msagent_"
condition:
uint16(0) == 0x5A4D and any of them
}
```
## Malleable C2 Profile Elements
| Element | Description |
|---------|-------------|
| `http-get` | GET request profile (URI, headers, metadata transform) |
| `http-post` | POST request profile (URI, body transform) |
| `set sleeptime` | Default beacon interval |
| `set jitter` | Randomization percentage |
| `set useragent` | HTTP User-Agent |
| `set pipename` | SMB named pipe name |
@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""Cobalt Strike beacon configuration extraction and analysis agent.
Extracts C2 configuration from beacon payloads including server addresses,
communication settings, malleable C2 profile details, and watermark values.
"""
import struct
import os
import sys
import json
import hashlib
import re
from collections import OrderedDict
# Cobalt Strike beacon configuration field IDs (Type-Length-Value format)
BEACON_CONFIG_FIELDS = {
1: ("BeaconType", "short"),
2: ("Port", "short"),
3: ("SleepTime", "int"),
4: ("MaxGetSize", "int"),
5: ("Jitter", "short"),
7: ("PublicKey", "bytes"),
8: ("C2Server", "str"),
9: ("UserAgent", "str"),
10: ("PostURI", "str"),
11: ("Malleable_C2_Instructions", "bytes"),
12: ("HttpGet_Metadata", "bytes"),
13: ("HttpPost_Metadata", "bytes"),
14: ("SpawnToX86", "str"),
15: ("SpawnToX64", "str"),
19: ("CryptoScheme", "short"),
26: ("GetVerb", "str"),
27: ("PostVerb", "str"),
28: ("HttpPostChunk", "int"),
29: ("Spawnto_x86", "str"),
30: ("Spawnto_x64", "str"),
31: ("CryptoScheme2", "str"),
37: ("Watermark", "int"),
38: ("StageCleanup", "short"),
39: ("CFGCaution", "short"),
43: ("DNS_Idle", "int"),
44: ("DNS_Sleep", "int"),
50: ("HostHeader", "str"),
54: ("PipeName", "str"),
}
BEACON_TYPES = {0: "HTTP", 1: "Hybrid HTTP/DNS", 2: "SMB", 4: "TCP", 8: "HTTPS", 16: "DNS over HTTPS"}
XOR_KEY_V3 = 0x69
XOR_KEY_V4 = 0x2E
def compute_hash(filepath):
"""Compute SHA-256 hash of file."""
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
return sha256.hexdigest()
def find_config_offset(data):
"""Find the beacon configuration blob in PE data or shellcode."""
# Look for XOR-encoded config patterns
for xor_key in [XOR_KEY_V3, XOR_KEY_V4]:
# Config starts with 0x0001 (BeaconType field ID) XOR-encoded
encoded_marker = bytes([0x00 ^ xor_key, 0x01 ^ xor_key, 0x00 ^ xor_key, 0x01 ^ xor_key])
offset = data.find(encoded_marker)
if offset != -1:
return offset, xor_key
# Try unencoded
for offset in range(len(data) - 100):
if data[offset:offset+4] == b"\x00\x01\x00\x01":
return offset, None
return -1, None
def xor_decode(data, key):
"""XOR decode data with single byte key."""
if key is None:
return data
return bytes(b ^ key for b in data)
def parse_config_field(data, offset):
"""Parse a single TLV config field."""
if offset + 6 > len(data):
return None, None, None, offset
field_id = struct.unpack_from(">H", data, offset)[0]
field_type = struct.unpack_from(">H", data, offset + 2)[0]
if field_type == 1: # short
value = struct.unpack_from(">H", data, offset + 4)[0]
return field_id, "short", value, offset + 6
elif field_type == 2: # int
value = struct.unpack_from(">I", data, offset + 4)[0]
return field_id, "int", value, offset + 8
elif field_type == 3: # str/bytes
length = struct.unpack_from(">H", data, offset + 4)[0]
if offset + 6 + length > len(data):
return None, None, None, offset
value = data[offset + 6:offset + 6 + length]
return field_id, "str", value, offset + 6 + length
return None, None, None, offset + 2
def extract_beacon_config(filepath):
"""Extract and parse Cobalt Strike beacon configuration."""
with open(filepath, "rb") as f:
data = f.read()
config_offset, xor_key = find_config_offset(data)
if config_offset == -1:
return {"error": "No beacon configuration found", "file": filepath}
config_data = xor_decode(data[config_offset:config_offset + 4096], xor_key)
config = OrderedDict()
config["_meta"] = {
"config_offset": f"0x{config_offset:08X}",
"xor_key": f"0x{xor_key:02X}" if xor_key else "none",
"version_guess": "4.x" if xor_key == XOR_KEY_V4 else "3.x" if xor_key == XOR_KEY_V3 else "unknown",
}
offset = 0
max_fields = 100
parsed = 0
while offset < len(config_data) - 4 and parsed < max_fields:
field_id, field_type, value, new_offset = parse_config_field(config_data, offset)
if field_id is None or new_offset == offset:
break
offset = new_offset
parsed += 1
field_info = BEACON_CONFIG_FIELDS.get(field_id)
if field_info:
field_name, expected_type = field_info
if isinstance(value, bytes):
try:
str_value = value.rstrip(b"\x00").decode("utf-8", errors="replace")
config[field_name] = str_value
except Exception:
config[field_name] = value.hex()[:100]
elif field_id == 1:
config[field_name] = BEACON_TYPES.get(value, f"Unknown({value})")
else:
config[field_name] = value
return config
def extract_c2_indicators(config):
"""Extract C2 indicators from parsed config for threat intelligence."""
indicators = {"c2_servers": [], "user_agents": [], "uris": [],
"pipes": [], "watermark": None, "dns": []}
c2 = config.get("C2Server", "")
if c2:
for server in c2.split(","):
server = server.strip().rstrip("/")
if server:
indicators["c2_servers"].append(server)
ua = config.get("UserAgent", "")
if ua:
indicators["user_agents"].append(ua)
for key in ["PostURI"]:
uri = config.get(key, "")
if uri:
indicators["uris"].append(uri)
pipe = config.get("PipeName", "")
if pipe:
indicators["pipes"].append(pipe)
wm = config.get("Watermark")
if wm:
indicators["watermark"] = wm
return indicators
def assess_operator_opsec(config):
"""Assess operator OPSEC based on beacon configuration."""
findings = []
sleep = config.get("SleepTime", 0)
jitter = config.get("Jitter", 0)
if sleep < 30000:
findings.append({"level": "INFO", "detail": f"Low sleep time: {sleep}ms - high beacon frequency"})
if jitter == 0:
findings.append({"level": "WARN", "detail": "No jitter configured - predictable beacon interval"})
ua = config.get("UserAgent", "")
if "Mozilla" not in ua and ua:
findings.append({"level": "WARN", "detail": f"Non-standard User-Agent: {ua[:60]}"})
spawn86 = config.get("SpawnToX86", config.get("Spawnto_x86", ""))
if "rundll32" in spawn86.lower():
findings.append({"level": "INFO", "detail": "Default spawn-to process (rundll32) - easy to detect"})
cleanup = config.get("StageCleanup", 0)
if cleanup == 0:
findings.append({"level": "INFO", "detail": "Stage cleanup disabled - beacon stub remains in memory"})
return findings
if __name__ == "__main__":
print("=" * 60)
print("Cobalt Strike Beacon Configuration Extractor")
print("C2 extraction, watermark analysis, OPSEC assessment")
print("=" * 60)
target = sys.argv[1] if len(sys.argv) > 1 else None
if not target or not os.path.exists(target):
print("\n[DEMO] Usage: python agent.py <beacon_sample.exe>")
print(" Extracts: C2 servers, sleep/jitter, watermark, malleable profile")
sys.exit(0)
print(f"\n[*] Analyzing: {target}")
print(f"[*] SHA-256: {compute_hash(target)}")
print(f"[*] Size: {os.path.getsize(target)} bytes")
config = extract_beacon_config(target)
if "error" in config:
print(f"\n[!] {config['error']}")
sys.exit(1)
print("\n--- Beacon Configuration ---")
for key, value in config.items():
if key == "_meta":
for mk, mv in value.items():
print(f" {mk}: {mv}")
else:
print(f" {key}: {value}")
indicators = extract_c2_indicators(config)
print("\n--- C2 Indicators ---")
for c2 in indicators["c2_servers"]:
print(f" [C2] {c2}")
if indicators["watermark"]:
print(f" [Watermark] {indicators['watermark']}")
for pipe in indicators["pipes"]:
print(f" [Pipe] {pipe}")
opsec = assess_operator_opsec(config)
print("\n--- Operator OPSEC Assessment ---")
for f in opsec:
print(f" [{f['level']}] {f['detail']}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,54 @@
---
name: analyzing-cobalt-strike-malleable-profiles
description: >
Parses Cobalt Strike malleable C2 profiles using pyMalleableC2 to extract beacon
configuration, HTTP communication patterns, and sleep/jitter settings. Combines with
JARM TLS fingerprinting to detect C2 servers on the network. Use when investigating
suspected Cobalt Strike infrastructure or building detection signatures for C2 traffic.
---
# Analyzing Cobalt Strike Malleable Profiles
## Instructions
Parse malleable C2 profiles to extract IOCs and detection opportunities using the
pyMalleableC2 library. Combine with JARM fingerprinting to identify C2 servers.
```python
from malleablec2 import Profile
# Parse a malleable profile from file
profile = Profile.from_file("amazon.profile")
# Extract global options (sleep, jitter, user-agent)
print(profile.ast.pretty())
# Access HTTP-GET block URIs and headers for network signatures
# Access HTTP-POST block for data exfiltration patterns
# Generate JARM fingerprints for known C2 infrastructure
```
Key analysis steps:
1. Parse the malleable profile to extract HTTP-GET/POST URI patterns
2. Extract User-Agent strings and custom headers for IDS signatures
3. Identify sleep time and jitter for beaconing detection thresholds
4. Scan suspect IPs with JARM to match known C2 fingerprint hashes
5. Cross-reference extracted IOCs with network traffic logs
## Examples
```python
# Parse profile and extract detection indicators
from malleablec2 import Profile
p = Profile.from_file("cobaltstrike.profile")
print(p) # Reconstructed source
# JARM scan a suspect C2 server
import subprocess
result = subprocess.run(
["python3", "jarm.py", "suspect-server.com"],
capture_output=True, text=True
)
print(result.stdout)
# Compare fingerprint against known CS JARM hashes
```
@@ -0,0 +1,69 @@
# API Reference: Analyzing Cobalt Strike Malleable Profiles
## pyMalleableC2
```python
from malleablec2 import Profile
from malleablec2.components import HttpGetBlock, HttpPostBlock, ClientBlock, ServerBlock
# Parse from file or string
p = Profile.from_file("amazon.profile")
p = Profile.from_string(code_string)
p = Profile.from_scratch()
# Set global options
p.set_option("sleeptime", "3000")
p.set_option("jitter", "0")
p.set_option("pipename", "mojo__##")
# HTTP blocks
http_get = HttpGetBlock()
http_get.set_option("uri", "/updates")
client = ClientBlock()
client.add_statement("header", "Accept", "*/*")
http_get.add_code_block(client)
p.add_code_block(http_get)
# AST and reconstruction
print(p.ast.pretty()) # Display AST
print(p) # Reconstruct source
```
## JARM TLS Fingerprinting
```bash
# Scan a single host
python3 jarm.py www.example.com
# Scan with specific port
python3 jarm.py 192.168.1.1 -p 8443
# Batch scan from file
python3 jarm.py -i targets.txt -o results.csv
```
Fingerprint format: 62-char hybrid hash
- First 30 chars: cipher + TLS version (10 handshakes x 3 chars)
- Last 32 chars: truncated SHA256 of cumulative extensions
## Known Cobalt Strike JARM Hashes
| JARM Hash | Description |
|-----------|-------------|
| `07d14d16d21d21d07c42d41d00041d...` | CS default config |
| `07d14d16d21d21d00042d41d00041d...` | CS with Java 11 |
## dissect.cobaltstrike (Alternative)
```python
from dissect.cobaltstrike import beacon
b = beacon.BeaconConfig.from_file("beacon.bin")
print(b.protocol, b.port, b.sleeptime)
```
### References
- pyMalleableC2: https://github.com/byt3bl33d3r/pyMalleableC2
- JARM scanner: https://github.com/salesforce/jarm
- dissect.cobaltstrike: https://github.com/fox-it/dissect.cobaltstrike
- C2 JARM list: https://github.com/cedowens/C2-JARM
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Agent for analyzing Cobalt Strike malleable C2 profiles and JARM fingerprinting."""
import os
import json
import subprocess
import argparse
from pathlib import Path
from datetime import datetime
from malleablec2 import Profile
def extract_profile_indicators(profile_path):
"""Extract detection indicators from a malleable C2 profile."""
with open(profile_path) as f:
content = f.read()
profile = Profile.from_string(content)
indicators = {
"file": str(profile_path),
"source_lines": len(content.splitlines()),
"reconstructed": str(profile),
}
keywords = ["sleeptime", "jitter", "useragent", "pipename", "host_stage",
"dns_idle", "dns_sleep", "spawnto_x86", "spawnto_x64"]
options = {}
for kw in keywords:
for line in content.splitlines():
stripped = line.strip().rstrip(";").strip()
if kw in stripped.lower() and "set " in stripped.lower():
parts = stripped.split('"')
if len(parts) >= 2:
options[kw] = parts[1]
indicators["global_options"] = options
uris = []
for line in content.splitlines():
if "set uri" in line.strip().lower():
parts = line.strip().split('"')
if len(parts) >= 2:
uris.append(parts[1])
indicators["uris"] = uris
headers = []
for line in content.splitlines():
stripped = line.strip()
if "header " in stripped.lower() and '"' in stripped:
parts = stripped.split('"')
if len(parts) >= 4:
headers.append({"name": parts[1], "value": parts[3]})
indicators["custom_headers"] = headers
return indicators
def scan_directory_profiles(directory):
"""Scan a directory for malleable C2 profiles and extract indicators."""
results = []
for path in Path(directory).rglob("*.profile"):
try:
indicators = extract_profile_indicators(str(path))
results.append(indicators)
except Exception as e:
results.append({"file": str(path), "error": str(e)})
return results
KNOWN_CS_JARM = {
"07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1":
"Cobalt Strike (default)",
"07d14d16d21d21d00042d41d00041de5fb3038104f457d92ba02e9311512c2":
"Cobalt Strike (Java 11)",
}
def compute_jarm_fingerprint(host, port=443):
"""Compute JARM fingerprint by invoking the salesforce/jarm scanner."""
jarm_script = os.getenv("JARM_SCRIPT", "jarm.py")
try:
result = subprocess.run(
["python3", jarm_script, host, "-p", str(port)],
capture_output=True, text=True, timeout=30,
)
for line in result.stdout.splitlines():
if len(line.strip()) >= 62:
return line.strip().split()[-1]
return result.stdout.strip()
except Exception as e:
return f"Error: {e}"
def check_jarm_against_known(fingerprint):
"""Check a JARM fingerprint against known Cobalt Strike signatures."""
for jarm_hash, description in KNOWN_CS_JARM.items():
if fingerprint.strip() == jarm_hash:
return {"match": True, "description": description, "fingerprint": fingerprint}
return {"match": False, "fingerprint": fingerprint}
def batch_jarm_scan(targets, port=443):
"""Scan multiple targets for JARM fingerprints and check against known CS hashes."""
results = []
for target in targets:
fp = compute_jarm_fingerprint(target, port)
match = check_jarm_against_known(fp)
match["target"] = target
results.append(match)
return results
def generate_snort_rules(indicators_list):
"""Generate Snort/Suricata rules from extracted profile indicators."""
rules = []
sid = 1000001
for ind in indicators_list:
for uri in ind.get("uris", []):
rules.append(
f'alert http $HOME_NET any -> $EXTERNAL_NET any '
f'(msg:"CS Beacon URI {uri}"; '
f'content:"{uri}"; http_uri; sid:{sid}; rev:1;)'
)
sid += 1
ua = ind.get("global_options", {}).get("useragent", "")
if ua:
rules.append(
f'alert http $HOME_NET any -> $EXTERNAL_NET any '
f'(msg:"CS Beacon User-Agent"; '
f'content:"{ua}"; http_header; sid:{sid}; rev:1;)'
)
sid += 1
return rules
def main():
parser = argparse.ArgumentParser(description="Cobalt Strike Malleable Profile Analyzer")
parser.add_argument("--profile", help="Path to a single malleable C2 profile")
parser.add_argument("--directory", help="Directory of malleable profiles")
parser.add_argument("--jarm-targets", nargs="*", help="Hosts to JARM fingerprint")
parser.add_argument("--output", default="cs_analysis_report.json")
parser.add_argument("--action", choices=[
"parse", "scan_dir", "jarm", "generate_rules", "full_analysis"
], default="full_analysis")
args = parser.parse_args()
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
if args.action in ("parse", "full_analysis") and args.profile:
indicators = extract_profile_indicators(args.profile)
report["findings"]["profile_indicators"] = indicators
print(f"[+] Parsed: {args.profile} ({len(indicators.get('uris', []))} URIs)")
if args.action in ("scan_dir", "full_analysis") and args.directory:
results = scan_directory_profiles(args.directory)
report["findings"]["directory_scan"] = results
print(f"[+] Scanned {len(results)} profiles in {args.directory}")
if args.action in ("jarm", "full_analysis") and args.jarm_targets:
jarm_results = batch_jarm_scan(args.jarm_targets)
report["findings"]["jarm_scan"] = jarm_results
matches = [r for r in jarm_results if r.get("match")]
print(f"[+] JARM: {len(jarm_results)} scanned, {len(matches)} CS matches")
if args.action in ("generate_rules", "full_analysis"):
profiles = report["findings"].get("directory_scan", [])
if not profiles and args.profile:
profiles = [report["findings"].get("profile_indicators", {})]
rules = generate_snort_rules(profiles)
report["findings"]["snort_rules"] = rules
print(f"[+] Generated {len(rules)} Snort rules")
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,112 @@
# API Reference: C2 Communication Analysis Tools
## Scapy - Packet Analysis Library (Python)
### Reading PCAPs
```python
from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR
packets = rdpcap("capture.pcap")
```
### Filtering Packets
```python
# TCP SYN packets (connection initiation)
syn_pkts = [p for p in packets if TCP in p and (p[TCP].flags & 0x02)]
# DNS queries
dns_pkts = [p for p in packets if DNS in p and p[DNS].qr == 0]
# Access fields
pkt[IP].src # Source IP
pkt[IP].dst # Destination IP
pkt[TCP].sport # Source port
pkt[TCP].dport # Destination port
pkt[TCP].flags # TCP flags (0x02 = SYN)
float(pkt.time) # Packet timestamp
```
## dpkt - Packet Parsing Library (Python)
### Reading PCAPs
```python
import dpkt
with open("capture.pcap", "rb") as f:
pcap = dpkt.pcap.Reader(f)
for timestamp, buf in pcap:
eth = dpkt.ethernet.Ethernet(buf)
ip = eth.data
tcp = ip.data
```
### HTTP Request Parsing
```python
http = dpkt.http.Request(tcp.data)
http.method # GET, POST
http.uri # /path
http.headers # dict of headers
http.body # POST body
```
## tshark - CLI Wireshark
### Beacon Analysis
```bash
tshark -r capture.pcap -T fields -e ip.dst -e tcp.dstport -e frame.time_epoch \
-Y "tcp.flags.syn==1" > syn_times.csv
```
### HTTP Extraction
```bash
tshark -r capture.pcap -Y "http.request" -T fields \
-e http.request.method -e http.host -e http.request.uri -e http.user_agent
```
### DNS Extraction
```bash
tshark -r capture.pcap -Y "dns.qr==0" -T fields \
-e dns.qry.name -e dns.qry.type -e ip.src
```
### JA3 TLS Fingerprinting
```bash
tshark -r capture.pcap -Y "tls.handshake.type==1" -T fields \
-e ip.src -e tls.handshake.ja3
```
## CobaltStrikeParser - Beacon Config Extraction
### Usage
```python
from cobalt_strike_parser import BeaconConfig
config = BeaconConfig.from_file("beacon.bin")
for key, value in config.items():
print(f"{key}: {value}")
```
### Key Config Fields
| Field | Description |
|-------|-------------|
| `BeaconType` | HTTP, HTTPS, DNS, SMB |
| `C2Server` | Primary C2 URL |
| `SleepTime` | Beacon interval (ms) |
| `Jitter` | Jitter percentage |
| `UserAgent` | HTTP User-Agent string |
| `Watermark` | License watermark ID |
## Suricata - Network IDS Rules
### Rule Syntax
```
alert <proto> <src> <port> -> <dst> <port> (msg:""; <options>; sid:N; rev:N;)
```
### Key Keywords
| Keyword | Purpose |
|---------|---------|
| `http.method` | Match HTTP method |
| `http.uri` | Match request URI |
| `http.header` | Match header content |
| `ja3.hash` | Match JA3 TLS fingerprint |
| `dns.query` | Match DNS query name |
| `tls.cert_subject` | Match TLS certificate CN |
| `threshold` | Rate-based detection |
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""C2 communication analysis agent for beacon detection and protocol decoding."""
import statistics
import base64
import json
import os
import sys
from collections import defaultdict
try:
from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, Raw
HAS_SCAPY = True
except ImportError:
HAS_SCAPY = False
try:
import dpkt
HAS_DPKT = True
except ImportError:
HAS_DPKT = False
def detect_beacons(pcap_path, min_connections=5, max_jitter_pct=25.0):
"""Analyze PCAP for periodic beacon patterns using TCP SYN timing."""
if not HAS_SCAPY:
print("[ERROR] scapy not installed: pip install scapy")
return []
packets = rdpcap(pcap_path)
connections = defaultdict(list)
for pkt in packets:
if IP in pkt and TCP in pkt and (pkt[TCP].flags & 0x02):
key = f"{pkt[IP].dst}:{pkt[TCP].dport}"
connections[key].append(float(pkt.time))
beacons = []
for dst, times in sorted(connections.items()):
if len(times) < min_connections:
continue
intervals = [times[i + 1] - times[i] for i in range(len(times) - 1)]
avg_interval = statistics.mean(intervals)
stdev = statistics.stdev(intervals) if len(intervals) > 1 else 0
jitter_pct = (stdev / avg_interval * 100) if avg_interval > 0 else 0
is_beacon = 5 < avg_interval < 7200 and jitter_pct < max_jitter_pct
record = {
"destination": dst,
"connections": len(times),
"duration_seconds": round(times[-1] - times[0], 1),
"avg_interval_seconds": round(avg_interval, 1),
"stdev_seconds": round(stdev, 1),
"jitter_percent": round(jitter_pct, 1),
"is_beacon": is_beacon,
}
if is_beacon:
beacons.append(record)
return beacons
def extract_http_requests(pcap_path):
"""Extract HTTP requests from a PCAP file using dpkt."""
if not HAS_DPKT:
print("[ERROR] dpkt not installed: pip install dpkt")
return []
requests = []
with open(pcap_path, "rb") as f:
pcap = dpkt.pcap.Reader(f)
for ts, buf in pcap:
try:
eth = dpkt.ethernet.Ethernet(buf)
if not isinstance(eth.data, dpkt.ip.IP):
continue
ip = eth.data
if not isinstance(ip.data, dpkt.tcp.TCP):
continue
tcp = ip.data
if len(tcp.data) == 0:
continue
try:
http = dpkt.http.Request(tcp.data)
decoded_body = None
if http.body:
try:
decoded_body = base64.b64decode(http.body).decode("utf-8", errors="replace")
except Exception:
decoded_body = http.body[:200]
requests.append({
"timestamp": ts,
"src_ip": ".".join(str(b) for b in ip.src),
"dst_ip": ".".join(str(b) for b in ip.dst),
"dst_port": tcp.dport,
"method": http.method,
"uri": http.uri,
"host": http.headers.get("host", ""),
"user_agent": http.headers.get("user-agent", ""),
"body_size": len(http.body) if http.body else 0,
"decoded_body_preview": decoded_body,
})
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
pass
except Exception:
continue
return requests
def extract_dns_queries(pcap_path):
"""Extract DNS queries from a PCAP for C2 domain identification."""
if not HAS_SCAPY:
return []
packets = rdpcap(pcap_path)
queries = []
for pkt in packets:
if DNS in pkt and pkt[DNS].qr == 0 and DNSQR in pkt:
qname = pkt[DNSQR].qname.decode("utf-8", errors="replace").rstrip(".")
queries.append({
"src_ip": pkt[IP].src if IP in pkt else "?",
"query": qname,
"type": pkt[DNSQR].qtype,
})
return queries
def identify_c2_framework(http_requests):
"""Match HTTP request patterns against known C2 framework signatures."""
cs_uris = ["/pixel", "/submit.php", "/__utm.gif", "/ca", "/dpixel",
"/push", "/visit.js", "/tab_icon"]
framework_hits = []
for req in http_requests:
uri = req.get("uri", "")
ua = req.get("user_agent", "")
for cs_uri in cs_uris:
if cs_uri in uri:
framework_hits.append({
"framework": "Cobalt Strike",
"indicator": f"URI pattern: {cs_uri}",
"request": req,
})
break
if "MeterSSL" in ua or len(uri) == 5 and uri.startswith("/"):
framework_hits.append({
"framework": "Metasploit/Meterpreter",
"indicator": f"URI/UA pattern: {uri} / {ua[:50]}",
"request": req,
})
return framework_hits
def generate_suricata_rules(beacons, http_requests):
"""Generate Suricata IDS rules from observed C2 patterns."""
rules = []
sid = 9000100
for beacon in beacons:
dst_ip, dst_port = beacon["destination"].rsplit(":", 1)
rules.append(
f'alert tcp $HOME_NET any -> {dst_ip} {dst_port} ('
f'msg:"MALWARE Detected C2 Beacon to {dst_ip}:{dst_port}"; '
f'flow:established,to_server; '
f'threshold:type threshold, track by_src, count 5, seconds 600; '
f'sid:{sid}; rev:1;)'
)
sid += 1
for req in http_requests[:5]:
if req.get("uri"):
uri = req["uri"]
rules.append(
f'alert http $HOME_NET any -> $EXTERNAL_NET any ('
f'msg:"MALWARE Suspected C2 HTTP Request {uri}"; '
f'flow:established,to_server; '
f'http.method; content:"{req["method"]}"; '
f'http.uri; content:"{uri}"; '
f'sid:{sid}; rev:1;)'
)
sid += 1
return rules
if __name__ == "__main__":
print("=" * 60)
print("C2 Communication Analysis Agent")
print("Beacon detection, protocol decoding, signature generation")
print("=" * 60)
pcap_file = sys.argv[1] if len(sys.argv) > 1 else None
if pcap_file and os.path.exists(pcap_file):
print(f"\n[*] Analyzing PCAP: {pcap_file}")
print("\n--- Beacon Detection ---")
beacons = detect_beacons(pcap_file)
for b in beacons:
print(f"[!] BEACON: {b['destination']} "
f"interval={b['avg_interval_seconds']}s "
f"jitter={b['jitter_percent']}% "
f"sessions={b['connections']}")
print("\n--- HTTP Requests ---")
http_reqs = extract_http_requests(pcap_file)
for r in http_reqs[:10]:
print(f" {r['method']} {r['host']}{r['uri']}")
print("\n--- DNS Queries ---")
dns_qs = extract_dns_queries(pcap_file)
for q in dns_qs[:10]:
print(f" {q['src_ip']} -> {q['query']}")
print("\n--- C2 Framework Identification ---")
hits = identify_c2_framework(http_reqs)
for h in hits:
print(f"[!] {h['framework']}: {h['indicator']}")
print("\n--- Suricata Rules ---")
rules = generate_suricata_rules(beacons, http_reqs)
for r in rules:
print(r)
else:
print("\n[DEMO] Usage: python agent.py <capture.pcap>")
print("[*] Provide a PCAP file to analyze for C2 communication patterns.")
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,96 @@
# API Reference: Cyber Kill Chain Analysis Tools
## Lockheed Martin Cyber Kill Chain Phases
| Phase | Name | MITRE ATT&CK Tactic |
|-------|------|---------------------|
| 1 | Reconnaissance | TA0043 Reconnaissance |
| 2 | Weaponization | TA0042 Resource Development |
| 3 | Delivery | TA0001 Initial Access |
| 4 | Exploitation | TA0002 Execution |
| 5 | Installation | TA0003 Persistence, TA0004 Privilege Escalation |
| 6 | Command & Control | TA0011 Command and Control |
| 7 | Actions on Objectives | TA0010 Exfiltration, TA0040 Impact |
## Courses of Action (COA) Matrix
| COA | Description |
|-----|-------------|
| Detect | Alert on adversary activity |
| Deny | Prevent phase completion |
| Disrupt | Interrupt adversary mid-phase |
| Degrade | Reduce adversary effectiveness |
| Deceive | Expose activity via deception |
| Destroy | Neutralize adversary infrastructure |
## MITRE ATT&CK Navigator
### JSON Layer Format
```json
{
"name": "Kill Chain Coverage",
"versions": {"navigator": "4.8", "layer": "4.4", "attack": "13"},
"domain": "enterprise-attack",
"techniques": [
{"techniqueID": "T1566", "color": "#ff6666", "comment": "Phase 3: Delivery"}
]
}
```
### CLI Usage
```bash
# Export layer via ATT&CK Navigator API
curl -X POST https://mitre-attack.github.io/attack-navigator/api/layers \
-d @layer.json -o coverage_map.svg
```
## Splunk - Kill Chain Phase Queries
### Phase 3 Detection (Delivery)
```spl
index=email sourcetype=exchange action=delivered
| eval has_macro=if(match(attachment, "\.(docm|xlsm|pptm)$"), 1, 0)
| where has_macro=1
| stats count by sender, subject, attachment
```
### Phase 6 Detection (C2)
```spl
index=proxy OR index=firewall
| stats count AS connections, dc(dest) AS unique_dests by src_ip
| where connections > 100 AND unique_dests < 3
| sort - connections
```
## Elastic Security EQL
### Multi-Phase Detection
```eql
sequence by host.name with maxspan=1h
[process where event.action == "start" and process.name == "WINWORD.EXE"]
[process where event.action == "start" and process.parent.name == "WINWORD.EXE"]
[network where destination.port == 443 and not destination.ip in ("known_good")]
```
## MISP - Kill Chain Tagging
### Galaxy Cluster Tags
```
misp-galaxy:kill-chain="reconnaissance"
misp-galaxy:kill-chain="delivery"
misp-galaxy:kill-chain="exploitation"
misp-galaxy:kill-chain="installation"
misp-galaxy:kill-chain="command-and-control"
misp-galaxy:kill-chain="actions-on-objectives"
```
### PyMISP Event Tagging
```python
from pymisp import PyMISP, MISPEvent
misp = PyMISP("https://misp.example.com", "API_KEY")
event = MISPEvent()
event.add_tag("kill-chain:delivery")
event.add_tag("mitre-attack-pattern:T1566 - Phishing")
misp.update_event(event)
```
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Cyber Kill Chain analysis agent for mapping incidents to Lockheed Martin kill chain phases."""
import json
import os
import sys
import datetime
KILL_CHAIN_PHASES = {
1: {
"name": "Reconnaissance",
"description": "Adversary gathers target information",
"indicators": [
"DNS queries from adversary IP",
"LinkedIn/social media scraping",
"Shodan/Censys scans of infrastructure",
"Job posting analysis for technology stack",
"WHOIS lookups on organization domains",
],
"mitre_tactics": ["TA0043 - Reconnaissance"],
"coas": {
"detect": "Monitor for anomalous DNS lookups and port scans from single sources",
"deny": "Limit public-facing information, restrict DNS zone transfers",
"disrupt": "Block scanning IPs at perimeter firewall",
"degrade": "Return honeypot responses to recon probes",
"deceive": "Deploy decoy infrastructure and fake employee profiles",
},
},
2: {
"name": "Weaponization",
"description": "Adversary creates attack tool (malware + exploit)",
"indicators": [
"Malware compilation timestamps",
"Exploit document metadata",
"Builder tool artifacts in samples",
"Reused infrastructure from previous campaigns",
],
"mitre_tactics": ["TA0042 - Resource Development"],
"coas": {
"detect": "Threat intelligence on adversary tooling and TTPs",
"deny": "Patch vulnerabilities targeted by known exploit kits",
"disrupt": "N/A (occurs outside defender visibility)",
"degrade": "Application hardening reduces exploit reliability",
"deceive": "Share deceptive vulnerability information",
},
},
3: {
"name": "Delivery",
"description": "Adversary transmits weapon to target",
"indicators": [
"Phishing emails with malicious attachments",
"Drive-by download URLs",
"USB device insertion events",
"Supply chain compromise artifacts",
"Watering hole website modifications",
],
"mitre_tactics": ["TA0001 - Initial Access"],
"coas": {
"detect": "Email security gateway alerts, proxy URL filtering alerts",
"deny": "Block malicious attachments, URL filtering, USB device control",
"disrupt": "Quarantine suspicious emails before delivery",
"degrade": "Sandbox detonation of attachments delays delivery",
"deceive": "Canary documents in email attachments",
},
},
4: {
"name": "Exploitation",
"description": "Adversary exploits vulnerability to execute code",
"indicators": [
"CVE exploitation in application logs",
"Memory corruption crash dumps",
"Shellcode execution artifacts",
"Exploit kit landing page access",
],
"mitre_tactics": ["TA0002 - Execution"],
"coas": {
"detect": "EDR behavioral detection, exploit guard alerts",
"deny": "Patch management, application whitelisting",
"disrupt": "ASLR, DEP, CFG memory protections",
"degrade": "Sandboxed application execution (Protected View)",
"deceive": "Honeypot applications with fake vulnerabilities",
},
},
5: {
"name": "Installation",
"description": "Adversary establishes persistence on target",
"indicators": [
"New scheduled tasks or services",
"Registry Run key modifications",
"Web shell deployment",
"Startup folder additions",
"DLL search-order hijacking",
],
"mitre_tactics": ["TA0003 - Persistence", "TA0004 - Privilege Escalation"],
"coas": {
"detect": "Sysmon EventID 11/12/13, EDR persistence monitoring",
"deny": "Application whitelisting, UAC enforcement",
"disrupt": "Real-time file integrity monitoring alerts",
"degrade": "Restrict write access to system directories",
"deceive": "Canary registry keys and file system canaries",
},
},
6: {
"name": "Command & Control",
"description": "Adversary communicates with compromised system",
"indicators": [
"Beaconing traffic at regular intervals",
"DNS tunneling (high entropy subdomain queries)",
"HTTPS to newly registered domains",
"Known C2 framework signatures",
],
"mitre_tactics": ["TA0011 - Command and Control"],
"coas": {
"detect": "Network beacon analysis, JA3 fingerprinting, DNS monitoring",
"deny": "DNS sinkholing, firewall egress filtering",
"disrupt": "TLS inspection to identify C2 in encrypted traffic",
"degrade": "Rate-limit suspicious outbound connections",
"deceive": "C2 interception and response manipulation",
},
},
7: {
"name": "Actions on Objectives",
"description": "Adversary achieves mission goals",
"indicators": [
"Data staging and exfiltration",
"Lateral movement to additional systems",
"Ransomware encryption activity",
"Destructive operations (wiper malware)",
"Credential dumping (LSASS access)",
],
"mitre_tactics": ["TA0010 - Exfiltration", "TA0040 - Impact"],
"coas": {
"detect": "DLP alerts, anomalous data transfers, UEBA",
"deny": "Network segmentation, data classification controls",
"disrupt": "Isolate compromised systems, kill C2 connections",
"degrade": "Encrypt sensitive data at rest (attacker gets ciphertext)",
"deceive": "Canary files and honeytoken credentials",
},
},
}
def map_event_to_phase(event_description):
"""Map an incident event description to the most likely kill chain phase."""
event_lower = event_description.lower()
keyword_phase_map = {
1: ["recon", "scan", "enumerat", "shodan", "whois", "dns lookup"],
2: ["weaponiz", "builder", "compile", "payload creat"],
3: ["phish", "email", "deliver", "download", "usb", "attachment", "watering hole"],
4: ["exploit", "cve-", "buffer overflow", "shellcode", "rce"],
5: ["persist", "scheduled task", "registry", "run key", "service install",
"web shell", "backdoor", "startup"],
6: ["beacon", "c2", "c&c", "command and control", "callback", "dns tunnel"],
7: ["exfiltrat", "lateral", "ransomware", "encrypt", "data stag", "credential dump",
"mimikatz", "wiper"],
}
scores = {phase: 0 for phase in range(1, 8)}
for phase, keywords in keyword_phase_map.items():
for kw in keywords:
if kw in event_lower:
scores[phase] += 1
best_phase = max(scores, key=scores.get)
if scores[best_phase] == 0:
return None
return best_phase
def analyze_incident(events):
"""Analyze a list of incident events and map to kill chain phases."""
analysis = {phase: {"events": [], "detected": False, "completed": False}
for phase in range(1, 8)}
for event in events:
phase = map_event_to_phase(event.get("description", ""))
if phase:
analysis[phase]["events"].append(event)
analysis[phase]["completed"] = True
if event.get("detected", False):
analysis[phase]["detected"] = True
return analysis
def generate_report(analysis):
"""Generate a kill chain analysis report."""
report_lines = [
"CYBER KILL CHAIN ANALYSIS REPORT",
"=" * 50,
f"Generated: {datetime.datetime.utcnow().isoformat()}Z",
"",
]
deepest_phase = 0
detection_phase = None
for phase_num in range(1, 8):
phase_data = analysis[phase_num]
phase_info = KILL_CHAIN_PHASES[phase_num]
if phase_data["completed"]:
deepest_phase = phase_num
if phase_data["detected"] and detection_phase is None:
detection_phase = phase_num
status = "COMPLETED" if phase_data["completed"] else "NOT REACHED"
if phase_data["detected"]:
status += " (DETECTED)"
report_lines.append(f"Phase {phase_num}: {phase_info['name']} -> {status}")
for evt in phase_data["events"]:
report_lines.append(f" - {evt.get('description', 'N/A')}")
report_lines.extend([
"",
f"Deepest phase reached: {deepest_phase} ({KILL_CHAIN_PHASES.get(deepest_phase, {}).get('name', 'N/A')})",
f"First detection at phase: {detection_phase or 'None'}",
"",
"RECOMMENDED COURSES OF ACTION:",
])
for phase_num in range(1, deepest_phase + 1):
phase_info = KILL_CHAIN_PHASES[phase_num]
report_lines.append(f"\n Phase {phase_num} - {phase_info['name']}:")
for coa_type, coa_desc in phase_info["coas"].items():
report_lines.append(f" {coa_type.upper()}: {coa_desc}")
return "\n".join(report_lines)
if __name__ == "__main__":
print("=" * 60)
print("Cyber Kill Chain Analysis Agent")
print("Lockheed Martin framework mapping with MITRE ATT&CK integration")
print("=" * 60)
# Demo incident events
demo_events = [
{"description": "Shodan scans detected from 203.0.113.50 targeting web servers",
"timestamp": "2025-09-10T08:00:00Z", "detected": False},
{"description": "Phishing email with malicious .docm attachment delivered to 5 users",
"timestamp": "2025-09-11T09:15:00Z", "detected": False},
{"description": "CVE-2023-23397 exploitation detected in Outlook process crash",
"timestamp": "2025-09-11T09:20:00Z", "detected": False},
{"description": "Scheduled task created for persistence by malware dropper",
"timestamp": "2025-09-11T09:25:00Z", "detected": True},
{"description": "C2 beacon detected to 185.220.101.42 on port 443",
"timestamp": "2025-09-11T09:30:00Z", "detected": True},
]
print("\n[*] Analyzing demo incident events...")
analysis = analyze_incident(demo_events)
report = generate_report(analysis)
print(f"\n{report}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,118 @@
# API Reference: Autopsy and The Sleuth Kit (TSK)
## mmls - Partition Layout
### Syntax
```bash
mmls <image_file>
mmls -t dos <image_file> # Force DOS partition table
mmls -t gpt <image_file> # Force GPT partition table
```
### Output Format
```
DOS Partition Table
Offset Sector: 0
Slot Start End Length Description
00: 00:00 0000002048 0001026047 0001024000 NTFS (0x07)
```
## fls - File Listing
### Syntax
```bash
fls -o <offset> <image> # List root directory
fls -r -o <offset> <image> # Recursive listing
fls -rd -o <offset> <image> # Deleted files only, recursive
fls -m "/" -r -o <offset> <image> # Bodyfile format for mactime
```
### Flags
| Flag | Description |
|------|-------------|
| `-r` | Recursive listing |
| `-d` | Deleted entries only |
| `-D` | Directories only |
| `-m "/"` | Output in bodyfile format with mount point |
| `-o` | Partition sector offset |
## icat - File Extraction by Inode
### Syntax
```bash
icat -o <offset> <image> <inode> > recovered_file
icat -r -o <offset> <image> <inode> > file # Recover slack space
```
## istat - File Metadata
### Syntax
```bash
istat -o <offset> <image> <inode>
```
### Output Includes
- MFT entry number and sequence
- File creation, modification, access, MFT change timestamps
- File size and data run locations
- Attribute list (NTFS: $STANDARD_INFORMATION, $FILE_NAME, $DATA)
## mactime - Timeline Generation
### Syntax
```bash
mactime -b <bodyfile> -d > timeline.csv
mactime -b <bodyfile> -d 2024-01-15..2024-01-20 > filtered.csv
mactime -b <bodyfile> -z UTC -d > timeline_utc.csv
```
### Output Columns
```
Date,Size,Type,Mode,UID,GID,Meta,File Name
```
## img_stat - Image Information
### Syntax
```bash
img_stat <image_file>
```
## sigfind - File Signature Search
### Syntax
```bash
sigfind -o <offset> <image> <hex_signature>
sigfind -o 2048 evidence.dd 25504446 # Find %PDF headers
sigfind -o 2048 evidence.dd 504B0304 # Find ZIP/DOCX headers
```
### Common Signatures
| Hex | File Type |
|-----|-----------|
| `FFD8FF` | JPEG |
| `89504E47` | PNG |
| `25504446` | PDF |
| `504B0304` | ZIP/DOCX/XLSX |
| `D0CF11E0` | OLE (DOC/XLS) |
## srch_strings - Keyword Search
### Syntax
```bash
srch_strings -a -o <offset> <image> | grep -i "keyword"
srch_strings -t d <image> # Print offset in decimal
```
## Autopsy GUI Ingest Modules
| Module | Function |
|--------|----------|
| Recent Activity | Browser history, downloads, cookies |
| Hash Lookup | NSRL and known-bad hash matching |
| File Type Identification | Signature-based file type detection |
| Keyword Search | Full-text content indexing |
| Email Parser | PST/MBOX/EML extraction |
| Extension Mismatch | Wrong file extension detection |
| Embedded File Extractor | ZIP, Office, PDF extraction |
| Encryption Detection | Encrypted container identification |
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Forensic disk image analysis agent using The Sleuth Kit (TSK) command-line tools."""
import subprocess
import os
import sys
import json
import csv
import datetime
def run_cmd(cmd):
"""Execute a shell command and return output."""
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def get_image_info(image_path):
"""Retrieve disk image metadata using img_stat."""
stdout, _, rc = run_cmd(f"img_stat {image_path}")
if rc == 0:
info = {}
for line in stdout.splitlines():
if ":" in line:
key, _, val = line.partition(":")
info[key.strip()] = val.strip()
return info
return None
def list_partitions(image_path):
"""List partition layout using mmls."""
stdout, _, rc = run_cmd(f"mmls {image_path}")
partitions = []
if rc == 0:
for line in stdout.splitlines():
parts = line.split()
if len(parts) >= 6 and parts[2].isdigit():
partitions.append({
"slot": parts[0].rstrip(":"),
"start": int(parts[2]),
"end": int(parts[3]),
"length": int(parts[4]),
"description": " ".join(parts[5:]),
})
return partitions
def list_files(image_path, offset, path="/", recursive=False):
"""List files in a partition using fls."""
flags = "-r" if recursive else ""
cmd = f"fls {flags} -o {offset} {image_path}"
if path != "/":
cmd += f" -D {path}"
stdout, _, rc = run_cmd(cmd)
files = []
if rc == 0:
for line in stdout.splitlines():
line = line.strip()
if not line:
continue
parts = line.split("\t", 1)
if len(parts) == 2:
meta = parts[0].strip()
name = parts[1].strip()
deleted = meta.startswith("*")
file_type = "d" if "d/" in meta else "r"
inode = ""
for token in meta.split():
if "-" in token and token.replace("-", "").isdigit():
inode = token
break
files.append({
"name": name,
"inode": inode,
"type": "directory" if file_type == "d" else "file",
"deleted": deleted,
})
return files
def list_deleted_files(image_path, offset):
"""List only deleted files using fls -rd."""
stdout, _, rc = run_cmd(f"fls -rd -o {offset} {image_path}")
deleted = []
if rc == 0:
for line in stdout.splitlines():
line = line.strip()
if line:
deleted.append(line)
return deleted
def recover_file(image_path, offset, inode, output_path):
"""Recover a file by inode using icat."""
cmd = f"icat -o {offset} {image_path} {inode} > {output_path}"
_, _, rc = run_cmd(cmd)
return rc == 0
def get_file_metadata(image_path, offset, inode):
"""Get detailed file metadata using istat."""
stdout, _, rc = run_cmd(f"istat -o {offset} {image_path} {inode}")
return stdout if rc == 0 else None
def create_bodyfile(image_path, offset, output_path):
"""Generate a TSK bodyfile for timeline creation."""
cmd = f'fls -r -m "/" -o {offset} {image_path} > {output_path}'
_, _, rc = run_cmd(cmd)
return rc == 0
def generate_timeline(bodyfile_path, output_csv, start_date=None, end_date=None):
"""Generate a timeline from a bodyfile using mactime."""
cmd = f"mactime -b {bodyfile_path} -d"
if start_date and end_date:
cmd += f" {start_date}..{end_date}"
cmd += f" > {output_csv}"
_, _, rc = run_cmd(cmd)
return rc == 0
def search_keywords(image_path, offset, keyword):
"""Search for keyword strings in the disk image."""
cmd = f'srch_strings -a -o {offset} {image_path} | grep -i "{keyword}"'
stdout, _, rc = run_cmd(cmd)
return stdout.splitlines() if rc == 0 else []
def find_file_signature(image_path, offset, hex_signature):
"""Find file signatures at the sector level using sigfind."""
stdout, _, rc = run_cmd(f"sigfind -o {offset} {image_path} {hex_signature}")
return stdout if rc == 0 else None
def analyze_image(image_path, case_dir):
"""Run a full automated analysis workflow on a disk image."""
os.makedirs(case_dir, exist_ok=True)
results = {"image": image_path, "timestamp": datetime.datetime.utcnow().isoformat()}
print(f"[*] Image info...")
results["image_info"] = get_image_info(image_path)
print(f"[*] Partition layout...")
partitions = list_partitions(image_path)
results["partitions"] = partitions
for part in partitions:
if "NTFS" in part.get("description", "") or "Linux" in part.get("description", ""):
offset = part["start"]
print(f"[*] Listing files at offset {offset} ({part['description']})...")
files = list_files(image_path, offset, recursive=True)
results[f"files_offset_{offset}"] = {
"total": len(files),
"deleted": sum(1 for f in files if f["deleted"]),
}
print(f" Total: {len(files)}, Deleted: {results[f'files_offset_{offset}']['deleted']}")
print(f"[*] Creating bodyfile for timeline...")
bf_path = os.path.join(case_dir, f"bodyfile_{offset}.txt")
create_bodyfile(image_path, offset, bf_path)
tl_path = os.path.join(case_dir, f"timeline_{offset}.csv")
generate_timeline(bf_path, tl_path)
report_path = os.path.join(case_dir, "analysis_summary.json")
with open(report_path, "w") as f:
json.dump(results, f, indent=2, default=str)
print(f"[*] Summary saved to {report_path}")
return results
if __name__ == "__main__":
print("=" * 60)
print("Disk Image Forensic Analysis Agent")
print("Tools: The Sleuth Kit (fls, icat, mmls, mactime)")
print("=" * 60)
if len(sys.argv) > 1:
image = sys.argv[1]
case = sys.argv[2] if len(sys.argv) > 2 else "/tmp/autopsy_case"
if os.path.exists(image):
analyze_image(image, case)
else:
print(f"[ERROR] Image not found: {image}")
else:
print("\n[DEMO] Usage: python agent.py <disk_image.dd> [case_directory]")
print("[*] Supported operations:")
print(" - Partition enumeration (mmls)")
print(" - File listing with deleted file recovery (fls, icat)")
print(" - Timeline generation (mactime)")
print(" - Keyword searching (srch_strings)")
print(" - File signature detection (sigfind)")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,112 @@
# API Reference: DNS Exfiltration Detection Tools
## Shannon Entropy Calculation
### Python Implementation
```python
import math
from collections import Counter
def shannon_entropy(text):
counter = Counter(text.lower())
length = len(text)
return -sum((c/length) * math.log2(c/length) for c in counter.values())
```
### Threshold Values
| Entropy | Classification |
|---------|---------------|
| < 2.5 | Normal domain (e.g., "google") |
| 2.5 - 3.5 | Borderline (monitor) |
| > 3.5 | Suspicious (likely DGA/tunneling) |
| > 4.0 | High confidence malicious |
## Splunk DNS Queries
### Tunneling Detection
```spl
index=dns sourcetype="stream:dns"
| eval subdomain_len=len(mvindex(split(query,"."),0))
| where subdomain_len > 50
| stats count by registered_domain, src_ip
```
### DGA Detection
```spl
index=dns
| eval sld=mvindex(split(query,"."), -2)
| where len(sld) > 12
| stats count, dc(query) AS unique by src_ip
```
### Volume Anomaly
```spl
index=dns earliest=-24h
| bin _time span=1h
| stats count AS queries by src_ip, _time
| eventstats avg(queries) AS avg_q, stdev(queries) AS stdev_q by src_ip
| eval z_score=(queries - avg_q) / stdev_q
| where z_score > 3
```
### TXT Record Abuse
```spl
index=dns query_type="TXT"
| stats count AS txt_queries by src_ip
| where txt_queries > 100
```
## Zeek DNS Log Format
### Log Fields (dns.log)
| Column | Field | Description |
|--------|-------|-------------|
| 0 | ts | Timestamp |
| 2 | id.orig_h | Source IP |
| 4 | id.resp_h | DNS server IP |
| 9 | query | Query domain name |
| 13 | qtype_name | Query type (A, TXT, CNAME) |
| 15 | rcode_name | Response code |
| 21 | answers | Response answers |
### Zeek CLI Analysis
```bash
cat dns.log | zeek-cut query qtype_name id.orig_h | sort | uniq -c | sort -rn
```
## DNS Tunneling Tools (Detection Signatures)
| Tool | DNS Pattern |
|------|-------------|
| iodine | `*.pirate.sea` (TXT/NULL records) |
| dnscat2 | `*.dnscat.` prefix in queries |
| dns2tcp | `*.dns2tcp.` pattern |
| Cobalt Strike DNS | Periodic TXT queries with encoded payloads |
## Passive DNS Lookup APIs
### Farsight DNSDB
```bash
curl -H "X-API-Key: $KEY" \
"https://api.dnsdb.info/dnsdb/v2/lookup/rrset/name/evil.com/A"
```
### VirusTotal Domain Resolutions
```bash
curl -H "x-apikey: $KEY" \
"https://www.virustotal.com/api/v3/domains/evil.com/resolutions"
```
## Cisco Umbrella (OpenDNS) Investigate API
### Domain Categorization
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://investigate.api.umbrella.com/domains/categorization/evil.com"
```
### Security Information
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://investigate.api.umbrella.com/security/name/evil.com"
```
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""DNS exfiltration detection agent using entropy analysis and query pattern detection."""
import math
import os
import sys
import json
import csv
import datetime
from collections import Counter, defaultdict
def shannon_entropy(text):
"""Calculate Shannon entropy of a string."""
if not text:
return 0.0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
def extract_subdomain(fqdn):
"""Extract the subdomain portion from a fully qualified domain name."""
parts = fqdn.rstrip(".").split(".")
if len(parts) > 2:
return ".".join(parts[:-2])
return ""
def extract_registered_domain(fqdn):
"""Extract the registered domain (SLD + TLD) from an FQDN."""
parts = fqdn.rstrip(".").split(".")
if len(parts) >= 2:
return ".".join(parts[-2:])
return fqdn
def detect_tunneling(dns_records, subdomain_len_threshold=50, min_queries=20):
"""Detect DNS tunneling based on subdomain length anomalies."""
domain_stats = defaultdict(lambda: {"queries": 0, "unique_queries": set(),
"subdomain_lengths": [], "sources": set()})
for record in dns_records:
query = record.get("query", "")
src = record.get("src_ip", "unknown")
subdomain = extract_subdomain(query)
reg_domain = extract_registered_domain(query)
if len(subdomain) > subdomain_len_threshold:
stats = domain_stats[reg_domain]
stats["queries"] += 1
stats["unique_queries"].add(query)
stats["subdomain_lengths"].append(len(subdomain))
stats["sources"].add(src)
alerts = []
for domain, stats in domain_stats.items():
if stats["queries"] >= min_queries:
avg_len = sum(stats["subdomain_lengths"]) / len(stats["subdomain_lengths"])
max_len = max(stats["subdomain_lengths"])
alerts.append({
"domain": domain,
"queries": stats["queries"],
"unique_queries": len(stats["unique_queries"]),
"avg_subdomain_length": round(avg_len, 1),
"max_subdomain_length": max_len,
"sources": list(stats["sources"]),
"verdict": "CRITICAL - Likely DNS tunneling",
})
return sorted(alerts, key=lambda x: x["avg_subdomain_length"], reverse=True)
def detect_dga(dns_records, entropy_threshold=3.5, min_sld_length=12):
"""Detect Domain Generation Algorithm queries using entropy scoring."""
suspicious = defaultdict(lambda: {"count": 0, "sources": set(), "entropies": []})
for record in dns_records:
query = record.get("query", "").rstrip(".")
src = record.get("src_ip", "unknown")
parts = query.split(".")
if len(parts) < 2:
continue
sld = parts[-2]
if len(sld) < min_sld_length:
continue
ent = shannon_entropy(sld)
if ent > entropy_threshold:
suspicious[query]["count"] += 1
suspicious[query]["sources"].add(src)
suspicious[query]["entropies"].append(ent)
alerts = []
for domain, data in suspicious.items():
avg_entropy = sum(data["entropies"]) / len(data["entropies"])
alerts.append({
"domain": domain,
"queries": data["count"],
"avg_entropy": round(avg_entropy, 4),
"sources": list(data["sources"]),
"verdict": "HIGH - Possible DGA domain",
})
return sorted(alerts, key=lambda x: x["avg_entropy"], reverse=True)
def detect_volume_anomaly(dns_records, z_score_threshold=3.0):
"""Detect hosts with anomalously high DNS query volumes."""
host_counts = defaultdict(int)
for record in dns_records:
src = record.get("src_ip", "unknown")
host_counts[src] += 1
if not host_counts:
return []
values = list(host_counts.values())
mean_q = sum(values) / len(values)
if len(values) < 2:
return []
variance = sum((x - mean_q) ** 2 for x in values) / (len(values) - 1)
stdev_q = variance ** 0.5
if stdev_q == 0:
return []
anomalies = []
for host, count in host_counts.items():
z = (count - mean_q) / stdev_q
if z > z_score_threshold:
anomalies.append({
"src_ip": host,
"queries": count,
"z_score": round(z, 2),
"mean": round(mean_q, 1),
"verdict": "HIGH - Anomalous query volume",
})
return sorted(anomalies, key=lambda x: x["z_score"], reverse=True)
def detect_txt_abuse(dns_records, threshold=100):
"""Detect excessive TXT record queries (common tunneling method)."""
txt_counts = defaultdict(lambda: {"count": 0, "unique_domains": set()})
for record in dns_records:
qtype = str(record.get("query_type", "")).upper()
if qtype in ("TXT", "16"):
src = record.get("src_ip", "unknown")
txt_counts[src]["count"] += 1
txt_counts[src]["unique_domains"].add(record.get("query", ""))
alerts = []
for src, data in txt_counts.items():
if data["count"] > threshold:
level = "CRITICAL" if data["count"] > 1000 else "HIGH" if data["count"] > 500 else "MEDIUM"
alerts.append({
"src_ip": src,
"txt_queries": data["count"],
"unique_domains": len(data["unique_domains"]),
"verdict": f"{level} - Possible DNS tunneling via TXT records",
})
return sorted(alerts, key=lambda x: x["txt_queries"], reverse=True)
def estimate_exfil_volume(dns_records, target_domain):
"""Estimate data volume encoded in DNS queries to a specific domain."""
total_encoded_bytes = 0
query_count = 0
for record in dns_records:
query = record.get("query", "")
if target_domain in query:
subdomain = extract_subdomain(query)
total_encoded_bytes += len(subdomain)
query_count += 1
decoded_bytes = int(total_encoded_bytes * 0.75) # Base64 decode factor
return {
"target_domain": target_domain,
"total_queries": query_count,
"encoded_bytes": total_encoded_bytes,
"estimated_decoded_bytes": decoded_bytes,
"estimated_kb": round(decoded_bytes / 1024, 1),
"estimated_mb": round(decoded_bytes / (1024 * 1024), 3),
}
def parse_zeek_dns_log(log_path):
"""Parse a Zeek dns.log file into structured records."""
records = []
with open(log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
parts = line.strip().split("\t")
if len(parts) >= 10:
records.append({
"timestamp": parts[0],
"src_ip": parts[2],
"src_port": parts[3],
"dst_ip": parts[4],
"query": parts[9] if len(parts) > 9 else "",
"query_type": parts[13] if len(parts) > 13 else "",
})
return records
if __name__ == "__main__":
print("=" * 60)
print("DNS Exfiltration Detection Agent")
print("Tunneling, DGA, volume anomaly, and TXT abuse detection")
print("=" * 60)
# Demo with synthetic DNS records
demo_records = [
{"query": f"{'a' * 60}.evil-tunnel.com", "src_ip": "192.168.1.105",
"query_type": "TXT"} for _ in range(50)
] + [
{"query": "x8kj2m9p4qw7nz3.xyz", "src_ip": "192.168.1.110",
"query_type": "A"} for _ in range(5)
] + [
{"query": "google.com", "src_ip": "192.168.1.50", "query_type": "A"}
for _ in range(10)
]
print("\n--- DNS Tunneling Detection ---")
tunneling = detect_tunneling(demo_records, subdomain_len_threshold=30, min_queries=10)
for t in tunneling:
print(f"[!] {t['domain']}: {t['queries']} queries, "
f"avg subdomain len={t['avg_subdomain_length']}")
print("\n--- DGA Detection ---")
dga = detect_dga(demo_records, entropy_threshold=3.0, min_sld_length=10)
for d in dga[:5]:
print(f"[!] {d['domain']}: entropy={d['avg_entropy']}")
print("\n--- TXT Record Abuse ---")
txt = detect_txt_abuse(demo_records, threshold=10)
for t in txt:
print(f"[!] {t['src_ip']}: {t['txt_queries']} TXT queries")
print("\n--- Entropy Examples ---")
examples = ["google", "x8kj2m9p4qw7n", "aGVsbG8gd29ybGQ"]
for ex in examples:
print(f" '{ex}' -> entropy={shannon_entropy(ex)}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,116 @@
# API Reference: Docker Container Forensics Tools
## docker inspect - Container Details
### Syntax
```bash
docker inspect <container_id>
docker inspect --format '{{.HostConfig.Privileged}}' <container_id>
docker inspect --format '{{json .Mounts}}' <container_id> | jq
docker inspect --format '{{.GraphDriver.Data.MergedDir}}' <container_id>
```
### Key JSON Paths
| Path | Description |
|------|-------------|
| `.HostConfig.Privileged` | Privileged mode status |
| `.HostConfig.CapAdd` | Added capabilities |
| `.HostConfig.PidMode` | PID namespace mode |
| `.HostConfig.NetworkMode` | Network namespace mode |
| `.Mounts` | Volume mount configuration |
| `.Config.User` | Container user |
| `.Config.Env` | Environment variables |
| `.Config.Image` | Source image name |
| `.State.StartedAt` | Container start time |
## docker diff - Filesystem Changes
### Syntax
```bash
docker diff <container_id>
```
### Output Codes
| Code | Meaning |
|------|---------|
| `A` | File or directory was added |
| `C` | File or directory was changed |
| `D` | File or directory was deleted |
## docker export - Container Filesystem Export
### Syntax
```bash
docker export <container_id> > container_fs.tar
docker export <container_id> | gzip > container_fs.tar.gz
```
## docker commit / docker save - Image Preservation
### Syntax
```bash
docker commit <container_id> forensic-evidence:case001
docker save forensic-evidence:case001 > evidence_image.tar
```
## docker logs - Container Log Retrieval
### Syntax
```bash
docker logs --timestamps <container_id>
docker logs --since 2024-01-15 <container_id>
docker logs --tail 1000 <container_id>
docker logs -f <container_id> # Follow (live)
```
## dive - Image Layer Analysis
### Syntax
```bash
dive <image_name> # Interactive mode
dive <image_name> --ci # CI mode (non-interactive)
dive <image_name> --ci --json out.json # JSON output
```
### Output Includes
- Layer-by-layer filesystem changes
- Image efficiency score
- Wasted space analysis
## container-diff - Image Comparison
### Syntax
```bash
container-diff diff daemon://nginx:latest daemon://suspect:latest \
--type=file --type=apt --type=history --json
```
### Diff Types
| Type | Description |
|------|-------------|
| `file` | File system differences |
| `apt` | APT package differences |
| `pip` | Python package differences |
| `history` | Docker build history differences |
## Trivy - Vulnerability Scanning
### Syntax
```bash
trivy image <image_name>
trivy image --format json <image_name>
trivy image --scanners vuln,secret <image_name>
trivy fs /path/to/exported/container/
```
### Severity Levels
`CRITICAL` | `HIGH` | `MEDIUM` | `LOW` | `UNKNOWN`
## docker-explorer - Offline Forensics
### Syntax
```bash
de.py -r /var/lib/docker list
de.py -r /var/lib/docker mount <container_id> /mnt/forensic
de.py -r /var/lib/docker history <container_id>
```
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""Docker container forensics agent for investigating compromised containers."""
import subprocess
import json
import os
import sys
import hashlib
import datetime
def run_cmd(cmd):
"""Execute a shell command and return output."""
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def list_containers(all_containers=True):
"""List Docker containers with detailed information."""
flags = "-a" if all_containers else ""
cmd = f"docker ps {flags} --no-trunc --format '{{{{json .}}}}'"
stdout, _, rc = run_cmd(cmd)
containers = []
if rc == 0 and stdout:
for line in stdout.splitlines():
try:
containers.append(json.loads(line))
except json.JSONDecodeError:
continue
return containers
def inspect_container(container_id):
"""Get detailed container inspection data."""
stdout, _, rc = run_cmd(f"docker inspect {container_id}")
if rc == 0 and stdout:
return json.loads(stdout)
return None
def analyze_security_config(inspect_data):
"""Analyze container security configuration for misconfigurations."""
if isinstance(inspect_data, list):
inspect_data = inspect_data[0]
findings = []
host_config = inspect_data.get("HostConfig", {})
config = inspect_data.get("Config", {})
if host_config.get("Privileged"):
findings.append({"severity": "CRITICAL", "finding": "Container running in PRIVILEGED mode"})
cap_add = host_config.get("CapAdd") or []
dangerous_caps = ["SYS_ADMIN", "SYS_PTRACE", "NET_ADMIN", "SYS_MODULE",
"DAC_OVERRIDE", "NET_RAW"]
for cap in cap_add:
if cap in dangerous_caps:
findings.append({"severity": "HIGH", "finding": f"Dangerous capability added: {cap}"})
if host_config.get("PidMode") == "host":
findings.append({"severity": "HIGH", "finding": "Shares host PID namespace"})
if host_config.get("NetworkMode") == "host":
findings.append({"severity": "HIGH", "finding": "Shares host network namespace"})
mounts = inspect_data.get("Mounts", [])
sensitive_paths = ["/", "/etc", "/var", "/root", "/home", "/var/run/docker.sock"]
for mount in mounts:
src = mount.get("Source", "")
rw = mount.get("RW", False)
if src in sensitive_paths and rw:
findings.append({
"severity": "CRITICAL",
"finding": f"Sensitive host path mounted RW: {src} -> {mount.get('Destination')}"
})
if "docker.sock" in src:
findings.append({
"severity": "CRITICAL",
"finding": "Docker socket mounted (container can control Docker daemon)"
})
user = config.get("User", "")
if not user or user == "root":
findings.append({"severity": "MEDIUM", "finding": "Running as root user"})
env_vars = config.get("Env", [])
secret_keywords = ["PASSWORD", "SECRET", "KEY", "TOKEN", "CREDENTIAL", "API_KEY"]
for env in env_vars:
key = env.split("=")[0]
if any(s in key.upper() for s in secret_keywords):
findings.append({"severity": "HIGH", "finding": f"Sensitive env var exposed: {key}"})
return findings
def get_filesystem_changes(container_id):
"""Get filesystem changes between container and its image."""
stdout, _, rc = run_cmd(f"docker diff {container_id}")
changes = {"added": [], "changed": [], "deleted": []}
if rc == 0 and stdout:
for line in stdout.splitlines():
line = line.strip()
if line.startswith("A "):
changes["added"].append(line[2:])
elif line.startswith("C "):
changes["changed"].append(line[2:])
elif line.startswith("D "):
changes["deleted"].append(line[2:])
return changes
def detect_suspicious_files(changes):
"""Analyze filesystem changes for indicators of compromise."""
suspicious_patterns = [
"/tmp/", "/dev/shm/", "/root/", ".sh", ".py", ".elf",
"reverse", "shell", "backdoor", "miner", "xmr", "nc ",
".php", "webshell", "c2", "beacon",
]
suspicious_changes = ["/etc/passwd", "/etc/shadow", "/etc/crontab",
"/etc/ssh", ".bashrc", "/etc/sudoers", "authorized_keys"]
findings = []
for f in changes["added"]:
for pattern in suspicious_patterns:
if pattern in f.lower():
findings.append({"type": "ADDED", "path": f, "reason": f"Matches pattern: {pattern}"})
break
for f in changes["changed"]:
for pattern in suspicious_changes:
if pattern in f.lower():
findings.append({"type": "CHANGED", "path": f, "reason": f"Critical file modified"})
break
return findings
def export_container(container_id, output_path):
"""Export container filesystem as a tarball for offline analysis."""
cmd = f"docker export {container_id} > {output_path}"
_, _, rc = run_cmd(cmd)
if rc == 0 and os.path.exists(output_path):
sha256 = hashlib.sha256()
with open(output_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
return True, sha256.hexdigest()
return False, None
def get_container_logs(container_id, tail=500):
"""Retrieve container logs with timestamps."""
stdout, stderr, rc = run_cmd(f"docker logs --timestamps --tail {tail} {container_id}")
return stdout + "\n" + stderr if rc == 0 else None
def scan_image_vulnerabilities(image_name):
"""Run Trivy vulnerability scan on a container image."""
cmd = f"trivy image --format json {image_name}"
stdout, _, rc = run_cmd(cmd)
if rc == 0 and stdout:
try:
return json.loads(stdout)
except json.JSONDecodeError:
return None
return None
def generate_report(container_id, inspect_data, security_findings,
fs_changes, suspicious_files):
"""Generate a forensic analysis report."""
container_name = "unknown"
image = "unknown"
if inspect_data:
data = inspect_data[0] if isinstance(inspect_data, list) else inspect_data
container_name = data.get("Name", "").lstrip("/")
image = data.get("Config", {}).get("Image", "unknown")
report = {
"report_type": "Docker Container Forensics",
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
"container_id": container_id,
"container_name": container_name,
"image": image,
"security_findings": security_findings,
"filesystem_changes": {
"added": len(fs_changes["added"]),
"changed": len(fs_changes["changed"]),
"deleted": len(fs_changes["deleted"]),
},
"suspicious_files": suspicious_files,
}
return report
if __name__ == "__main__":
print("=" * 60)
print("Docker Container Forensics Agent")
print("Security analysis, filesystem diffing, evidence collection")
print("=" * 60)
container_id = sys.argv[1] if len(sys.argv) > 1 else None
if container_id:
print(f"\n[*] Analyzing container: {container_id}")
inspect_data = inspect_container(container_id)
if not inspect_data:
print("[ERROR] Failed to inspect container. Is Docker running?")
sys.exit(1)
print("\n--- Security Configuration Analysis ---")
findings = analyze_security_config(inspect_data)
for f in findings:
print(f"[{f['severity']}] {f['finding']}")
print("\n--- Filesystem Changes ---")
changes = get_filesystem_changes(container_id)
print(f" Added: {len(changes['added'])}, Changed: {len(changes['changed'])}, "
f"Deleted: {len(changes['deleted'])}")
print("\n--- Suspicious Files ---")
suspicious = detect_suspicious_files(changes)
for s in suspicious:
print(f"[!] {s['type']}: {s['path']} ({s['reason']})")
report = generate_report(container_id, inspect_data, findings, changes, suspicious)
print(f"\n[*] Report:\n{json.dumps(report, indent=2)}")
else:
print("\n[*] Listing all containers...")
containers = list_containers()
for c in containers:
print(f" {c.get('ID', '?')[:12]} {c.get('Names', '?')} {c.get('Status', '?')}")
print(f"\n[DEMO] Usage: python agent.py <container_id>")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,121 @@
# API Reference: Email Header Analysis Tools
## Python email Module
### Parsing EML Files
```python
import email
from email import policy
with open("phishing.eml", "r") as f:
msg = email.message_from_file(f, policy=policy.default)
msg["From"] # From header
msg["To"] # To header
msg["Subject"] # Subject line
msg["Message-ID"] # Unique message identifier
msg["Reply-To"] # Reply-To address
msg["Return-Path"] # Envelope sender
msg.get_all("Received") # All Received headers (list)
msg.get_all("Authentication-Results") # Auth results
```
### Body and Attachment Extraction
```python
body = msg.get_body(preferencelist=("html", "plain"))
content = body.get_content()
for part in msg.walk():
if part.get_content_disposition() == "attachment":
filename = part.get_filename()
data = part.get_payload(decode=True)
```
## dig - DNS Record Lookup
### SPF Record
```bash
dig TXT example.com +short
# Output: "v=spf1 include:_spf.google.com ~all"
```
### DKIM Record
```bash
dig TXT selector1._domainkey.example.com +short
```
### DMARC Record
```bash
dig TXT _dmarc.example.com +short
# Output: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"
```
## pyspf - SPF Validation (Python)
### Syntax
```python
import spf
result, explanation = spf.check2(
i="203.0.113.45", # Sending IP
s="sender@example.com", # Envelope sender
h="mail.example.com" # HELO hostname
)
# Results: pass, fail, softfail, neutral, none, temperror, permerror
```
## dkimpy - DKIM Verification (Python)
### Syntax
```python
import dkim
with open("email.eml", "rb") as f:
message = f.read()
result = dkim.verify(message)
# Returns True/False
```
## AbuseIPDB - IP Reputation
### API Endpoint
```bash
curl -G "https://api.abuseipdb.com/api/v2/check" \
-H "Key: YOUR_API_KEY" \
-H "Accept: application/json" \
-d "ipAddress=203.0.113.45" -d "maxAgeInDays=90"
```
### Response Fields
| Field | Description |
|-------|-------------|
| `abuseConfidenceScore` | 0-100 confidence of abuse |
| `totalReports` | Number of abuse reports |
| `countryCode` | Source country |
| `isp` | Internet service provider |
## VirusTotal - Domain/URL Reputation
### Domain Lookup
```bash
curl -H "x-apikey: YOUR_KEY" \
"https://www.virustotal.com/api/v3/domains/suspicious.com"
```
### URL Scan
```bash
curl -X POST "https://www.virustotal.com/api/v3/urls" \
-H "x-apikey: YOUR_KEY" \
-d "url=http://suspicious-url.com/login"
```
## whois - Domain Registration
### Syntax
```bash
whois suspicious-domain.com
```
### Key Fields
- `Registrar` - Domain registrar
- `Creation Date` - When domain was registered
- `Registrant` - Domain owner info
- `Name Server` - Authoritative DNS servers
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Email header analysis agent for phishing investigation and sender verification."""
import email
import email.utils
import re
import hashlib
import os
import sys
import subprocess
import json
from email import policy
def parse_email_file(eml_path):
"""Parse an EML file and extract key header fields."""
with open(eml_path, "r", errors="replace") as f:
msg = email.message_from_file(f, policy=policy.default)
headers = {
"from": str(msg["From"] or ""),
"to": str(msg["To"] or ""),
"subject": str(msg["Subject"] or ""),
"date": str(msg["Date"] or ""),
"message_id": str(msg["Message-ID"] or ""),
"reply_to": str(msg["Reply-To"] or ""),
"return_path": str(msg["Return-Path"] or ""),
"x_mailer": str(msg["X-Mailer"] or ""),
"x_originating_ip": str(msg["X-Originating-IP"] or ""),
}
return msg, headers
def extract_received_chain(msg):
"""Extract and parse the Received header chain (bottom-up = chronological)."""
received_headers = msg.get_all("Received") or []
hops = []
ip_pattern = re.compile(r"\[?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]?")
for i, header in enumerate(reversed(received_headers)):
ips = ip_pattern.findall(header)
hops.append({
"hop": i + 1,
"header": header.strip()[:200],
"ips": ips,
})
return hops
def extract_authentication_results(msg):
"""Extract SPF, DKIM, and DMARC results from Authentication-Results headers."""
auth_results = msg.get_all("Authentication-Results") or []
received_spf = str(msg.get("Received-SPF", ""))
dkim_sig = str(msg.get("DKIM-Signature", ""))
results = {
"spf": "unknown",
"dkim": "unknown",
"dmarc": "unknown",
"raw_authentication_results": [],
"received_spf": received_spf,
"has_dkim_signature": bool(dkim_sig),
}
for ar in auth_results:
results["raw_authentication_results"].append(ar.strip())
ar_lower = ar.lower()
if "spf=" in ar_lower:
spf_match = re.search(r"spf=(\w+)", ar_lower)
if spf_match:
results["spf"] = spf_match.group(1)
if "dkim=" in ar_lower:
dkim_match = re.search(r"dkim=(\w+)", ar_lower)
if dkim_match:
results["dkim"] = dkim_match.group(1)
if "dmarc=" in ar_lower:
dmarc_match = re.search(r"dmarc=(\w+)", ar_lower)
if dmarc_match:
results["dmarc"] = dmarc_match.group(1)
return results
def check_from_replyto_mismatch(headers):
"""Detect mismatch between From and Reply-To addresses."""
from_addr = email.utils.parseaddr(headers["from"])[1].lower()
reply_to = headers["reply_to"]
if reply_to:
reply_addr = email.utils.parseaddr(reply_to)[1].lower()
if reply_addr and from_addr != reply_addr:
return True, from_addr, reply_addr
return False, from_addr, None
def extract_urls(msg):
"""Extract all URLs from the email body."""
body = msg.get_body(preferencelist=("html", "plain"))
urls = []
if body:
content = body.get_content()
urls = list(set(re.findall(r"https?://[^\s<>\"']+", content)))
return urls
def detect_url_mismatch(msg):
"""Detect hyperlinks where display text differs from actual href."""
body = msg.get_body(preferencelist=("html",))
mismatches = []
if body:
content = body.get_content()
href_pattern = re.findall(
r'<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', content, re.DOTALL
)
for href, text in href_pattern:
display_urls = re.findall(r"https?://[^\s<]+", text)
if display_urls:
for display_url in display_urls:
if display_url.rstrip("/") != href.rstrip("/"):
mismatches.append({
"display_url": display_url,
"actual_url": href,
})
return mismatches
def extract_attachments(msg, output_dir=None):
"""Extract and hash all email attachments."""
attachments = []
for part in msg.walk():
if part.get_content_disposition() == "attachment":
filename = part.get_filename() or "unnamed_attachment"
content = part.get_payload(decode=True)
if content:
sha256 = hashlib.sha256(content).hexdigest()
md5 = hashlib.md5(content).hexdigest()
att_info = {
"filename": filename,
"size": len(content),
"sha256": sha256,
"md5": md5,
"content_type": part.get_content_type(),
}
if output_dir:
os.makedirs(output_dir, exist_ok=True)
filepath = os.path.join(output_dir, filename)
with open(filepath, "wb") as f:
f.write(content)
att_info["saved_to"] = filepath
attachments.append(att_info)
return attachments
def dns_lookup(domain, record_type="TXT"):
"""Perform DNS lookup for SPF/DKIM/DMARC records."""
cmd = f"dig {record_type} {domain} +short"
stdout, _, rc = subprocess.run(cmd, shell=True, capture_output=True, text=True,
timeout=10).stdout, "", 0
return stdout.strip() if stdout else ""
def check_domain_spf(domain):
"""Look up the SPF record for a domain."""
return dns_lookup(domain, "TXT")
def check_domain_dmarc(domain):
"""Look up the DMARC record for a domain."""
return dns_lookup(f"_dmarc.{domain}", "TXT")
def generate_phishing_indicators(headers, auth, hops, url_mismatches, attachments):
"""Compile a list of phishing indicators from the analysis."""
indicators = []
mismatch, from_addr, reply_addr = check_from_replyto_mismatch(headers)
if mismatch:
indicators.append(f"From/Reply-To mismatch: {from_addr} vs {reply_addr}")
if auth["spf"] in ("fail", "softfail"):
indicators.append(f"SPF {auth['spf']}")
if auth["dkim"] == "fail" or not auth["has_dkim_signature"]:
indicators.append("DKIM failed or missing")
if auth["dmarc"] in ("fail", "none"):
indicators.append(f"DMARC {auth['dmarc']}")
if url_mismatches:
indicators.append(f"{len(url_mismatches)} URL display/href mismatches detected")
for att in attachments:
if any(att["filename"].endswith(ext) for ext in [".exe", ".scr", ".vbs", ".js",
".docm", ".xlsm", ".bat", ".ps1", ".hta"]):
indicators.append(f"Suspicious attachment: {att['filename']}")
return indicators
if __name__ == "__main__":
print("=" * 60)
print("Email Header Phishing Analysis Agent")
print("SPF/DKIM/DMARC validation, URL analysis, attachment extraction")
print("=" * 60)
eml_file = sys.argv[1] if len(sys.argv) > 1 else None
if eml_file and os.path.exists(eml_file):
print(f"\n[*] Analyzing: {eml_file}")
msg, headers = parse_email_file(eml_file)
print(f" From: {headers['from']}")
print(f" To: {headers['to']}")
print(f" Subject: {headers['subject']}")
print(f" Date: {headers['date']}")
hops = extract_received_chain(msg)
print(f"\n[*] Delivery path: {len(hops)} hops")
for hop in hops:
print(f" Hop {hop['hop']}: IPs={hop['ips']}")
auth = extract_authentication_results(msg)
print(f"\n[*] Authentication: SPF={auth['spf']} DKIM={auth['dkim']} DMARC={auth['dmarc']}")
urls = extract_urls(msg)
print(f"\n[*] URLs found: {len(urls)}")
url_mismatches = detect_url_mismatch(msg)
for m in url_mismatches:
print(f" [!] MISMATCH: Display='{m['display_url']}' Actual='{m['actual_url']}'")
attachments = extract_attachments(msg)
print(f"\n[*] Attachments: {len(attachments)}")
for att in attachments:
print(f" {att['filename']} ({att['size']} bytes) SHA256={att['sha256'][:16]}...")
indicators = generate_phishing_indicators(headers, auth, hops, url_mismatches, attachments)
if indicators:
print(f"\n[!] PHISHING INDICATORS:")
for ind in indicators:
print(f" - {ind}")
else:
print(f"\n[DEMO] Usage: python agent.py <email.eml>")
print("[*] Provide an EML file for phishing analysis.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,90 @@
# API Reference: Go Malware Analysis with Ghidra
## Ghidra Go Analysis Setup
### GoResolver Script (Volexity)
```bash
# Install GoResolver for stripped Go binary function recovery
git clone https://github.com/volexity/GoResolver
# Run against Ghidra project
analyzeHeadless /ghidra_projects MyProject -process go_malware.exe \
-postScript GoResolver.java
```
### Ghidra Built-in Go Support (10.3+)
```
File > Import > Select Go binary
Analysis > Auto Analyze (includes GolangAnalyzer)
Window > Function Tags > Filter "go."
```
## Go Binary Characteristics
### Build Info Magic
```
Offset in .go.buildinfo section: "\xff Go buildinf:"
```
### gopclntab Magic Bytes
| Go Version | Magic |
|------------|-------|
| 1.2-1.15 | `FB FF FF FF 00 00` |
| 1.16-1.17 | `FA FF FF FF 00 00` |
| 1.18-1.19 | `F0 FF FF FF 00 00` |
| 1.20+ | `F1 FF FF FF 00 00` |
### String Format
Go strings are length-prefixed (not null-terminated):
```
struct GoString {
char *ptr; // pointer to string data
int64 length; // string length
};
```
## Go-Specific Ghidra Scripts
### GoReSym (Mandiant)
```bash
GoReSym -t -d -p /path/to/binary
# -t: Recover type information
# -d: Dump function metadata
# -p: Print package listing
```
### redress (Go Reverse Engineering)
```bash
redress -src binary.exe # Reconstruct source tree
redress -pkg binary.exe # List packages
redress -type binary.exe # Type information
redress -string binary.exe # Go string extraction
redress -interface binary.exe # Interface types
```
## Go Obfuscation Tools
| Tool | Technique | Detection |
|------|-----------|-----------|
| garble | Function name hashing, literal obfuscation | Hash-like symbols, missing debug info |
| gobfuscate | Package/function renaming | Random 8-char names |
| go-strip | Symbol table removal | Missing gopclntab entries |
## Common Go Malware Families
| Family | Type | Notable Packages |
|--------|------|-----------------|
| Sliver | C2 implant | protobuf, grpc, mtls |
| Merlin | C2 agent | http2, jose, websocket |
| Sunlogin/Cobalt | RAT | screenshot, clipboard, keylog |
| BianLian | Ransomware | crypto/aes, filepath.Walk |
| Royal | Ransomware | goroutine-based parallel encryption |
## Key Ghidra Analysis Steps
```
1. Search > For Strings > "go1." (version identification)
2. Search > For Bytes > FB FF FF FF (gopclntab)
3. Symbol Table > Filter "main." (entry points)
4. Navigation > Go To "runtime.main" (program start)
5. Decompiler > Check goroutine spawns (runtime.newproc)
6. Data Types > Apply GoString struct to string references
```
@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""Go malware analysis agent for Ghidra-assisted reverse engineering.
Analyzes Go binaries to extract function names, strings, build metadata,
package information, and detects common Go malware characteristics.
"""
import struct
import os
import sys
import json
import hashlib
import re
import math
from collections import Counter
def compute_hash(filepath):
"""Compute SHA-256 hash of file."""
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
return sha256.hexdigest()
def shannon_entropy(data):
"""Calculate Shannon entropy."""
if not data:
return 0.0
freq = Counter(data)
length = len(data)
return -sum((c / length) * math.log2(c / length) for c in freq.values())
def detect_go_binary(filepath):
"""Detect if a binary is compiled with Go and extract version info."""
with open(filepath, "rb") as f:
data = f.read()
indicators = {
"is_go_binary": False,
"go_version": None,
"go_buildinfo": False,
"gopclntab_found": False,
}
# Go build info magic
buildinfo_magic = b"\xff Go buildinf:"
offset = data.find(buildinfo_magic)
if offset != -1:
indicators["is_go_binary"] = True
indicators["go_buildinfo"] = True
# Go version string
version_pattern = rb"go(\d+\.\d+(?:\.\d+)?)"
matches = re.findall(version_pattern, data)
if matches:
indicators["is_go_binary"] = True
versions = sorted(set(m.decode() for m in matches))
indicators["go_version"] = versions[-1] if versions else None
# gopclntab (Go PC line table) magic bytes
gopclntab_magics = [
b"\xfb\xff\xff\xff\x00\x00", # Go 1.2-1.15
b"\xfa\xff\xff\xff\x00\x00", # Go 1.16-1.17
b"\xf0\xff\xff\xff\x00\x00", # Go 1.18+
b"\xf1\xff\xff\xff\x00\x00", # Go 1.20+
]
for magic in gopclntab_magics:
if magic in data:
indicators["gopclntab_found"] = True
indicators["is_go_binary"] = True
break
# Runtime strings
go_strings = [b"runtime.main", b"runtime.goexit", b"runtime.gopanic",
b"runtime.newproc", b"GOROOT", b"GOPATH"]
found_runtime = sum(1 for s in go_strings if s in data)
if found_runtime >= 2:
indicators["is_go_binary"] = True
indicators["runtime_strings_found"] = found_runtime
return indicators
def extract_go_strings(filepath, min_length=6):
"""Extract Go-style strings (length-prefixed, not null-terminated)."""
with open(filepath, "rb") as f:
data = f.read()
# Standard ASCII string extraction
ascii_pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_length)
strings = [m.group().decode("ascii", errors="replace") for m in ascii_pattern.finditer(data)]
return strings
def extract_go_packages(strings_list):
"""Identify Go packages from extracted strings."""
packages = set()
pkg_pattern = re.compile(r"^([a-zA-Z0-9_]+(?:/[a-zA-Z0-9_.-]+)+)\.")
for s in strings_list:
match = pkg_pattern.match(s)
if match:
packages.add(match.group(1))
# Also look for known Go import paths
for s in strings_list:
if s.startswith("github.com/") or s.startswith("golang.org/"):
parts = s.split("/")
if len(parts) >= 3:
packages.add("/".join(parts[:3]))
return sorted(packages)
SUSPICIOUS_GO_PACKAGES = {
"github.com/kbinani/screenshot": "Screen capture capability",
"github.com/atotto/clipboard": "Clipboard access",
"github.com/go-vgo/robotgo": "Desktop automation / keylogging",
"github.com/miekg/dns": "Custom DNS resolution (C2/tunneling)",
"golang.org/x/crypto/ssh": "SSH client (lateral movement)",
"github.com/shirou/gopsutil": "System enumeration",
"github.com/mitchellh/go-ps": "Process listing",
"github.com/gobuffalo/packr": "Binary resource embedding",
"github.com/Ne0nd0g/merlin": "Merlin C2 agent",
"github.com/BishopFox/sliver": "Sliver C2 framework",
"github.com/traefik/yaegi": "Go interpreter (dynamic execution)",
}
def detect_suspicious_packages(packages):
"""Flag suspicious Go packages commonly used in malware."""
findings = []
for pkg in packages:
for sus_pkg, description in SUSPICIOUS_GO_PACKAGES.items():
if sus_pkg in pkg:
findings.append({"package": pkg, "concern": description})
return findings
def analyze_sections(filepath):
"""Analyze PE/ELF sections for Go binary characteristics."""
with open(filepath, "rb") as f:
magic = f.read(4)
f.seek(0)
data = f.read()
sections = []
if magic[:2] == b"MZ": # PE
try:
import pefile
pe = pefile.PE(data=data)
for section in pe.sections:
name = section.Name.rstrip(b"\x00").decode("ascii", errors="replace")
entropy = section.get_entropy()
sections.append({
"name": name, "virtual_size": section.Misc_VirtualSize,
"raw_size": section.SizeOfRawData, "entropy": round(entropy, 3),
})
pe.close()
except ImportError:
sections.append({"note": "pefile not installed"})
elif magic[:4] == b"\x7fELF":
try:
from elftools.elf.elffile import ELFFile
from io import BytesIO
elf = ELFFile(BytesIO(data))
for section in elf.iter_sections():
sec_data = section.data() if section.header.sh_size > 0 else b""
entropy = shannon_entropy(sec_data) if sec_data else 0
sections.append({
"name": section.name, "size": section.header.sh_size,
"entropy": round(entropy, 3), "type": section.header.sh_type,
})
except ImportError:
sections.append({"note": "pyelftools not installed"})
return sections
def detect_obfuscation(go_info, strings_list):
"""Detect Go binary obfuscation (garble, gobfuscate)."""
indicators = {"obfuscated": False, "techniques": []}
# Garble replaces function names with hashes
hash_names = sum(1 for s in strings_list if re.match(r"^[a-f0-9]{16,}$", s))
if hash_names > 20:
indicators["obfuscated"] = True
indicators["techniques"].append("Possible garble obfuscation (hash-like function names)")
# Missing gopclntab suggests stripping
if not go_info.get("gopclntab_found"):
indicators["techniques"].append("gopclntab not found - may be stripped or modified")
# Low runtime string count
if go_info.get("runtime_strings_found", 0) < 2:
indicators["obfuscated"] = True
indicators["techniques"].append("Low Go runtime string count - possible obfuscation")
return indicators
def generate_report(filepath):
"""Generate comprehensive Go malware analysis report."""
report = {
"file": filepath,
"sha256": compute_hash(filepath),
"size": os.path.getsize(filepath),
}
go_info = detect_go_binary(filepath)
report["go_detection"] = go_info
if not go_info["is_go_binary"]:
report["conclusion"] = "Not identified as a Go binary"
return report
strings_list = extract_go_strings(filepath)
report["total_strings"] = len(strings_list)
packages = extract_go_packages(strings_list)
report["packages"] = packages[:50]
suspicious = detect_suspicious_packages(packages)
report["suspicious_packages"] = suspicious
sections = analyze_sections(filepath)
report["sections"] = sections
obfuscation = detect_obfuscation(go_info, strings_list)
report["obfuscation"] = obfuscation
return report
if __name__ == "__main__":
print("=" * 60)
print("Go Malware Analysis Agent (Ghidra-assisted)")
print("Go binary detection, package extraction, obfuscation detection")
print("=" * 60)
target = sys.argv[1] if len(sys.argv) > 1 else None
if not target or not os.path.exists(target):
print("\n[DEMO] Usage: python agent.py <go_binary>")
sys.exit(0)
report = generate_report(target)
go = report.get("go_detection", {})
print(f"\n[*] File: {target}")
print(f"[*] SHA-256: {report['sha256']}")
print(f"[*] Go binary: {go.get('is_go_binary', False)}")
print(f"[*] Go version: {go.get('go_version', 'unknown')}")
print(f"[*] Strings: {report.get('total_strings', 0)}")
print("\n--- Packages ---")
for pkg in report.get("packages", [])[:15]:
print(f" {pkg}")
print("\n--- Suspicious Packages ---")
for s in report.get("suspicious_packages", []):
print(f" [!] {s['package']}: {s['concern']}")
print("\n--- Obfuscation ---")
obf = report.get("obfuscation", {})
print(f" Obfuscated: {obf.get('obfuscated', False)}")
for t in obf.get("techniques", []):
print(f" {t}")
print(f"\n{json.dumps(report, indent=2, default=str)}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,120 @@
# API Reference: IOC Enrichment Tools
## VirusTotal API v3
### File Hash Lookup
```bash
curl -H "x-apikey: $VT_KEY" \
"https://www.virustotal.com/api/v3/files/<sha256>"
```
### Domain Lookup
```bash
curl -H "x-apikey: $VT_KEY" \
"https://www.virustotal.com/api/v3/domains/<domain>"
```
### IP Lookup
```bash
curl -H "x-apikey: $VT_KEY" \
"https://www.virustotal.com/api/v3/ip_addresses/<ip>"
```
### Key Response Fields
| Field | Description |
|-------|-------------|
| `last_analysis_stats.malicious` | Number of AV engines detecting as malicious |
| `last_analysis_stats.undetected` | AV engines finding clean |
| `reputation` | Community reputation score |
| `popular_threat_classification` | Threat label consensus |
### Python (vt-py)
```python
import vt
client = vt.Client("API_KEY")
file_obj = client.get_object(f"/files/{sha256}")
stats = file_obj.last_analysis_stats
client.close()
```
## AbuseIPDB API v2
### Check IP
```bash
curl -G "https://api.abuseipdb.com/api/v2/check" \
-H "Key: $ABUSE_KEY" -H "Accept: application/json" \
-d "ipAddress=1.2.3.4" -d "maxAgeInDays=90"
```
### Response Fields
| Field | Description |
|-------|-------------|
| `abuseConfidenceScore` | 0-100 abuse confidence |
| `totalReports` | Report count in timeframe |
| `countryCode` | Source country |
| `isp` | Internet service provider |
| `isTor` | Tor exit node flag |
## MalwareBazaar API (abuse.ch)
### Hash Lookup
```bash
curl -X POST "https://mb-api.abuse.ch/api/v1/" \
-d "query=get_info" -d "hash=<sha256>"
```
### Response Fields
| Field | Description |
|-------|-------------|
| `signature` | Malware family name |
| `tags` | Associated tags |
| `file_type` | File type identification |
| `first_seen` | First submission date |
| `reporter` | Submitting analyst |
## URLScan.io API
### Submit URL for Scan
```bash
curl -X POST "https://urlscan.io/api/v1/scan/" \
-H "API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"url": "http://suspicious.com", "visibility": "private"}'
```
### Retrieve Results
```bash
curl "https://urlscan.io/api/v1/result/<uuid>/"
```
## Shodan API
### IP Lookup
```bash
curl "https://api.shodan.io/shodan/host/<ip>?key=$SHODAN_KEY"
```
### Response Fields
| Field | Description |
|-------|-------------|
| `ports` | Open ports list |
| `os` | Operating system |
| `org` | Organization |
| `asn` | Autonomous system number |
| `hostnames` | Associated hostnames |
## IOC Confidence Scoring Framework
| Score | Disposition | Criteria |
|-------|-------------|----------|
| >= 70 | BLOCK | 15+ VT detections, AbuseIPDB >= 70%, or MalwareBazaar match |
| 40-69 | MONITOR | 5-14 VT detections, moderate abuse score |
| < 40 | INVESTIGATE | Low detection, no campaign attribution |
## Defanging Convention
| Original | Defanged |
|----------|----------|
| `http://` | `hxxp://` |
| `https://` | `hxxps://` |
| `.com` | `[.]com` |
| `evil.com` | `evil[.]com` |
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""IOC analysis and enrichment agent using VirusTotal, AbuseIPDB, and MalwareBazaar APIs."""
import re
import os
import sys
import json
import hashlib
import datetime
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
def classify_ioc(value):
"""Classify an IOC by type: ipv4, domain, url, sha256, sha1, md5, email."""
value = value.strip()
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value):
return "ipv4"
if re.match(r"^[a-fA-F0-9]{64}$", value):
return "sha256"
if re.match(r"^[a-fA-F0-9]{40}$", value):
return "sha1"
if re.match(r"^[a-fA-F0-9]{32}$", value):
return "md5"
if re.match(r"^https?://", value):
return "url"
if re.match(r"^[^@]+@[^@]+\.[^@]+$", value):
return "email"
if re.match(r"^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", value):
return "domain"
return "unknown"
def defang_ioc(value):
"""Defang an IOC for safe documentation."""
value = value.replace("http://", "hxxp://")
value = value.replace("https://", "hxxps://")
value = re.sub(r"\.(?=\w)", "[.]", value)
return value
def refang_ioc(value):
"""Refang a defanged IOC for querying APIs."""
value = value.replace("hxxp://", "http://")
value = value.replace("hxxps://", "https://")
value = value.replace("[.]", ".")
value = value.replace("[://]", "://")
return value
def is_private_ip(ip):
"""Check if an IP is RFC 1918 private."""
octets = [int(o) for o in ip.split(".")]
if octets[0] == 10:
return True
if octets[0] == 172 and 16 <= octets[1] <= 31:
return True
if octets[0] == 192 and octets[1] == 168:
return True
if octets[0] == 127:
return True
return False
def query_virustotal_hash(sha256, api_key):
"""Query VirusTotal for a file hash."""
url = f"https://www.virustotal.com/api/v3/files/{sha256}"
resp = requests.get(url, headers={"x-apikey": api_key})
if resp.status_code == 200:
data = resp.json().get("data", {}).get("attributes", {})
stats = data.get("last_analysis_stats", {})
return {
"sha256": sha256,
"malicious": stats.get("malicious", 0),
"total": sum(stats.values()),
"type_description": data.get("type_description", ""),
"popular_threat_name": data.get("popular_threat_classification", {}).get(
"suggested_threat_label", ""),
"tags": data.get("tags", []),
}
return None
def query_virustotal_domain(domain, api_key):
"""Query VirusTotal for domain reputation."""
url = f"https://www.virustotal.com/api/v3/domains/{domain}"
resp = requests.get(url, headers={"x-apikey": api_key})
if resp.status_code == 200:
data = resp.json().get("data", {}).get("attributes", {})
stats = data.get("last_analysis_stats", {})
return {
"domain": domain,
"malicious": stats.get("malicious", 0),
"suspicious": stats.get("suspicious", 0),
"reputation": data.get("reputation", 0),
"registrar": data.get("registrar", ""),
"creation_date": data.get("creation_date", ""),
}
return None
def query_abuseipdb(ip, api_key, max_age_days=90):
"""Query AbuseIPDB for IP reputation."""
url = "https://api.abuseipdb.com/api/v2/check"
resp = requests.get(url, headers={"Key": api_key, "Accept": "application/json"},
params={"ipAddress": ip, "maxAgeInDays": max_age_days})
if resp.status_code == 200:
data = resp.json().get("data", {})
return {
"ip": ip,
"abuse_confidence": data.get("abuseConfidenceScore", 0),
"total_reports": data.get("totalReports", 0),
"country": data.get("countryCode", ""),
"isp": data.get("isp", ""),
"domain": data.get("domain", ""),
"is_tor": data.get("isTor", False),
}
return None
def query_malwarebazaar(sha256):
"""Query MalwareBazaar for file hash information."""
url = "https://mb-api.abuse.ch/api/v1/"
resp = requests.post(url, data={"query": "get_info", "hash": sha256})
if resp.status_code == 200:
result = resp.json()
if result.get("query_status") == "ok" and result.get("data"):
entry = result["data"][0]
return {
"sha256": sha256,
"signature": entry.get("signature", ""),
"tags": entry.get("tags", []),
"file_type": entry.get("file_type", ""),
"reporter": entry.get("reporter", ""),
"first_seen": entry.get("first_seen", ""),
}
return None
def score_ioc(vt_result=None, abuse_result=None, mb_result=None):
"""Assign a confidence score and disposition to an IOC."""
score = 0
reasons = []
if vt_result:
malicious = vt_result.get("malicious", 0)
if malicious >= 15:
score += 40
reasons.append(f"VT: {malicious} detections (high)")
elif malicious >= 5:
score += 20
reasons.append(f"VT: {malicious} detections (moderate)")
elif malicious > 0:
score += 5
reasons.append(f"VT: {malicious} detections (low)")
if abuse_result:
abuse_score = abuse_result.get("abuse_confidence", 0)
if abuse_score >= 70:
score += 30
reasons.append(f"AbuseIPDB: {abuse_score}% confidence")
elif abuse_score >= 30:
score += 15
reasons.append(f"AbuseIPDB: {abuse_score}% confidence")
if mb_result:
score += 30
reasons.append(f"MalwareBazaar: {mb_result.get('signature', 'known malware')}")
if score >= 70:
disposition = "BLOCK"
elif score >= 40:
disposition = "MONITOR"
else:
disposition = "INVESTIGATE"
return {"score": score, "disposition": disposition, "reasons": reasons}
def enrich_ioc(value, vt_key=None, abuse_key=None):
"""Enrich a single IOC with multi-source intelligence."""
ioc_type = classify_ioc(value)
result = {
"ioc": value,
"type": ioc_type,
"defanged": defang_ioc(value),
"enrichment": {},
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
}
if not HAS_REQUESTS:
result["error"] = "requests library not installed"
return result
if ioc_type == "ipv4" and is_private_ip(value):
result["note"] = "RFC 1918 private IP - skipping external enrichment"
return result
if ioc_type in ("sha256", "sha1", "md5") and vt_key:
result["enrichment"]["virustotal"] = query_virustotal_hash(value, vt_key)
result["enrichment"]["malwarebazaar"] = query_malwarebazaar(value)
elif ioc_type == "ipv4":
if abuse_key:
result["enrichment"]["abuseipdb"] = query_abuseipdb(value, abuse_key)
if vt_key:
result["enrichment"]["virustotal"] = query_virustotal_domain(value, vt_key)
elif ioc_type == "domain" and vt_key:
result["enrichment"]["virustotal"] = query_virustotal_domain(value, vt_key)
scoring = score_ioc(
result["enrichment"].get("virustotal"),
result["enrichment"].get("abuseipdb"),
result["enrichment"].get("malwarebazaar"),
)
result["score"] = scoring["score"]
result["disposition"] = scoring["disposition"]
result["reasons"] = scoring["reasons"]
return result
if __name__ == "__main__":
print("=" * 60)
print("IOC Analysis & Enrichment Agent")
print("VirusTotal, AbuseIPDB, MalwareBazaar integration")
print("=" * 60)
demo_iocs = [
"185.220.101.42",
"evil-domain.com",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"http://malicious-site.com/payload.exe",
"192.168.1.100",
]
print("\n--- IOC Classification & Defanging ---")
for ioc in demo_iocs:
ioc_type = classify_ioc(ioc)
defanged = defang_ioc(ioc)
private = " (private)" if ioc_type == "ipv4" and is_private_ip(ioc) else ""
print(f" {ioc_type:8s} | {defanged}{private}")
vt_key = os.environ.get("VT_API_KEY")
abuse_key = os.environ.get("ABUSEIPDB_API_KEY")
if vt_key or abuse_key:
print("\n--- Enrichment (live API queries) ---")
for ioc in demo_iocs:
result = enrich_ioc(ioc, vt_key, abuse_key)
print(f"\n {result['ioc']} ({result['type']})")
print(f" Disposition: {result.get('disposition', 'N/A')} "
f"(score: {result.get('score', 0)})")
for reason in result.get("reasons", []):
print(f" - {reason}")
else:
print("\n[*] Set VT_API_KEY and/or ABUSEIPDB_API_KEY environment variables for live enrichment.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,105 @@
# API Reference: iOS App Security with Objection
## Objection CLI
### Launch
```bash
objection -g com.example.app explore # Attach to running app
objection -g com.example.app explore -s "command" # Run startup command
objection patchipa --source app.ipa # Patch IPA with Frida gadget
```
### Keychain & Data Storage
```bash
ios keychain dump # Dump keychain items
ios keychain dump --json # JSON output
ios cookies get # List HTTP cookies
ios nsuserdefaults get # Read NSUserDefaults
ios plist cat Info.plist # Read plist file
```
### SSL Pinning
```bash
ios sslpinning disable # Bypass SSL pinning
ios sslpinning disable --quiet # Quiet mode
```
### Jailbreak Detection
```bash
ios jailbreak disable # Bypass jailbreak detection
ios jailbreak simulate # Simulate jailbroken device
```
### Hooking
```bash
ios hooking list classes # List all classes
ios hooking list classes --include Auth # Filter classes
ios hooking list class_methods ClassName # List methods
ios hooking watch method "-[Class method]" # Watch method calls
ios hooking set return_value "-[Class isJB]" false # Override return
```
### Filesystem
```bash
ls / # List app sandbox root
ls /Documents # List Documents directory
file download /path/to/file local.out # Download file
file upload local.file /remote/path # Upload file
```
### Memory
```bash
memory dump all dump.bin # Dump all memory
memory search "password" # Search memory for string
memory list modules # List loaded modules
memory list exports libModule.dylib # List module exports
```
## Frida CLI
### Syntax
```bash
frida -U -n AppName # Attach by name
frida -U -f com.app.id # Spawn and attach
frida -U -n AppName -l script.js # Load script
frida-ps -U # List running processes
frida-ls-devices # List connected devices
```
### Common Frida Scripts
```javascript
// Hook method and log arguments
ObjC.choose(ObjC.classes.ClassName, {
onMatch: function(instance) {
Interceptor.attach(instance['- methodName:'].implementation, {
onEnter: function(args) {
console.log('arg1:', ObjC.Object(args[2]));
}
});
}, onComplete: function() {}
});
```
## OWASP Mobile Top 10 (2024)
| ID | Category | Objection Check |
|----|----------|-----------------|
| M1 | Improper Credential Usage | `ios keychain dump` |
| M2 | Inadequate Supply Chain Security | Binary analysis |
| M3 | Insecure Authentication | Hook auth classes |
| M4 | Insufficient Input/Output Validation | Hook input methods |
| M5 | Insecure Communication | `ios sslpinning disable` |
| M6 | Inadequate Privacy Controls | `ios nsuserdefaults get` |
| M7 | Insufficient Binary Protections | Check PIE, ARC, stack canary |
| M8 | Security Misconfiguration | `ios plist cat Info.plist` |
| M9 | Insecure Data Storage | Filesystem + keychain review |
| M10 | Insufficient Cryptography | Hook crypto classes |
## iOS App Sandbox Paths
| Path | Contents |
|------|----------|
| `/Documents` | User-generated data |
| `/Library/Caches` | Cached data |
| `/Library/Preferences` | Plist settings |
| `/tmp` | Temporary files |
| `/Library/Cookies` | Cookie storage |
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""iOS app security analysis agent using Objection/Frida concepts.
Performs runtime security assessment of iOS apps including SSL pinning bypass,
keychain dumping, filesystem inspection, and jailbreak detection bypass.
"""
import subprocess
import json
import os
import sys
import re
def run_objection(command, app_id=None, timeout=30):
"""Execute an Objection command against a target app."""
cmd = ["objection"]
if app_id:
cmd.extend(["-g", app_id])
cmd.extend(["explore", "-c", command])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return result.stdout, result.returncode
except FileNotFoundError:
return "objection not installed (pip install objection)", 1
except subprocess.TimeoutExpired:
return "Command timed out", 1
def run_frida(script_code, app_id, timeout=30):
"""Execute a Frida script against target app."""
cmd = ["frida", "-U", "-n", app_id, "-e", script_code]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return result.stdout, result.returncode
except FileNotFoundError:
return "frida not installed (pip install frida-tools)", 1
except subprocess.TimeoutExpired:
return "Command timed out", 1
def dump_keychain(app_id):
"""Dump keychain items accessible by the application."""
return run_objection("ios keychain dump", app_id)
def dump_cookies(app_id):
"""Dump HTTP cookies stored by the application."""
return run_objection("ios cookies get", app_id)
def list_classes(app_id, filter_str=None):
"""List Objective-C classes loaded in the app."""
cmd = "ios hooking list classes"
if filter_str:
cmd += f" --include {filter_str}"
return run_objection(cmd, app_id)
def check_ssl_pinning(app_id):
"""Check and bypass SSL certificate pinning."""
return run_objection("ios sslpinning disable", app_id)
def check_jailbreak_detection(app_id):
"""Check for and bypass jailbreak detection."""
return run_objection("ios jailbreak disable", app_id)
def inspect_filesystem(app_id, path="/"):
"""Inspect the application's filesystem sandbox."""
return run_objection(f"ls {path}", app_id)
def dump_plist(app_id):
"""Dump application plist configuration files."""
return run_objection("ios plist cat Info.plist", app_id)
def check_pasteboard(app_id):
"""Check pasteboard/clipboard for sensitive data."""
return run_objection("ios pasteboard monitor", app_id)
def search_binary_strings(app_id, pattern):
"""Search for strings in the app binary."""
return run_objection(f"memory search '{pattern}'", app_id)
OWASP_MOBILE_CHECKS = {
"M1_Improper_Platform_Usage": {
"checks": ["ios keychain dump", "ios plist cat Info.plist"],
"description": "Check for misuse of platform security features",
},
"M2_Insecure_Data_Storage": {
"checks": ["ios keychain dump", "ios cookies get", "ios nsuserdefaults get"],
"description": "Check for sensitive data in insecure storage",
},
"M3_Insecure_Communication": {
"checks": ["ios sslpinning disable"],
"description": "Test SSL/TLS implementation and certificate pinning",
},
"M4_Insecure_Authentication": {
"checks": ["ios hooking list classes --include Auth",
"ios hooking list classes --include Login"],
"description": "Analyze authentication mechanisms",
},
"M5_Insufficient_Cryptography": {
"checks": ["ios hooking list classes --include Crypto",
"ios hooking list classes --include AES"],
"description": "Review cryptographic implementations",
},
"M8_Code_Tampering": {
"checks": ["ios jailbreak disable"],
"description": "Test runtime integrity and jailbreak detection",
},
"M9_Reverse_Engineering": {
"checks": ["ios hooking list classes"],
"description": "Assess reverse engineering protections",
},
}
def run_owasp_assessment(app_id):
"""Run OWASP Mobile Top 10 security checks."""
results = {}
for category, config in OWASP_MOBILE_CHECKS.items():
category_results = {"description": config["description"], "findings": []}
for check in config["checks"]:
output, rc = run_objection(check, app_id)
category_results["findings"].append({
"command": check,
"status": "success" if rc == 0 else "failed",
"output_preview": output[:200] if output else "",
})
results[category] = category_results
return results
FRIDA_SCRIPTS = {
"ssl_pinning_bypass": """
ObjC.choose(ObjC.classes.NSURLSessionConfiguration, {
onMatch: function(instance) {
instance['- setTLSMinimumSupportedProtocol:'](0);
}, onComplete: function() {}
});
""",
"jailbreak_bypass": """
var paths = ['/Applications/Cydia.app', '/usr/sbin/sshd', '/etc/apt'];
Interceptor.attach(ObjC.classes.NSFileManager['- fileExistsAtPath:'].implementation, {
onEnter: function(args) { this.path = ObjC.Object(args[2]).toString(); },
onLeave: function(retval) {
if (paths.some(p => this.path.includes(p))) retval.replace(0);
}
});
""",
"keychain_dump": """
var kSecClass = ObjC.classes.__NSDictionary.dictionaryWithObject_forKey_(
ObjC.classes.__NSCFConstantString.alloc().initWithUTF8String_('genp'),
ObjC.classes.__NSCFConstantString.alloc().initWithUTF8String_('class')
);
console.log('Keychain query prepared');
""",
}
def generate_report(app_id, assessment_results):
"""Generate iOS security assessment report."""
findings_count = sum(
len(cat["findings"]) for cat in assessment_results.values()
)
return {
"app_identifier": app_id,
"assessment_framework": "OWASP Mobile Top 10",
"categories_tested": len(assessment_results),
"total_checks": findings_count,
"results": assessment_results,
}
if __name__ == "__main__":
print("=" * 60)
print("iOS App Security Analysis Agent (Objection/Frida)")
print("Runtime analysis, SSL bypass, keychain dump, OWASP checks")
print("=" * 60)
app_id = sys.argv[1] if len(sys.argv) > 1 else None
if not app_id:
print("\n[DEMO] Usage: python agent.py <app_bundle_id>")
print(" e.g. python agent.py com.example.app")
print("\nAvailable checks:")
for category, config in OWASP_MOBILE_CHECKS.items():
print(f" {category}: {config['description']}")
print("\nFrida scripts available:")
for name in FRIDA_SCRIPTS:
print(f" {name}")
sys.exit(0)
print(f"\n[*] Target: {app_id}")
print("[*] Running OWASP Mobile Top 10 assessment...")
results = run_owasp_assessment(app_id)
report = generate_report(app_id, results)
for category, data in results.items():
status_counts = {"success": 0, "failed": 0}
for f in data["findings"]:
status_counts[f["status"]] += 1
print(f"\n [{category}] {data['description']}")
print(f" Checks: {status_counts['success']} passed, {status_counts['failed']} failed")
print(f"\n{json.dumps(report, indent=2, default=str)}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,43 @@
---
name: analyzing-kubernetes-audit-logs
description: >
Parses Kubernetes API server audit logs (JSON lines) to detect exec-into-pod, secret
access, RBAC modifications, privileged pod creation, and anonymous API access. Builds
threat detection rules from audit event patterns. Use when investigating Kubernetes
cluster compromise or building k8s-specific SIEM detection rules.
---
# Analyzing Kubernetes Audit Logs
## Instructions
Parse Kubernetes audit log files (JSON lines format) to detect security-relevant
events including unauthorized access, privilege escalation, and data exfiltration.
```python
import json
with open("/var/log/kubernetes/audit.log") as f:
for line in f:
event = json.loads(line)
verb = event.get("verb")
resource = event.get("objectRef", {}).get("resource")
user = event.get("user", {}).get("username")
if verb == "create" and resource == "pods/exec":
print(f"Pod exec by {user}")
```
Key events to detect:
1. pods/exec and pods/attach (shell into containers)
2. secrets access (get/list/watch)
3. clusterrolebindings creation (RBAC escalation)
4. Privileged pod creation
5. Anonymous or system:unauthenticated access
## Examples
```python
# Detect secret enumeration
if verb in ("get", "list") and resource == "secrets":
print(f"Secret access: {user} -> {event['objectRef'].get('name')}")
```
@@ -0,0 +1,57 @@
# API Reference: Analyzing Kubernetes Audit Logs
## Audit Log Format (JSON Lines)
```json
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "RequestResponse",
"verb": "create",
"user": {"username": "admin", "groups": ["system:masters"]},
"sourceIPs": ["10.0.0.5"],
"objectRef": {
"resource": "pods",
"subresource": "exec",
"namespace": "default",
"name": "web-pod"
},
"responseStatus": {"code": 200},
"requestReceivedTimestamp": "2025-03-15T14:00:00Z"
}
```
## Security-Critical Audit Events
| Event | objectRef | Severity |
|-------|-----------|----------|
| Pod exec | `resource: pods, subresource: exec` | HIGH |
| Secret access | `resource: secrets, verb: get/list` | HIGH |
| RBAC change | `resource: clusterrolebindings` | CRITICAL |
| Privileged pod | `requestObject.spec.containers[].securityContext.privileged` | CRITICAL |
| Anonymous access | `user.username: system:anonymous` | CRITICAL |
## Audit Policy Levels
| Level | Captures |
|-------|----------|
| None | No logging |
| Metadata | Timestamp, user, verb, resource |
| Request | Metadata + request body |
| RequestResponse | Request + response body |
## Python Parsing
```python
import json
with open("audit.log") as f:
for line in f:
event = json.loads(line)
print(event["verb"], event["objectRef"]["resource"])
```
### References
- K8s Auditing: https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/
- Audit policy: https://kubernetes.io/docs/reference/config-api/apiserver-audit.v1/
- Datadog k8s audit: https://www.datadoghq.com/blog/monitor-kubernetes-audit-logs/
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Agent for analyzing Kubernetes audit logs for security threats."""
import os
import json
import argparse
from collections import defaultdict
from datetime import datetime
def parse_audit_log(log_path):
"""Parse Kubernetes audit log file (JSON lines format)."""
events = []
with open(log_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
events.append(json.loads(line))
except json.JSONDecodeError:
continue
return events
def detect_pod_exec(events):
"""Detect kubectl exec and attach events (shell access to pods)."""
findings = []
for event in events:
obj_ref = event.get("objectRef", {})
subresource = obj_ref.get("subresource", "")
if subresource in ("exec", "attach"):
findings.append({
"timestamp": event.get("requestReceivedTimestamp", ""),
"user": event.get("user", {}).get("username", ""),
"groups": event.get("user", {}).get("groups", []),
"verb": event.get("verb", ""),
"namespace": obj_ref.get("namespace", ""),
"pod": obj_ref.get("name", ""),
"subresource": subresource,
"source_ip": event.get("sourceIPs", [""])[0],
"severity": "HIGH",
})
return findings
def detect_secret_access(events):
"""Detect access to Kubernetes secrets."""
findings = []
for event in events:
obj_ref = event.get("objectRef", {})
if obj_ref.get("resource") != "secrets":
continue
verb = event.get("verb", "")
if verb not in ("get", "list", "watch", "create", "update", "delete"):
continue
findings.append({
"timestamp": event.get("requestReceivedTimestamp", ""),
"user": event.get("user", {}).get("username", ""),
"verb": verb,
"namespace": obj_ref.get("namespace", ""),
"secret_name": obj_ref.get("name", ""),
"source_ip": event.get("sourceIPs", [""])[0],
"severity": "HIGH" if verb in ("list", "delete") else "MEDIUM",
})
return findings
def detect_rbac_changes(events):
"""Detect RBAC role and binding modifications."""
rbac_resources = {"clusterroles", "clusterrolebindings", "roles", "rolebindings"}
findings = []
for event in events:
obj_ref = event.get("objectRef", {})
resource = obj_ref.get("resource", "")
verb = event.get("verb", "")
if resource in rbac_resources and verb in ("create", "update", "patch", "delete"):
findings.append({
"timestamp": event.get("requestReceivedTimestamp", ""),
"user": event.get("user", {}).get("username", ""),
"verb": verb,
"resource": resource,
"name": obj_ref.get("name", ""),
"namespace": obj_ref.get("namespace", ""),
"source_ip": event.get("sourceIPs", [""])[0],
"severity": "CRITICAL" if "cluster" in resource else "HIGH",
})
return findings
def detect_privileged_pods(events):
"""Detect creation of privileged pods."""
findings = []
for event in events:
if event.get("verb") != "create":
continue
obj_ref = event.get("objectRef", {})
if obj_ref.get("resource") != "pods":
continue
request_obj = event.get("requestObject", {})
spec = request_obj.get("spec", {})
containers = spec.get("containers", [])
for container in containers:
sc = container.get("securityContext", {})
if sc.get("privileged"):
findings.append({
"timestamp": event.get("requestReceivedTimestamp", ""),
"user": event.get("user", {}).get("username", ""),
"namespace": obj_ref.get("namespace", ""),
"pod": obj_ref.get("name", ""),
"container": container.get("name", ""),
"severity": "CRITICAL",
})
return findings
def detect_anonymous_access(events):
"""Detect API access by anonymous or unauthenticated users."""
findings = []
anon_users = {"system:anonymous", "system:unauthenticated"}
for event in events:
user = event.get("user", {}).get("username", "")
groups = event.get("user", {}).get("groups", [])
if user in anon_users or "system:unauthenticated" in groups:
status_code = event.get("responseStatus", {}).get("code", 0)
if status_code < 400:
findings.append({
"timestamp": event.get("requestReceivedTimestamp", ""),
"user": user,
"verb": event.get("verb", ""),
"resource": event.get("objectRef", {}).get("resource", ""),
"source_ip": event.get("sourceIPs", [""])[0],
"status_code": status_code,
"severity": "CRITICAL",
})
return findings
def detect_forbidden_surge(events, threshold=20):
"""Detect 403 surges indicating enumeration or brute force."""
user_forbidden = defaultdict(int)
for event in events:
if event.get("responseStatus", {}).get("code") == 403:
user = event.get("user", {}).get("username", "")
user_forbidden[user] += 1
surges = []
for user, count in user_forbidden.items():
if count >= threshold:
surges.append({"user": user, "forbidden_count": count, "severity": "MEDIUM"})
return sorted(surges, key=lambda x: x["forbidden_count"], reverse=True)
def main():
parser = argparse.ArgumentParser(description="Kubernetes Audit Log Analyzer")
parser.add_argument("--audit-log", required=True, help="Path to audit log file")
parser.add_argument("--output", default="k8s_audit_report.json")
parser.add_argument("--action", choices=[
"exec", "secrets", "rbac", "privileged", "anonymous", "full_analysis"
], default="full_analysis")
args = parser.parse_args()
events = parse_audit_log(args.audit_log)
report = {"log_file": args.audit_log, "total_events": len(events),
"generated_at": datetime.utcnow().isoformat(), "findings": {}}
print(f"[+] Parsed {len(events)} audit events")
if args.action in ("exec", "full_analysis"):
findings = detect_pod_exec(events)
report["findings"]["pod_exec"] = findings
print(f"[+] Pod exec/attach events: {len(findings)}")
if args.action in ("secrets", "full_analysis"):
findings = detect_secret_access(events)
report["findings"]["secret_access"] = findings
print(f"[+] Secret access events: {len(findings)}")
if args.action in ("rbac", "full_analysis"):
findings = detect_rbac_changes(events)
report["findings"]["rbac_changes"] = findings
print(f"[+] RBAC changes: {len(findings)}")
if args.action in ("privileged", "full_analysis"):
findings = detect_privileged_pods(events)
report["findings"]["privileged_pods"] = findings
print(f"[+] Privileged pod creation: {len(findings)}")
if args.action in ("anonymous", "full_analysis"):
findings = detect_anonymous_access(events)
report["findings"]["anonymous_access"] = findings
print(f"[+] Anonymous access events: {len(findings)}")
forbidden = detect_forbidden_surge(events)
report["findings"]["forbidden_surges"] = forbidden
print(f"[+] 403 surges: {len(forbidden)}")
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,119 @@
# API Reference: Linux ELF Malware Analysis Tools
## readelf - ELF Binary Inspection
### Syntax
```bash
readelf -h <binary> # ELF header
readelf -S <binary> # Section headers
readelf -l <binary> # Program headers (segments)
readelf -s <binary> # Symbol table
readelf -d <binary> # Dynamic section
readelf -r <binary> # Relocation entries
readelf -n <binary> # Notes section
```
### Key ELF Header Fields
| Field | Description |
|-------|-------------|
| `Class` | 32-bit or 64-bit |
| `Machine` | Architecture (x86-64, ARM, MIPS) |
| `Type` | EXEC (executable), DYN (shared object) |
| `Entry point` | Code execution start address |
## pyelftools - Python ELF Parsing
### Usage
```python
from elftools.elf.elffile import ELFFile
with open("binary", "rb") as f:
elf = ELFFile(f)
elf.elfclass # 32 or 64
elf.little_endian # True/False
elf.header.e_machine # Architecture
elf.header.e_entry # Entry point
elf.num_sections() # Section count
elf.get_section_by_name(".symtab") # Symbol table
```
## strings - String Extraction
### Syntax
```bash
strings <binary> # ASCII strings (default min 4)
strings -n 8 <binary> # Minimum 8 characters
strings -e l <binary> # 16-bit little-endian (Unicode)
strings -t x <binary> # Print offset in hex
```
## strace - System Call Tracing
### Syntax
```bash
strace -f ./binary # Follow forks
strace -e trace=network ./binary # Network calls only
strace -e trace=file ./binary # File operations only
strace -e trace=process ./binary # Process operations
strace -o output.txt ./binary # Log to file
strace -c ./binary # Summary statistics
```
### Key System Calls
| Call | Category |
|------|----------|
| `socket`, `connect`, `bind` | Network |
| `fork`, `execve`, `clone` | Process |
| `open`, `read`, `write`, `unlink` | File I/O |
| `ptrace` | Anti-debug/injection |
## ltrace - Library Call Tracing
### Syntax
```bash
ltrace -f ./binary # Follow child processes
ltrace -e malloc+free ./binary # Specific functions
ltrace -o output.txt ./binary # Log to file
```
## GDB - GNU Debugger
### Syntax
```bash
gdb ./binary
(gdb) break main
(gdb) break *0x400580 # Break at address
(gdb) run
(gdb) info registers
(gdb) x/20s $rdi # Examine string at RDI
(gdb) x/10i $rip # Disassemble at RIP
(gdb) bt # Backtrace
```
## UPX - Packer Detection/Unpacking
### Syntax
```bash
upx -t <binary> # Test if packed
upx -d <binary> # Decompress/unpack
upx -l <binary> # List compression details
```
## objdump - Disassembly
### Syntax
```bash
objdump -d <binary> # Disassemble .text
objdump -D <binary> # Disassemble all sections
objdump -M intel -d <binary> # Intel syntax
objdump -t <binary> # Symbol table
```
## nm - Symbol Listing
### Syntax
```bash
nm <binary> # List symbols
nm -D <binary> # Dynamic symbols only
nm -u <binary> # Undefined (imported) symbols
```
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""Linux ELF malware static analysis agent using pyelftools and binary inspection."""
import hashlib
import math
import os
import sys
import subprocess
import struct
from collections import Counter
try:
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
HAS_ELFTOOLS = True
except ImportError:
HAS_ELFTOOLS = False
def compute_hashes(filepath):
"""Compute MD5, SHA1, and SHA256 hashes of a file."""
md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
md5.update(chunk)
sha1.update(chunk)
sha256.update(chunk)
return {"md5": md5.hexdigest(), "sha1": sha1.hexdigest(), "sha256": sha256.hexdigest()}
def calculate_entropy(data):
"""Calculate Shannon entropy of binary data."""
if not data:
return 0.0
counter = Counter(data)
length = len(data)
return -sum((c / length) * math.log2(c / length) for c in counter.values())
def analyze_elf_header(filepath):
"""Parse ELF header and extract key properties."""
if not HAS_ELFTOOLS:
return {"error": "pyelftools not installed: pip install pyelftools"}
with open(filepath, "rb") as f:
elf = ELFFile(f)
symtab = elf.get_section_by_name(".symtab")
info = {
"class": f"{elf.elfclass}-bit",
"endian": "Little" if elf.little_endian else "Big",
"machine": elf.header.e_machine,
"type": elf.header.e_type,
"entry_point": f"0x{elf.header.e_entry:X}",
"stripped": symtab is None,
"num_sections": elf.num_sections(),
"num_segments": elf.num_segments(),
}
return info
def analyze_sections(filepath):
"""Analyze ELF sections for entropy and suspicious characteristics."""
if not HAS_ELFTOOLS:
return []
sections = []
with open(filepath, "rb") as f:
elf = ELFFile(f)
for section in elf.iter_sections():
data = section.data()
if len(data) == 0:
continue
entropy = calculate_entropy(data)
sections.append({
"name": section.name,
"type": section["sh_type"],
"size": len(data),
"entropy": round(entropy, 4),
"high_entropy": entropy > 7.0,
"flags": section["sh_flags"],
})
return sections
def extract_strings(filepath, min_length=6):
"""Extract ASCII strings from the binary and categorize by type."""
stdout, _, rc = subprocess.run(
f"strings -n {min_length} {filepath}", shell=True,
capture_output=True, text=True
).stdout, "", 0
if not stdout:
return {}
all_strings = stdout.strip().splitlines()
categorized = {
"urls": [], "ips": [], "domains": [], "shell_commands": [],
"crypto_mining": [], "persistence": [], "ssh_related": [],
"total": len(all_strings),
}
for s in all_strings:
s_lower = s.lower()
if any(proto in s_lower for proto in ["http://", "https://", "ftp://"]):
categorized["urls"].append(s)
if any(p in s_lower for p in ["stratum", "xmr", "monero", "pool.", "mining"]):
categorized["crypto_mining"].append(s)
if any(p in s_lower for p in ["crontab", "systemd", "init.d", "rc.local",
"ld.so.preload", "systemctl"]):
categorized["persistence"].append(s)
if any(p in s_lower for p in ["ssh", "authorized_keys", "id_rsa", "shadow", "passwd"]):
categorized["ssh_related"].append(s)
if any(p in s_lower for p in ["bash", "wget", "curl", "chmod", "/tmp/", "/dev/"]):
categorized["shell_commands"].append(s)
import re
if re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", s):
categorized["ips"].append(s)
if re.match(r"[a-zA-Z0-9.-]+\.(com|net|org|io|ru|cn|xyz)", s):
categorized["domains"].append(s)
return categorized
def check_packing(filepath):
"""Check if the binary is packed with UPX or other packers."""
with open(filepath, "rb") as f:
data = f.read(4096)
indicators = []
if b"UPX!" in data:
indicators.append("UPX packer detected (UPX! magic)")
if b"UPX0" in data or b"UPX1" in data:
indicators.append("UPX section names found")
stdout, _, _ = subprocess.run(f"upx -t {filepath} 2>&1", shell=True,
capture_output=True, text=True).stdout, "", 0
if stdout and "packed" in stdout.lower():
indicators.append("UPX verification confirms packing")
return indicators
def analyze_dynamic_linking(filepath):
"""Analyze dynamic linking information and imported functions."""
stdout, _, rc = subprocess.run(f"readelf -d {filepath}", shell=True,
capture_output=True, text=True).stdout, "", 0
dynamic_info = {"libraries": [], "rpath": None}
if stdout:
for line in stdout.splitlines():
if "NEEDED" in line:
lib = line.split("[")[-1].rstrip("]") if "[" in line else ""
dynamic_info["libraries"].append(lib)
if "RPATH" in line or "RUNPATH" in line:
dynamic_info["rpath"] = line.split("[")[-1].rstrip("]")
stdout2, _, _ = subprocess.run(
f"readelf -r {filepath} | grep -E 'socket|connect|exec|fork|open|write|bind|listen|send|recv'",
shell=True, capture_output=True, text=True
).stdout, "", 0
dynamic_info["suspicious_imports"] = [
line.strip() for line in (stdout2 or "").splitlines() if line.strip()
]
return dynamic_info
def detect_malware_type(strings_data):
"""Classify malware type based on extracted strings."""
classifications = []
if strings_data.get("crypto_mining"):
classifications.append("Cryptominer")
if any("flood" in s.lower() or "ddos" in s.lower()
for s in strings_data.get("shell_commands", [])):
classifications.append("DDoS Botnet")
if strings_data.get("ssh_related") and strings_data.get("persistence"):
classifications.append("Backdoor/Trojan")
if any("insmod" in s or "modprobe" in s or "init_module" in s
for s in strings_data.get("shell_commands", [])):
classifications.append("Rootkit")
if any("ransom" in s.lower() or "encrypt" in s.lower() or "bitcoin" in s.lower()
for cat in strings_data.values() if isinstance(cat, list) for s in cat):
classifications.append("Ransomware")
return classifications or ["Unknown"]
if __name__ == "__main__":
print("=" * 60)
print("Linux ELF Malware Analysis Agent")
print("Static analysis with pyelftools, strings, readelf")
print("=" * 60)
target = sys.argv[1] if len(sys.argv) > 1 else None
if target and os.path.exists(target):
print(f"\n[*] Analyzing: {target}")
print(f"[*] Size: {os.path.getsize(target)} bytes")
hashes = compute_hashes(target)
print(f"[*] MD5: {hashes['md5']}")
print(f"[*] SHA256: {hashes['sha256']}")
elf_info = analyze_elf_header(target)
print(f"\n--- ELF Header ---")
for k, v in elf_info.items():
print(f" {k}: {v}")
packing = check_packing(target)
if packing:
for p in packing:
print(f"[!] {p}")
sections = analyze_sections(target)
high_ent = [s for s in sections if s.get("high_entropy")]
if high_ent:
print(f"\n[!] High entropy sections (possible packing/encryption):")
for s in high_ent:
print(f" {s['name']}: entropy={s['entropy']}, size={s['size']}")
strings_data = extract_strings(target)
print(f"\n--- Strings Analysis ({strings_data.get('total', 0)} total) ---")
for category in ["urls", "ips", "domains", "crypto_mining", "persistence", "ssh_related"]:
items = strings_data.get(category, [])
if items:
print(f" {category}: {len(items)}")
for item in items[:5]:
print(f" - {item}")
classification = detect_malware_type(strings_data)
print(f"\n[*] Classification: {', '.join(classification)}")
else:
print(f"\n[DEMO] Usage: python agent.py <elf_binary>")
print("[*] Provide a Linux ELF binary for analysis.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,114 @@
# API Reference: Linux Forensic Artifact Analysis Tools
## Key Artifact Locations
| Artifact | Path | Description |
|----------|------|-------------|
| Auth logs | `/var/log/auth.log` (Debian) `/var/log/secure` (RHEL) | Authentication events |
| Login history | `/var/log/wtmp` | Successful logins (binary, use `last`) |
| Failed logins | `/var/log/btmp` | Failed logins (binary, use `lastb`) |
| Bash history | `~/.bash_history` | Command history per user |
| SSH keys | `~/.ssh/authorized_keys` | Authorized public keys |
| Crontab | `/etc/crontab`, `/var/spool/cron/crontabs/` | Scheduled tasks |
| Systemd services | `/etc/systemd/system/` | Service definitions |
| LD_PRELOAD | `/etc/ld.so.preload` | Shared library preloading |
| SUID binaries | `find / -perm -4000` | Setuid executables |
## last / lastb - Login History
### Syntax
```bash
last -f /var/log/wtmp # Successful logins
lastb -f /var/log/btmp # Failed logins
last -i -f /var/log/wtmp # Show IP addresses
last -s 2024-01-15 -t 2024-01-20 # Date range filter
```
### Output Format
```
user pts/0 192.168.1.50 Mon Jan 15 09:00 still logged in
```
## chkrootkit - Rootkit Scanner
### Syntax
```bash
chkrootkit # Full scan
chkrootkit -r /mnt/evidence # Scan mounted evidence
chkrootkit -q # Quiet (infected only)
```
## rkhunter - Rootkit Hunter
### Syntax
```bash
rkhunter --check # Full system check
rkhunter --check --rootdir /mnt/ev # Check evidence root
rkhunter --list tests # List available tests
rkhunter --propupd # Update file properties DB
```
### Check Categories
| Check | Description |
|-------|-------------|
| `rootkits` | Known rootkit signatures |
| `trojans` | Trojanized system binaries |
| `properties` | File permission anomalies |
| `filesystem` | Hidden files and directories |
## auditd Log Parsing
### ausearch Syntax
```bash
ausearch -m execve -ts recent # Recent command execution
ausearch -m USER_AUTH -ts today # Authentication events
ausearch -k suspicious_activity # Custom audit rule key
ausearch -ua 0 -ts today # Root user actions
```
### aureport Syntax
```bash
aureport --auth # Authentication summary
aureport --login # Login summary
aureport --file # File access summary
aureport --summary # Overall summary
```
## osquery - SQL-based System Queries
### Syntax
```bash
osqueryi "SELECT * FROM users WHERE uid = 0"
osqueryi "SELECT * FROM crontab"
osqueryi "SELECT * FROM authorized_keys"
osqueryi "SELECT * FROM suid_bin"
osqueryi "SELECT * FROM process_open_sockets"
```
### Key Tables
| Table | Content |
|-------|---------|
| `users` | User account information |
| `crontab` | Cron job entries |
| `authorized_keys` | SSH authorized keys |
| `suid_bin` | SUID binaries |
| `process_open_sockets` | Network connections by process |
| `shell_history` | Command history entries |
## Plaso / log2timeline - Super Timeline
### Syntax
```bash
log2timeline.py /cases/timeline.plaso /mnt/evidence
psort.py -o l2tcsv /cases/timeline.plaso > timeline.csv
psort.py -o l2tcsv /cases/timeline.plaso "date > '2024-01-15'"
```
## AIDE - File Integrity
### Syntax
```bash
aide --init # Initialize database
aide --check # Check for changes
aide --compare # Compare databases
```
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Linux system artifact forensics agent for investigating compromised systems."""
import os
import sys
import glob
import json
import re
import datetime
import subprocess
def run_cmd(cmd):
"""Execute a shell command and return output."""
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def analyze_passwd(passwd_path):
"""Analyze /etc/passwd for suspicious accounts."""
findings = []
with open(passwd_path, "r") as f:
for line in f:
parts = line.strip().split(":")
if len(parts) < 7:
continue
username, _, uid, gid = parts[0], parts[1], int(parts[2]), int(parts[3])
home, shell = parts[5], parts[6]
if uid == 0 and username != "root":
findings.append({
"severity": "CRITICAL",
"finding": f"UID 0 account: {username} (shell: {shell})",
})
login_shells = ["/bin/bash", "/bin/sh", "/bin/zsh", "/usr/bin/zsh"]
if uid < 1000 and uid > 0 and shell in login_shells:
findings.append({
"severity": "WARNING",
"finding": f"System account with login shell: {username} (UID:{uid})",
})
if uid >= 1000 and shell not in ["/bin/false", "/usr/sbin/nologin", "/bin/sync"]:
findings.append({
"severity": "INFO",
"finding": f"Interactive user: {username} (UID:{uid}, Home:{home})",
})
return findings
def analyze_shadow(shadow_path):
"""Analyze /etc/shadow for password hash types and status."""
findings = []
with open(shadow_path, "r") as f:
for line in f:
parts = line.strip().split(":")
if len(parts) < 3:
continue
username = parts[0]
pwd_hash = parts[1]
if pwd_hash and pwd_hash not in ("*", "!", "!!", ""):
hash_type = "Unknown"
if pwd_hash.startswith("$6$"):
hash_type = "SHA-512"
elif pwd_hash.startswith("$5$"):
hash_type = "SHA-256"
elif pwd_hash.startswith("$y$"):
hash_type = "yescrypt"
elif pwd_hash.startswith("$1$"):
hash_type = "MD5 (WEAK)"
findings.append({
"severity": "WARNING",
"finding": f"{username} uses weak MD5 password hash",
})
findings.append({
"severity": "INFO",
"finding": f"{username}: {hash_type} hash, last changed day {parts[2]}",
})
return findings
def analyze_bash_history(history_path, username="unknown"):
"""Analyze bash history for suspicious commands."""
suspicious_patterns = [
"wget", "curl", "nc ", "ncat", "netcat", "python -c", "python3 -c",
"perl -e", "base64", "chmod 777", "chmod +s", "/dev/tcp", "/dev/udp",
"nmap", "masscan", "hydra", "john", "hashcat", "passwd", "useradd",
"iptables -F", "ufw disable", "history -c", "rm -rf", "dd if=",
"crontab", "systemctl enable", "ssh-keygen", "scp ", "rsync",
"/tmp/", "/dev/shm/", "mkfifo", "socat",
]
findings = []
with open(history_path, "r", errors="ignore") as f:
lines = f.readlines()
for i, line in enumerate(lines):
line_stripped = line.strip()
for pattern in suspicious_patterns:
if pattern in line_stripped.lower():
findings.append({
"user": username,
"line_number": i + 1,
"command": line_stripped[:200],
"matched_pattern": pattern,
})
break
return findings
def check_cron_persistence(evidence_root):
"""Check cron jobs for persistence mechanisms."""
findings = []
cron_paths = [
os.path.join(evidence_root, "etc/crontab"),
*glob.glob(os.path.join(evidence_root, "etc/cron.d/*")),
*glob.glob(os.path.join(evidence_root, "var/spool/cron/crontabs/*")),
]
for cron_path in cron_paths:
if os.path.exists(cron_path) and os.path.isfile(cron_path):
with open(cron_path, "r", errors="ignore") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
suspicious = any(
p in line.lower()
for p in ["wget", "curl", "/tmp/", "/dev/shm/", "base64",
"python", "bash -i", "reverse", "nc ", "ncat"]
)
if suspicious:
findings.append({
"severity": "HIGH",
"source": cron_path,
"entry": line[:200],
})
return findings
def check_ssh_keys(evidence_root):
"""Check for unauthorized SSH authorized_keys."""
findings = []
key_files = glob.glob(
os.path.join(evidence_root, "home/*/.ssh/authorized_keys")
) + glob.glob(
os.path.join(evidence_root, "root/.ssh/authorized_keys")
)
for key_file in key_files:
if os.path.exists(key_file):
with open(key_file, "r") as f:
keys = [l.strip() for l in f if l.strip() and not l.startswith("#")]
if keys:
findings.append({
"file": key_file,
"key_count": len(keys),
"keys": [k[:80] + "..." for k in keys],
})
return findings
def check_systemd_persistence(evidence_root):
"""Check for suspicious systemd service files."""
findings = []
service_dirs = [
os.path.join(evidence_root, "etc/systemd/system"),
os.path.join(evidence_root, "usr/lib/systemd/system"),
]
for svc_dir in service_dirs:
if not os.path.exists(svc_dir):
continue
for svc_file in glob.glob(os.path.join(svc_dir, "*.service")):
with open(svc_file, "r", errors="ignore") as f:
content = f.read()
suspicious = any(
p in content.lower()
for p in ["/tmp/", "/dev/shm/", "wget", "curl", "reverse",
"bash -i", "nc ", "python", "base64"]
)
if suspicious:
findings.append({
"severity": "HIGH",
"file": svc_file,
"preview": content[:300],
})
return findings
def check_ld_preload(evidence_root):
"""Check for LD_PRELOAD rootkit indicators."""
findings = []
preload_path = os.path.join(evidence_root, "etc/ld.so.preload")
if os.path.exists(preload_path):
with open(preload_path, "r") as f:
content = f.read().strip()
if content:
findings.append({
"severity": "CRITICAL",
"finding": f"/etc/ld.so.preload contains: {content}",
})
return findings
def find_suid_binaries(evidence_root):
"""Find SUID/SGID binaries (potential privilege escalation)."""
stdout, _, rc = run_cmd(
f"find {evidence_root} -perm -4000 -type f 2>/dev/null"
)
return stdout.splitlines() if rc == 0 and stdout else []
def find_suspicious_tmp_files(evidence_root):
"""Find suspicious files in /tmp and /dev/shm."""
findings = []
for tmp_dir in ["tmp", "dev/shm"]:
full_path = os.path.join(evidence_root, tmp_dir)
if os.path.exists(full_path):
for root, dirs, files in os.walk(full_path):
for fname in files:
fpath = os.path.join(root, fname)
findings.append(fpath)
return findings
if __name__ == "__main__":
print("=" * 60)
print("Linux System Artifacts Forensics Agent")
print("User accounts, persistence, shell history, rootkit detection")
print("=" * 60)
evidence_root = sys.argv[1] if len(sys.argv) > 1 else "/mnt/evidence"
if os.path.exists(evidence_root):
print(f"\n[*] Examining evidence root: {evidence_root}")
passwd_path = os.path.join(evidence_root, "etc/passwd")
if os.path.exists(passwd_path):
print("\n--- User Account Analysis ---")
for f in analyze_passwd(passwd_path):
print(f" [{f['severity']}] {f['finding']}")
print("\n--- Cron Persistence ---")
cron = check_cron_persistence(evidence_root)
for c in cron:
print(f" [{c['severity']}] {c['source']}: {c['entry'][:80]}")
print("\n--- SSH Authorized Keys ---")
ssh = check_ssh_keys(evidence_root)
for s in ssh:
print(f" {s['file']}: {s['key_count']} keys")
print("\n--- Systemd Persistence ---")
systemd = check_systemd_persistence(evidence_root)
for s in systemd:
print(f" [{s['severity']}] {s['file']}")
print("\n--- LD_PRELOAD Rootkit Check ---")
ld = check_ld_preload(evidence_root)
for l in ld:
print(f" [{l['severity']}] {l['finding']}")
print("\n--- Suspicious Temp Files ---")
tmp = find_suspicious_tmp_files(evidence_root)
for t in tmp[:20]:
print(f" {t}")
else:
print(f"\n[DEMO] Usage: python agent.py <evidence_mount_point>")
print("[*] Mount a forensic image and provide the path for analysis.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,109 @@
# API Reference: LNK File and Jump List Forensics
## LECmd (Eric Zimmerman) - LNK Parser
### Syntax
```bash
LECmd.exe -f <file.lnk> # Single file
LECmd.exe -d <directory> --all # All files in directory
LECmd.exe -d <dir> --csv <output_dir> # CSV export
LECmd.exe -d <dir> --json <output_dir> # JSON export
LECmd.exe -f <file.lnk> -q # Quiet mode
LECmd.exe -d <dir> -r # Only removable drives
```
### Output Fields
| Field | Description |
|-------|-------------|
| SourceFile | Path to the .lnk file |
| TargetCreated | Target file creation timestamp |
| TargetModified | Target file modification timestamp |
| TargetAccessed | Target file access timestamp |
| FileSize | Target file size |
| RelativePath | Relative path to target |
| WorkingDirectory | Working directory for target |
| Arguments | Command-line arguments |
| LocalPath | Full local path to target |
| VolumeSerialNumber | Volume serial of target drive |
| DriveType | Fixed, Removable, Network |
| MachineID | NetBIOS name from tracker block |
| MacAddress | MAC from distributed tracker |
## JLECmd (Eric Zimmerman) - Jump List Parser
### Syntax
```bash
JLECmd.exe -f <jumplist_file> # Single file
JLECmd.exe -d <directory> # All jump lists
JLECmd.exe -d <dir> --csv <output> # CSV export
JLECmd.exe -d <dir> --fd # Full LNK details
JLECmd.exe -d <dir> --dumpTo <dir> # Extract embedded LNK files
```
### Jump List Locations
```
%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\
%APPDATA%\Microsoft\Windows\Recent\CustomDestinations\
```
## LnkParse3 (Python)
### Installation
```bash
pip install LnkParse3
```
### Usage
```python
import LnkParse3
with open("shortcut.lnk", "rb") as f:
lnk = LnkParse3.lnk_file(f)
info = lnk.get_json()
print(info["data"]["relative_path"])
print(info["header"]["creation_time"])
print(info["link_info"]["local_base_path"])
# Extra data blocks
extra = info.get("extra", {})
tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {})
print(tracker.get("machine_id"))
print(tracker.get("mac_address"))
```
## Shell Link Binary Format (MS-SHLLINK)
### Header Structure (76 bytes)
| Offset | Size | Field |
|--------|------|-------|
| 0 | 4 | HeaderSize (0x0000004C) |
| 4 | 16 | LinkCLSID |
| 20 | 4 | LinkFlags |
| 24 | 4 | FileAttributes |
| 28 | 8 | CreationTime (FILETIME) |
| 36 | 8 | AccessTime (FILETIME) |
| 44 | 8 | WriteTime (FILETIME) |
| 52 | 4 | FileSize |
| 56 | 4 | IconIndex |
| 60 | 4 | ShowCommand |
### Common App IDs (Jump Lists)
| App ID | Application |
|--------|-------------|
| 1b4dd67f29cb1962 | Windows Explorer |
| 5d696d521de238c3 | Google Chrome |
| ecd21b58c2f65a4f | Firefox |
| 1bc392b8e104a00e | Remote Desktop (mstsc) |
| b8ab77100df80ab2 | Microsoft Word |
| cfb56c56fa0f0478 | PuTTY |
| b74736c2bd8cc8a5 | WinSCP |
## Suspicious LNK Indicators
| Pattern | Concern |
|---------|---------|
| PowerShell in arguments | Script execution via shortcut |
| cmd.exe /c in target | Command execution chain |
| UNC path to IP | Network-based payload delivery |
| Base64 encoded arguments | Obfuscated commands |
| mshta/wscript target | Living-off-the-land execution |
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Windows LNK file and Jump List artifact analysis agent.
Parses Windows Shell Link (.lnk) files and Jump List artifacts to extract
file access evidence, program execution history, and user activity timelines.
Uses LnkParse3 for binary parsing and supports LECmd/JLECmd CSV output analysis.
"""
import struct
import os
import sys
import json
import hashlib
import datetime
import re
import glob as glob_mod
try:
import LnkParse3
HAS_LNKPARSE = True
except ImportError:
HAS_LNKPARSE = False
def compute_hash(filepath):
"""Compute SHA-256 hash of file."""
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
return sha256.hexdigest()
def parse_lnk_with_lnkparse3(filepath):
"""Parse LNK file using LnkParse3 library."""
if not HAS_LNKPARSE:
return {"error": "LnkParse3 not installed. pip install LnkParse3"}
with open(filepath, "rb") as f:
lnk = LnkParse3.lnk_file(f)
info = lnk.get_json()
result = {
"target_path": info.get("data", {}).get("relative_path", ""),
"working_dir": info.get("data", {}).get("working_directory", ""),
"arguments": info.get("data", {}).get("command_line_arguments", ""),
"icon_location": info.get("data", {}).get("icon_location", ""),
"description": info.get("data", {}).get("description", ""),
}
header = info.get("header", {})
result["creation_time"] = header.get("creation_time", "")
result["access_time"] = header.get("access_time", "")
result["write_time"] = header.get("write_time", "")
result["file_size"] = header.get("file_size", 0)
result["file_flags"] = header.get("file_attributes", "")
link_info = info.get("link_info", {})
if link_info:
result["local_base_path"] = link_info.get("local_base_path", "")
result["volume_serial"] = link_info.get("volume_serial_number", "")
result["volume_label"] = link_info.get("volume_label", "")
result["drive_type"] = link_info.get("drive_type", "")
extra = info.get("extra", {})
if extra:
tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {})
if tracker:
result["machine_id"] = tracker.get("machine_id", "")
result["mac_address"] = tracker.get("mac_address", "")
result["droid_volume_id"] = tracker.get("droid_volume_identifier", "")
result["droid_file_id"] = tracker.get("droid_file_identifier", "")
return result
def parse_lnk_header_raw(filepath):
"""Parse LNK file header manually from raw bytes."""
with open(filepath, "rb") as f:
data = f.read()
if len(data) < 76:
return {"error": "File too small for LNK header"}
# Shell Link Header (76 bytes)
header_size = struct.unpack_from("<I", data, 0)[0]
if header_size != 0x4C:
return {"error": f"Invalid header size: {header_size:#x} (expected 0x4C)"}
# CLSID check: 00021401-0000-0000-C000-000000000046
clsid = data[4:20]
expected_clsid = bytes.fromhex("01140200000000c0000000000000046".replace("0", "0"))
link_flags = struct.unpack_from("<I", data, 20)[0]
file_attrs = struct.unpack_from("<I", data, 24)[0]
creation_time = filetime_to_datetime(struct.unpack_from("<Q", data, 28)[0])
access_time = filetime_to_datetime(struct.unpack_from("<Q", data, 36)[0])
write_time = filetime_to_datetime(struct.unpack_from("<Q", data, 44)[0])
file_size = struct.unpack_from("<I", data, 52)[0]
icon_index = struct.unpack_from("<I", data, 56)[0]
show_command = struct.unpack_from("<I", data, 60)[0]
result = {
"header_size": header_size,
"link_flags": f"0x{link_flags:08X}",
"file_attributes": f"0x{file_attrs:08X}",
"creation_time": creation_time,
"access_time": access_time,
"write_time": write_time,
"target_file_size": file_size,
"icon_index": icon_index,
"show_command": {1: "Normal", 3: "Maximized", 7: "Minimized"}.get(show_command, str(show_command)),
"flags_decoded": decode_link_flags(link_flags),
}
return result
def filetime_to_datetime(filetime):
"""Convert Windows FILETIME to ISO string."""
if filetime == 0:
return "N/A"
try:
epoch = datetime.datetime(1601, 1, 1)
delta = datetime.timedelta(microseconds=filetime // 10)
return (epoch + delta).isoformat() + "Z"
except (OverflowError, OSError):
return "Invalid"
def decode_link_flags(flags):
"""Decode Shell Link header flags."""
flag_names = {
0x00000001: "HasLinkTargetIDList",
0x00000002: "HasLinkInfo",
0x00000004: "HasName",
0x00000008: "HasRelativePath",
0x00000010: "HasWorkingDir",
0x00000020: "HasArguments",
0x00000040: "HasIconLocation",
0x00000080: "IsUnicode",
0x00000100: "ForceNoLinkInfo",
0x00000800: "RunInSeparateProcess",
0x00001000: "HasDarwinID",
0x00002000: "RunAsUser",
0x00004000: "HasExpIcon",
0x00020000: "HasExpString",
0x00040000: "RunInSeparateProcess",
0x00080000: "PreferEnvironmentPath",
0x00200000: "DisableLinkPathTracking",
0x00800000: "EnableTargetMetadata",
0x04000000: "AllowLinkToLink",
}
decoded = []
for bit, name in flag_names.items():
if flags & bit:
decoded.append(name)
return decoded
JUMP_LIST_APP_IDS = {
"1b4dd67f29cb1962": "Windows Explorer",
"5d696d521de238c3": "Google Chrome",
"9b9cdc69c1c24e2b": "Notepad",
"f01b4d95cf55d32a": "Windows Explorer",
"a7bd71699cd38d1c": "Notepad++",
"918e0ecb43d17e23": "Notepad (Win10)",
"12dc1ea8e34b5a6": "Microsoft Paint",
"b8ab77100df80ab2": "Microsoft Word 2019",
"a4a5324453625195": "Microsoft Excel 2019",
"bc0c37e84e063727": "Microsoft PowerPoint 2019",
"9839aec31243a928": "Microsoft Outlook 2019",
"fb3b0dbfee58fac8": "Acrobat Reader DC",
"ecd21b58c2f65a4f": "Firefox",
"1bc392b8e104a00e": "Remote Desktop (mstsc)",
"b91050d8b077a4e8": "WinRAR",
"290532160612e071": "Windows Media Player",
"28c8b86deab549a1": "Internet Explorer",
"7e4dca80246863e3": "Control Panel",
"e2a593822e01aed3": "Snipping Tool",
"b74736c2bd8cc8a5": "WinSCP",
"cfb56c56fa0f0478": "PuTTY",
}
def scan_jump_lists(jump_list_dir):
"""Scan Jump List directory for automatic and custom destinations."""
results = []
auto_pattern = os.path.join(jump_list_dir, "*.automaticDestinations-ms")
custom_pattern = os.path.join(jump_list_dir, "*.customDestinations-ms")
for jl_file in sorted(glob_mod.glob(auto_pattern) + glob_mod.glob(custom_pattern)):
basename = os.path.basename(jl_file)
app_id = basename.split(".")[0]
jl_type = "automatic" if "automatic" in basename else "custom"
app_name = JUMP_LIST_APP_IDS.get(app_id, "Unknown Application")
results.append({
"file": basename,
"app_id": app_id,
"app_name": app_name,
"type": jl_type,
"size": os.path.getsize(jl_file),
"modified": datetime.datetime.fromtimestamp(
os.path.getmtime(jl_file)).isoformat(),
})
return results
def detect_suspicious_lnk(parsed_lnk):
"""Detect suspicious characteristics in LNK files."""
findings = []
args = parsed_lnk.get("arguments", "")
target = parsed_lnk.get("target_path", "") + " " + parsed_lnk.get("local_base_path", "")
suspicious_patterns = [
(r"powershell", "PowerShell execution via LNK"),
(r"cmd\.exe\s*/c", "Command prompt execution via LNK"),
(r"mshta", "MSHTA execution (HTA payload)"),
(r"certutil.*-decode", "CertUtil decode (file download)"),
(r"bitsadmin.*transfer", "BitsAdmin file download"),
(r"regsvr32.*scrobj", "Regsvr32 COM scriptlet execution"),
(r"wscript|cscript", "Script host execution"),
(r"\\\\[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\\", "UNC path to IP address"),
(r"http[s]?://", "URL in LNK arguments"),
(r"-enc\s+[A-Za-z0-9+/=]{20,}", "Base64-encoded PowerShell"),
]
combined = f"{target} {args}".lower()
for pattern, description in suspicious_patterns:
if re.search(pattern, combined, re.IGNORECASE):
findings.append({"indicator": description, "pattern": pattern})
if parsed_lnk.get("drive_type") == "DRIVE_REMOTE":
findings.append({"indicator": "Target on network drive", "pattern": "DRIVE_REMOTE"})
return findings
def scan_lnk_directory(directory):
"""Scan directory for LNK files and analyze each."""
results = []
for lnk_file in sorted(glob_mod.glob(os.path.join(directory, "*.lnk"))):
parsed = parse_lnk_with_lnkparse3(lnk_file) if HAS_LNKPARSE else parse_lnk_header_raw(lnk_file)
suspicious = detect_suspicious_lnk(parsed)
results.append({
"file": os.path.basename(lnk_file),
"sha256": compute_hash(lnk_file),
"parsed": parsed,
"suspicious": suspicious,
})
return results
if __name__ == "__main__":
print("=" * 60)
print("Windows LNK & Jump List Forensics Agent")
print("Shell Link parsing, Jump List analysis, suspicious detection")
print("=" * 60)
target = sys.argv[1] if len(sys.argv) > 1 else None
if not target or not os.path.exists(target):
print("\n[DEMO] Usage:")
print(" python agent.py <file.lnk> # Analyze single LNK")
print(" python agent.py <directory> # Scan directory for LNK/JumpList")
print(f"\n LnkParse3 available: {HAS_LNKPARSE}")
sys.exit(0)
if os.path.isfile(target) and target.lower().endswith(".lnk"):
print(f"\n[*] Analyzing: {target}")
print(f"[*] SHA-256: {compute_hash(target)}")
if HAS_LNKPARSE:
parsed = parse_lnk_with_lnkparse3(target)
else:
parsed = parse_lnk_header_raw(target)
print("\n--- LNK Properties ---")
for k, v in parsed.items():
print(f" {k}: {v}")
suspicious = detect_suspicious_lnk(parsed)
if suspicious:
print("\n--- Suspicious Indicators ---")
for s in suspicious:
print(f" [!] {s['indicator']}")
elif os.path.isdir(target):
print(f"\n[*] Scanning directory: {target}")
lnk_results = scan_lnk_directory(target)
print(f"[*] Found {len(lnk_results)} LNK files")
for r in lnk_results[:20]:
print(f" {r['file']}: {r['parsed'].get('target_path', r['parsed'].get('local_base_path', '?'))}")
for s in r.get("suspicious", []):
print(f" [!] {s['indicator']}")
jl_dir = os.path.join(target, "AutomaticDestinations")
if not os.path.isdir(jl_dir):
jl_dir = target
jl_results = scan_jump_lists(jl_dir)
if jl_results:
print(f"\n--- Jump Lists ({len(jl_results)}) ---")
for jl in jl_results:
print(f" {jl['app_name']:30s} [{jl['type']}] {jl['app_id']}")
print(f"\n{json.dumps({'lnk_count': len(lnk_results) if os.path.isdir(target) else 1}, indent=2)}")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,112 @@
# API Reference: Office Macro Malware Analysis Tools
## olevba - VBA Macro Extraction (oletools)
### CLI Syntax
```bash
olevba document.docm # Full analysis
olevba --decode --deobf document.docm # Decode + deobfuscate
olevba --code document.docm # Extract VBA source only
olevba --json document.docm # JSON output
olevba --reveal document.docm # Reveal hidden content
```
### Output Sections
| Section | Content |
|---------|---------|
| `AutoExec` | Auto-execution triggers (AutoOpen, Document_Open) |
| `Suspicious` | Dangerous functions (Shell, WScript, CreateObject) |
| `IOC` | Extracted indicators (URLs, IPs, file paths) |
| `Hex String` | Decoded hex-encoded strings |
### Python API
```python
from oletools.olevba import VBA_Parser
vba = VBA_Parser("document.docm")
if vba.detect_vba_macros():
for (fn, stream, vba_fn, code) in vba.extract_macros():
print(code)
for (kw_type, keyword, desc) in vba.analyze_macros():
print(f"{kw_type}: {keyword}")
vba.close()
```
## oleid - Document Capability Identification
### CLI Syntax
```bash
oleid document.docm
```
### Indicators
| Indicator | Risk Values |
|-----------|-------------|
| `VBA Macros` | True/False |
| `XLM Macros` | True/False |
| `External Relationships` | True/False |
| `ObjectPool` | True/False |
| `Flash` | True/False |
## oledump.py - OLE Stream Analysis
### CLI Syntax
```bash
oledump.py document.docm # List streams
oledump.py -s 8 -v document.docm # Extract stream 8
oledump.py -p plugin_vba_dco document.docm # VBA decompile
oledump.py -p plugin_msg.py document.msg # MSG file parsing
```
### Stream Markers
| Marker | Meaning |
|--------|---------|
| `M` | Contains VBA macros |
| `m` | Contains macro attributes |
| `O` | Contains OLE objects |
## XLMDeobfuscator - Excel 4.0 Macros
### CLI Syntax
```bash
xlmdeobfuscator -f document.xlsm
xlmdeobfuscator -f document.xlsm --output-format json
```
### Dangerous XLM Functions
| Function | Purpose |
|----------|---------|
| `EXEC()` | Execute shell command |
| `CALL()` | Call DLL function |
| `REGISTER()` | Register DLL function |
| `URLDownloadToFileA` | Download file from URL |
## VBA Auto-Execution Triggers
| Trigger | Application |
|---------|-------------|
| `Auto_Open` / `AutoOpen` | Word |
| `Document_Open` | Word |
| `Workbook_Open` | Excel |
| `Auto_Close` | Word |
| `AutoExec` | Word |
## VBA Suspicious Functions
| Function | Risk |
|----------|------|
| `Shell()` | Command execution |
| `WScript.Shell` | Windows scripting |
| `CreateObject()` | COM object instantiation |
| `URLDownloadToFile` | File download |
| `MSXML2.XMLHTTP` | HTTP requests |
| `ADODB.Stream` | Binary file writing |
| `CallByName` | Indirect method invocation |
| `Environ()` | Environment variable access |
## ViperMonkey - VBA Emulation
### Syntax
```bash
vmonkey document.docm
vmonkey --iocs document.docm # Extract IOCs only
```
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""Office macro malware analysis agent using oletools for VBA extraction and deobfuscation."""
import re
import os
import sys
import hashlib
import subprocess
import json
import zipfile
try:
from oletools.olevba import VBA_Parser, TYPE_OLE, TYPE_OpenXML
from oletools import oleid
HAS_OLETOOLS = True
except ImportError:
HAS_OLETOOLS = False
def compute_hash(filepath):
"""Compute SHA-256 hash of a file."""
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
return sha256.hexdigest()
def triage_document(filepath):
"""Quick triage using oleid to identify document capabilities."""
if not HAS_OLETOOLS:
return {"error": "oletools not installed: pip install oletools"}
oid = oleid.OleID(filepath)
indicators = oid.check()
results = {}
for indicator in indicators:
results[indicator.name] = {
"value": str(indicator.value),
"risk": indicator.risk,
"description": indicator.description,
}
return results
def extract_vba_macros(filepath):
"""Extract VBA macro code from an Office document."""
if not HAS_OLETOOLS:
return {"error": "oletools not installed"}
vba_parser = VBA_Parser(filepath)
macros = []
if vba_parser.detect_vba_macros():
for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros():
macros.append({
"filename": filename,
"stream_path": stream_path,
"vba_filename": vba_filename,
"code": vba_code,
"code_length": len(vba_code),
})
vba_parser.close()
return macros
def analyze_vba_suspicious(filepath):
"""Analyze VBA macros for suspicious keywords and patterns."""
if not HAS_OLETOOLS:
return {"error": "oletools not installed"}
vba_parser = VBA_Parser(filepath)
analysis = {"auto_exec": [], "suspicious": [], "iocs": [], "hex_strings": []}
if vba_parser.detect_vba_macros():
results = vba_parser.analyze_macros()
for (kw_type, keyword, description) in results:
entry = {"type": kw_type, "keyword": keyword, "description": description}
if kw_type == "AutoExec":
analysis["auto_exec"].append(entry)
elif kw_type == "Suspicious":
analysis["suspicious"].append(entry)
elif kw_type == "IOC":
analysis["iocs"].append(entry)
elif kw_type == "Hex String":
analysis["hex_strings"].append(entry)
vba_parser.close()
return analysis
def deobfuscate_chr_calls(vba_code):
"""Resolve Chr() and ChrW() calls in VBA code."""
def resolve_chr(match):
try:
return chr(int(match.group(1)))
except (ValueError, OverflowError):
return match.group(0)
code = re.sub(r'Chr\$?\((\d+)\)', resolve_chr, vba_code)
code = re.sub(r'ChrW\$?\((\d+)\)', resolve_chr, code)
return code
def deobfuscate_concatenation(vba_code):
"""Remove string concatenation: "abc" & "def" -> "abcdef"."""
return re.sub(r'"\s*&\s*"', '', vba_code)
def deobfuscate_strreverse(vba_code):
"""Resolve StrReverse() calls."""
def resolve_reverse(match):
return '"' + match.group(1)[::-1] + '"'
return re.sub(r'StrReverse\("([^"]+)"\)', resolve_reverse, vba_code)
def deobfuscate_replace(vba_code):
"""Resolve Replace() function calls."""
def resolve_replace(match):
original = match.group(1)
find = match.group(2)
replace_with = match.group(3)
return '"' + original.replace(find, replace_with) + '"'
return re.sub(r'Replace\("([^"]+)",\s*"([^"]+)",\s*"([^"]*)"\)',
resolve_replace, vba_code)
def full_deobfuscation(vba_code):
"""Apply all deobfuscation techniques to VBA code."""
code = deobfuscate_chr_calls(vba_code)
code = deobfuscate_concatenation(code)
code = deobfuscate_strreverse(code)
code = deobfuscate_replace(code)
return code
def extract_urls_from_code(code):
"""Extract URLs from deobfuscated VBA code."""
return list(set(re.findall(r'https?://[^\s"\'<>]+', code)))
def check_dde(filepath):
"""Check for DDE (Dynamic Data Exchange) attacks in OOXML documents."""
findings = []
try:
z = zipfile.ZipFile(filepath)
for name in z.namelist():
if name.endswith(".xml") or name.endswith(".rels"):
content = z.read(name).decode("utf-8", errors="ignore")
if "DDEAUTO" in content or "DDE " in content:
dde_cmds = re.findall(r'DDEAUTO[^"]*"([^"]+)"', content)
findings.append({
"type": "DDE",
"file": name,
"commands": dde_cmds,
})
if "attachedTemplate" in content or "Target=" in content:
urls = re.findall(r'Target="(https?://[^"]+)"', content)
for url in urls:
findings.append({
"type": "Remote Template",
"file": name,
"url": url,
})
except (zipfile.BadZipFile, KeyError):
pass
return findings
def check_external_relationships(filepath):
"""Check OOXML relationships for external references."""
externals = []
try:
z = zipfile.ZipFile(filepath)
for name in z.namelist():
if ".rels" in name:
content = z.read(name).decode("utf-8", errors="ignore")
urls = re.findall(r'Target="(https?://[^"]+)"', content)
for url in urls:
externals.append({"file": name, "url": url})
except (zipfile.BadZipFile, KeyError):
pass
return externals
def generate_report(filepath, triage, macros, analysis, deobfuscated_urls, dde_findings):
"""Generate a comprehensive macro malware analysis report."""
report = {
"file": filepath,
"sha256": compute_hash(filepath),
"size": os.path.getsize(filepath),
"triage": triage,
"macro_count": len(macros),
"auto_exec_triggers": [e["keyword"] for e in analysis.get("auto_exec", [])],
"suspicious_functions": [e["keyword"] for e in analysis.get("suspicious", [])],
"iocs": [e["keyword"] for e in analysis.get("iocs", [])],
"extracted_urls": deobfuscated_urls,
"dde_findings": dde_findings,
}
return report
if __name__ == "__main__":
print("=" * 60)
print("Office Macro Malware Analysis Agent")
print("oletools-based VBA extraction and deobfuscation")
print("=" * 60)
target = sys.argv[1] if len(sys.argv) > 1 else None
if target and os.path.exists(target):
print(f"\n[*] Analyzing: {target}")
print(f"[*] SHA-256: {compute_hash(target)}")
print("\n--- Document Triage (oleid) ---")
triage = triage_document(target)
for name, info in triage.items():
risk_tag = f" [{info['risk']}]" if info.get("risk") else ""
print(f" {name}: {info['value']}{risk_tag}")
print("\n--- VBA Macro Extraction ---")
macros = extract_vba_macros(target)
print(f" Macro streams found: {len(macros)}")
for m in macros:
print(f" - {m['vba_filename']} ({m['code_length']} chars)")
print("\n--- Suspicious Analysis ---")
analysis = analyze_vba_suspicious(target)
for trigger in analysis["auto_exec"]:
print(f" [!] Auto-exec: {trigger['keyword']}")
for sus in analysis["suspicious"]:
print(f" [!] Suspicious: {sus['keyword']} - {sus['description']}")
for ioc in analysis["iocs"]:
print(f" [IOC] {ioc['keyword']}")
print("\n--- Deobfuscation ---")
all_urls = []
for m in macros:
deobfuscated = full_deobfuscation(m["code"])
urls = extract_urls_from_code(deobfuscated)
all_urls.extend(urls)
for url in set(all_urls):
print(f" URL: {url}")
print("\n--- DDE / Remote Template Check ---")
dde = check_dde(target)
for d in dde:
print(f" [{d['type']}] {d.get('url', d.get('commands', ''))}")
report = generate_report(target, triage, macros, analysis, list(set(all_urls)), dde)
print(f"\n[*] Report: {json.dumps(report, indent=2, default=str)[:500]}...")
else:
print(f"\n[DEMO] Usage: python agent.py <document.docm|xlsm>")
print("[*] Provide an Office document for macro analysis.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,68 @@
# API Reference: urlscan.io URL Analysis
## Base URL
```
https://urlscan.io/api/v1
```
## Authentication
```
API-Key: YOUR_API_KEY
```
## Submit Scan
```
POST /scan/
```
```json
{"url": "https://example.com", "visibility": "private"}
```
| Field | Values | Description |
|-------|--------|-------------|
| `url` | URL string | URL to scan |
| `visibility` | public/unlisted/private | Scan visibility |
Response: `{"uuid": "...", "result": "https://urlscan.io/result/UUID/", "api": "..."}`
## Get Result
```
GET /result/{uuid}/
```
Returns 404 while scanning, 200 when complete.
## Search
```
GET /search/?q=domain:example.com&size=100
```
Query fields: `domain:`, `ip:`, `server:`, `country:`, `filename:`, `hash:`
## Result Structure
| Field | Description |
|-------|-------------|
| `page.url` | Final URL after redirects |
| `page.domain` | Domain name |
| `page.ip` | Resolved IP |
| `page.country` | Server country |
| `page.status` | HTTP status code |
| `page.title` | Page title |
| `page.server` | Server header |
| `page.tlsIssuer` | TLS certificate issuer |
| `verdicts.overall.malicious` | Boolean malicious verdict |
| `verdicts.overall.score` | Risk score (0-100) |
| `lists.ips` | List of contacted IPs |
| `lists.certificates` | TLS certificates observed |
| `stats.resourceStats` | Resource type statistics |
## Screenshot
```
GET /screenshots/{uuid}.png
```
## DOM Snapshot
```
GET /dom/{uuid}/
```
## Rate Limits
- Free: 100 scans/day, 1000 searches/day
- Paid: Higher limits per plan
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""URLScan.io Malicious URL Analysis Agent - Submits and analyzes URLs via the urlscan.io API."""
import json
import time
import logging
import argparse
from datetime import datetime
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
URLSCAN_API = "https://urlscan.io/api/v1"
def submit_url(url, api_key, visibility="private"):
"""Submit a URL to urlscan.io for scanning."""
headers = {"API-Key": api_key, "Content-Type": "application/json"}
payload = {"url": url, "visibility": visibility}
resp = requests.post(f"{URLSCAN_API}/scan/", headers=headers, json=payload, timeout=30)
resp.raise_for_status()
data = resp.json()
logger.info("Submitted URL: %s -> scan UUID: %s", url, data.get("uuid"))
return data
def get_scan_result(uuid, api_key, max_wait=120):
"""Poll for scan results until complete."""
headers = {"API-Key": api_key}
for _ in range(max_wait // 5):
try:
resp = requests.get(f"{URLSCAN_API}/result/{uuid}/", headers=headers, timeout=30)
if resp.status_code == 200:
return resp.json()
except requests.RequestException:
pass
time.sleep(5)
return None
def search_urlscan(query, api_key, size=100):
"""Search urlscan.io for existing scans."""
headers = {"API-Key": api_key}
params = {"q": query, "size": size}
resp = requests.get(f"{URLSCAN_API}/search/", headers=headers, params=params, timeout=30)
resp.raise_for_status()
return resp.json().get("results", [])
def analyze_result(result):
"""Analyze urlscan.io scan result for malicious indicators."""
findings = []
verdicts = result.get("verdicts", {})
overall = verdicts.get("overall", {})
urlscan_verdict = verdicts.get("urlscan", {})
community = verdicts.get("community", {})
if overall.get("malicious"):
findings.append({"type": "Malicious verdict", "severity": "critical", "source": "overall", "score": overall.get("score", 0)})
if urlscan_verdict.get("malicious"):
findings.append({"type": "URLScan malicious", "severity": "critical", "score": urlscan_verdict.get("score", 0)})
if community.get("score", 0) < 0:
findings.append({"type": "Negative community score", "severity": "high", "score": community.get("score")})
page = result.get("page", {})
lists = result.get("lists", {})
stats = result.get("stats", {})
if lists.get("ips", []):
for ip in lists["ips"]:
if ip.get("malicious"):
findings.append({"type": "Malicious IP contacted", "severity": "high", "ip": ip.get("ip"), "asn": ip.get("asn")})
for cert in lists.get("certificates", []):
if cert.get("validTo"):
try:
exp = datetime.fromisoformat(cert["validTo"].replace("Z", "+00:00"))
if exp < datetime.now(exp.tzinfo):
findings.append({"type": "Expired TLS certificate", "severity": "medium", "subject": cert.get("subjectName")})
except (ValueError, TypeError):
pass
js_count = len([r for r in stats.get("resourceStats", []) if "javascript" in r.get("type", "").lower()])
if js_count > 20:
findings.append({"type": "High JavaScript resource count", "severity": "medium", "count": js_count})
redirects = stats.get("uniqCountries", 0)
if result.get("data", {}).get("requests"):
redirect_chain = [r.get("request", {}).get("redirectHasExtraInfo") for r in result["data"]["requests"][:5]]
return {
"url": page.get("url", ""),
"domain": page.get("domain", ""),
"ip": page.get("ip", ""),
"country": page.get("country", ""),
"server": page.get("server", ""),
"status_code": page.get("status", 0),
"title": page.get("title", ""),
"mime_type": page.get("mimeType", ""),
"tls_issuer": page.get("tlsIssuer", ""),
"overall_malicious": overall.get("malicious", False),
"overall_score": overall.get("score", 0),
"findings": findings,
}
def bulk_analyze(urls, api_key):
"""Submit and analyze multiple URLs."""
results = []
for url in urls:
try:
submission = submit_url(url, api_key)
uuid = submission.get("uuid")
if uuid:
result = get_scan_result(uuid, api_key)
if result:
analysis = analyze_result(result)
results.append(analysis)
else:
results.append({"url": url, "error": "Scan timeout"})
except requests.RequestException as e:
results.append({"url": url, "error": str(e)})
return results
def generate_report(analyses):
"""Generate URL analysis report."""
malicious = [a for a in analyses if a.get("overall_malicious")]
report = {
"timestamp": datetime.utcnow().isoformat(),
"urls_analyzed": len(analyses),
"malicious_count": len(malicious),
"results": analyses,
}
print(f"URLSCAN REPORT: {len(analyses)} URLs analyzed, {len(malicious)} malicious")
return report
def main():
parser = argparse.ArgumentParser(description="URLScan.io Malicious URL Analysis Agent")
parser.add_argument("--api-key", required=True, help="urlscan.io API key")
parser.add_argument("--url", help="Single URL to scan")
parser.add_argument("--url-file", help="File with URLs (one per line)")
parser.add_argument("--search", help="Search query for existing scans")
parser.add_argument("--output", default="urlscan_report.json")
args = parser.parse_args()
urls = []
if args.url:
urls.append(args.url)
if args.url_file:
with open(args.url_file) as f:
urls.extend(line.strip() for line in f if line.strip())
if args.search:
results = search_urlscan(args.search, args.api_key)
analyses = [analyze_result(r) for r in results if "page" in r]
else:
analyses = bulk_analyze(urls, args.api_key)
report = generate_report(analyses)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,121 @@
# API Reference: Cuckoo Sandbox
## Cuckoo CLI
### Sample Submission
```bash
cuckoo submit /path/to/sample.exe
cuckoo submit --timeout 300 /path/to/sample.exe
cuckoo submit --machine win10_x64 --package exe sample.exe
cuckoo submit --url "http://malicious-url.com"
```
### Status
```bash
cuckoo status
tail -f /opt/cuckoo/log/cuckoo.log
```
## Cuckoo REST API
### Submit File
```bash
curl -F "file=@sample.exe" -F "timeout=300" \
http://localhost:8090/tasks/create/file
```
Response: `{"task_id": 1}`
### Submit URL
```bash
curl -F "url=http://malicious.com" -F "timeout=300" \
http://localhost:8090/tasks/create/url
```
### Check Task Status
```bash
curl http://localhost:8090/tasks/view/<task_id>
```
Status values: `pending`, `running`, `completed`, `reported`
### Get Report
```bash
curl http://localhost:8090/tasks/report/<task_id>
curl http://localhost:8090/tasks/report/<task_id>/json
```
### List Tasks
```bash
curl http://localhost:8090/tasks/list
curl http://localhost:8090/tasks/list?limit=50&offset=0
```
## Report JSON Structure
### Key Paths
| Path | Content |
|------|---------|
| `info.score` | Threat score (0-10) |
| `info.duration` | Analysis duration (seconds) |
| `behavior.processes` | Process tree with API calls |
| `behavior.summary.files` | Created/modified files |
| `behavior.summary.keys` | Modified registry keys |
| `network.dns` | DNS resolutions |
| `network.http` | HTTP requests |
| `network.tcp` | TCP connections |
| `dropped` | Dropped files with hashes |
| `signatures` | Triggered behavioral signatures |
### Signature Severity Levels
| Level | Meaning |
|-------|---------|
| 1 | Informational |
| 2 | Low |
| 3 | Medium |
| 4 | High |
| 5 | Critical |
## Analysis Packages
| Package | File Type |
|---------|-----------|
| `exe` | Windows executables |
| `dll` | DLL files (uses rundll32) |
| `doc` | Word documents |
| `xls` | Excel spreadsheets |
| `pdf` | PDF documents |
| `js` | JavaScript files |
| `vbs` | VBScript files |
| `ps1` | PowerShell scripts |
| `zip` | Archives (auto-extracted) |
## InetSim - Network Simulation
### Syntax
```bash
inetsim --bind-address 192.168.56.1
inetsim --report-dir /var/log/inetsim
```
### Simulated Services
- HTTP/HTTPS (ports 80, 443)
- DNS (port 53)
- SMTP (port 25)
- FTP (port 21)
- IRC (port 6667)
## FakeNet-NG - Network Redirection
### Syntax
```bash
fakenet
fakenet -c custom_config.ini
```
## Volatility Integration
### Syntax
```bash
vol3 -f /opt/cuckoo/storage/analyses/<id>/memory.dmp windows.pslist
vol3 -f /opt/cuckoo/storage/analyses/<id>/memory.dmp windows.malfind
vol3 -f /opt/cuckoo/storage/analyses/<id>/memory.dmp windows.netscan
```
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""Cuckoo Sandbox behavioral analysis agent for automated malware detonation and reporting."""
import json
import os
import sys
import subprocess
import hashlib
import datetime
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
CUCKOO_API = os.environ.get("CUCKOO_API", "http://localhost:8090")
CUCKOO_STORAGE = os.environ.get("CUCKOO_STORAGE", "/opt/cuckoo/storage/analyses")
def submit_file(filepath, timeout=300, machine=None, package=None):
"""Submit a malware sample to Cuckoo via REST API."""
if not HAS_REQUESTS:
return None
url = f"{CUCKOO_API}/tasks/create/file"
files = {"file": (os.path.basename(filepath), open(filepath, "rb"))}
data = {"timeout": timeout}
if machine:
data["machine"] = machine
if package:
data["package"] = package
resp = requests.post(url, files=files, data=data)
if resp.status_code == 200:
return resp.json().get("task_id")
return None
def submit_url(url_to_analyze, timeout=300):
"""Submit a URL to Cuckoo for analysis."""
if not HAS_REQUESTS:
return None
url = f"{CUCKOO_API}/tasks/create/url"
data = {"url": url_to_analyze, "timeout": timeout}
resp = requests.post(url, data=data)
if resp.status_code == 200:
return resp.json().get("task_id")
return None
def get_task_status(task_id):
"""Check the status of a Cuckoo analysis task."""
if not HAS_REQUESTS:
return None
url = f"{CUCKOO_API}/tasks/view/{task_id}"
resp = requests.get(url)
if resp.status_code == 200:
return resp.json().get("task", {}).get("status")
return None
def load_report(task_id, report_dir=None):
"""Load a Cuckoo JSON report from disk."""
if report_dir is None:
report_dir = CUCKOO_STORAGE
report_path = os.path.join(report_dir, str(task_id), "reports", "report.json")
if os.path.exists(report_path):
with open(report_path, "r") as f:
return json.load(f)
return None
def analyze_processes(report):
"""Extract and analyze the process tree from the Cuckoo report."""
processes = []
for proc in report.get("behavior", {}).get("processes", []):
pid = proc.get("pid")
ppid = proc.get("ppid")
name = proc.get("process_name")
suspicious_apis = []
dangerous_apis = [
"CreateRemoteThread", "VirtualAllocEx", "WriteProcessMemory",
"NtCreateThreadEx", "RegSetValueExA", "URLDownloadToFileA",
"ShellExecuteA", "ShellExecuteW", "WinExec", "CreateProcessA",
"NtWriteVirtualMemory", "QueueUserAPC",
]
for call in proc.get("calls", []):
if call.get("api") in dangerous_apis:
args = {arg["name"]: arg["value"] for arg in call.get("arguments", [])}
suspicious_apis.append({"api": call["api"], "args": args})
processes.append({
"pid": pid,
"ppid": ppid,
"name": name,
"suspicious_api_calls": len(suspicious_apis),
"top_suspicious": suspicious_apis[:10],
})
return processes
def analyze_network(report):
"""Extract network activity from the Cuckoo report."""
network = report.get("network", {})
return {
"dns": [
{"request": d.get("request"), "answers": d.get("answers", [])}
for d in network.get("dns", [])
],
"http": [
{"method": h.get("method"), "host": h.get("host"),
"uri": h.get("uri"), "body_size": len(h.get("body", ""))}
for h in network.get("http", [])
],
"tcp_connections": [
{"src": t.get("src"), "sport": t.get("sport"),
"dst": t.get("dst"), "dport": t.get("dport")}
for t in network.get("tcp", [])
],
"udp_connections": [
{"src": u.get("src"), "sport": u.get("sport"),
"dst": u.get("dst"), "dport": u.get("dport")}
for u in network.get("udp", [])
],
}
def analyze_dropped_files(report):
"""Extract dropped file information from the report."""
dropped = []
for d in report.get("dropped", []):
dropped.append({
"filepath": d.get("filepath", ""),
"sha256": d.get("sha256", ""),
"size": d.get("size", 0),
"type": d.get("type", ""),
})
return dropped
def analyze_signatures(report):
"""Extract triggered behavioral signatures."""
signatures = []
for sig in report.get("signatures", []):
marks = []
for mark in sig.get("marks", []):
if mark.get("ioc"):
marks.append(mark["ioc"])
elif mark.get("call"):
marks.append(mark["call"].get("api", ""))
signatures.append({
"name": sig.get("name"),
"severity": sig.get("severity"),
"description": sig.get("description"),
"marks": marks[:5],
})
return sorted(signatures, key=lambda x: x.get("severity", 0), reverse=True)
def analyze_registry(report):
"""Extract registry modifications from behavior summary."""
summary = report.get("behavior", {}).get("summary", {})
return {
"keys_modified": summary.get("keys", [])[:20],
"files_created": summary.get("files", [])[:20],
"mutexes": summary.get("mutexes", [])[:10],
}
def generate_summary(report, processes, network, dropped, signatures, registry):
"""Generate a consolidated analysis summary."""
info = report.get("info", {})
score = info.get("score", 0)
return {
"task_id": info.get("id"),
"sample": info.get("category", "file"),
"analysis_time": info.get("duration", 0),
"machine": info.get("machine", {}).get("name", ""),
"threat_score": score,
"process_count": len(processes),
"suspicious_api_total": sum(p["suspicious_api_calls"] for p in processes),
"dns_queries": len(network["dns"]),
"http_requests": len(network["http"]),
"tcp_connections": len(network["tcp_connections"]),
"dropped_files": len(dropped),
"signatures_triggered": len(signatures),
"high_severity_sigs": len([s for s in signatures if s["severity"] >= 3]),
"registry_keys_modified": len(registry["keys_modified"]),
"files_created": len(registry["files_created"]),
}
if __name__ == "__main__":
print("=" * 60)
print("Cuckoo Sandbox Behavioral Analysis Agent")
print("Automated malware detonation and report parsing")
print("=" * 60)
if len(sys.argv) > 1:
arg = sys.argv[1]
# Check if argument is a report JSON path
if arg.endswith(".json") and os.path.exists(arg):
print(f"\n[*] Loading report: {arg}")
with open(arg, "r") as f:
report = json.load(f)
elif arg.isdigit():
print(f"\n[*] Loading report for task ID: {arg}")
report = load_report(int(arg))
elif os.path.exists(arg):
print(f"\n[*] Submitting sample: {arg}")
sha256 = hashlib.sha256(open(arg, "rb").read()).hexdigest()
print(f"[*] SHA-256: {sha256}")
task_id = submit_file(arg)
if task_id:
print(f"[*] Task submitted: ID={task_id}")
print(f"[*] Monitor at: {CUCKOO_API.replace('8090', '8080')}/analysis/{task_id}/")
else:
print("[ERROR] Failed to submit. Check Cuckoo API connection.")
sys.exit(0)
else:
report = None
if report:
processes = analyze_processes(report)
network = analyze_network(report)
dropped = analyze_dropped_files(report)
signatures = analyze_signatures(report)
registry = analyze_registry(report)
summary = generate_summary(report, processes, network, dropped, signatures, registry)
print(f"\n--- Analysis Summary ---")
print(f" Score: {summary['threat_score']}/10")
print(f" Processes: {summary['process_count']}")
print(f" Suspicious APIs: {summary['suspicious_api_total']}")
print(f" Signatures: {summary['signatures_triggered']} "
f"({summary['high_severity_sigs']} high severity)")
print(f"\n--- Network ---")
print(f" DNS: {summary['dns_queries']}, HTTP: {summary['http_requests']}, "
f"TCP: {summary['tcp_connections']}")
for http in network["http"][:5]:
print(f" {http['method']} {http['host']}{http['uri']}")
print(f"\n--- Dropped Files ---")
for d in dropped[:5]:
print(f" {d['filepath']} ({d['size']} bytes)")
print(f"\n--- Top Signatures ---")
for s in signatures[:5]:
print(f" [{s['severity']}/5] {s['name']}: {s['description']}")
else:
print(f"\n[DEMO] Usage:")
print(f" python agent.py <sample.exe> # Submit to Cuckoo")
print(f" python agent.py <task_id> # Parse existing report")
print(f" python agent.py <report.json> # Parse JSON report file")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,99 @@
# API Reference: Volatility 3 Memory Forensics
## Core Syntax
```bash
vol3 -f <memory_dump> <plugin> [options]
vol3 -f memory.dmp --help # List all plugins
vol3 -f memory.dmp <plugin> --help # Plugin-specific help
```
## Windows Plugins
### Process Analysis
| Plugin | Purpose |
|--------|---------|
| `windows.pslist` | List active processes |
| `windows.pstree` | Process tree (parent-child) |
| `windows.psscan` | Pool-tag scan (finds hidden processes) |
| `windows.cmdline` | Process command-line arguments |
| `windows.envars` | Process environment variables |
| `windows.handles` | Process handle table |
### Code Injection Detection
| Plugin | Purpose |
|--------|---------|
| `windows.malfind` | Detect injected code (RWX memory + PE headers) |
| `windows.hollowfind` | Detect process hollowing |
| `windows.dlllist` | List loaded DLLs per process |
| `windows.ldrmodules` | Detect unlinked DLLs |
### Network
| Plugin | Purpose |
|--------|---------|
| `windows.netscan` | List network connections and listeners |
| `windows.netstat` | Network connections (older Windows) |
### Kernel / Rootkit
| Plugin | Purpose |
|--------|---------|
| `windows.ssdt` | System Service Descriptor Table hooks |
| `windows.callbacks` | Kernel callback registrations |
| `windows.driverscan` | Scan for driver objects |
| `windows.modules` | Loaded kernel modules |
| `windows.idt` | Interrupt Descriptor Table |
### Credentials
| Plugin | Purpose |
|--------|---------|
| `windows.hashdump` | Dump SAM password hashes |
| `windows.cachedump` | Dump cached domain credentials |
| `windows.lsadump` | Dump LSA secrets |
### Registry
| Plugin | Purpose |
|--------|---------|
| `windows.registry.printkey` | Print registry key values |
| `windows.registry.hivelist` | List registry hives |
| `windows.registry.certificates` | Extract certificates |
### File System
| Plugin | Purpose |
|--------|---------|
| `windows.filescan` | Scan for file objects |
| `windows.dumpfiles` | Extract files from memory |
| `windows.memmap` | Dump process memory |
### YARA Scanning
```bash
vol3 -f memory.dmp yarascan.YaraScan --yara-file rules.yar
vol3 -f memory.dmp yarascan.YaraScan --yara-file rules.yar --pid 2184
vol3 -f memory.dmp yarascan.YaraScan --yara-rules "rule Test { strings: $s = \"cmd.exe\" condition: $s }"
```
### Timeline
```bash
vol3 -f memory.dmp timeliner.Timeliner --output-file timeline.csv
```
## Output Options
```bash
vol3 -f memory.dmp windows.pslist --output csv > processes.csv
vol3 -f memory.dmp windows.pslist --output json > processes.json
vol3 -f memory.dmp windows.malfind --dump --pid 2184
```
## Memory Acquisition Tools
| Tool | Platform | Command |
|------|----------|---------|
| WinPmem | Windows | `winpmem_mini_x64.exe memdump.raw` |
| DumpIt | Windows | `DumpIt.exe` (interactive) |
| LiME | Linux | `insmod lime.ko "path=/tmp/mem.lime format=lime"` |
| AVML | Linux | `avml /tmp/memory.lime` |
## Symbols
```bash
# Download symbol packs
# https://downloads.volatilityfoundation.org/volatility3/symbols/
# Place in: volatility3/symbols/
```
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Memory forensics agent using Volatility 3 for malware detection in RAM dumps."""
import subprocess
import os
import sys
import json
import csv
import re
import io
def run_vol3(memory_dump, plugin, extra_args=""):
"""Execute a Volatility 3 plugin and return output."""
cmd = f"vol3 -f {memory_dump} {plugin} {extra_args}"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def get_os_info(memory_dump):
"""Identify the OS from the memory dump."""
stdout, _, rc = run_vol3(memory_dump, "windows.info")
if rc == 0:
return {"os": "windows", "info": stdout}
stdout, _, rc = run_vol3(memory_dump, "linux.info")
if rc == 0:
return {"os": "linux", "info": stdout}
return {"os": "unknown", "info": ""}
def list_processes(memory_dump):
"""List all running processes using pslist."""
stdout, _, rc = run_vol3(memory_dump, "windows.pslist")
processes = []
if rc == 0:
for line in stdout.splitlines()[2:]:
parts = line.split()
if len(parts) >= 6 and parts[0].isdigit():
processes.append({
"pid": int(parts[0]),
"ppid": int(parts[1]),
"name": parts[4] if len(parts) > 4 else "",
"offset": parts[0] if not parts[0].isdigit() else "",
})
return processes
def scan_hidden_processes(memory_dump):
"""Scan for hidden/unlinked processes using psscan."""
stdout, _, rc = run_vol3(memory_dump, "windows.psscan")
processes = []
if rc == 0:
for line in stdout.splitlines()[2:]:
parts = line.split()
if len(parts) >= 5 and parts[1].isdigit():
processes.append({
"offset": parts[0],
"pid": int(parts[1]),
"ppid": int(parts[2]) if parts[2].isdigit() else 0,
"name": parts[4] if len(parts) > 4 else "",
})
return processes
def find_hidden_processes(pslist_procs, psscan_procs):
"""Compare pslist and psscan to identify DKOM-hidden processes."""
pslist_pids = {p["pid"] for p in pslist_procs}
hidden = [p for p in psscan_procs if p["pid"] not in pslist_pids and p["pid"] > 4]
return hidden
def detect_code_injection(memory_dump, pid=None):
"""Detect injected code using malfind plugin."""
extra = f"--pid {pid}" if pid else ""
stdout, _, rc = run_vol3(memory_dump, "windows.malfind", extra)
injections = []
if rc == 0:
current = {}
for line in stdout.splitlines():
if "PID" in line and "Process" in line:
continue
parts = line.split()
if len(parts) >= 4 and parts[0].isdigit():
if current:
injections.append(current)
current = {
"pid": int(parts[0]),
"process": parts[1] if len(parts) > 1 else "",
"address": parts[2] if len(parts) > 2 else "",
"protection": parts[3] if len(parts) > 3 else "",
}
elif current and line.strip():
current["data_preview"] = current.get("data_preview", "") + line.strip() + " "
if current:
injections.append(current)
return injections
def get_network_connections(memory_dump):
"""Extract network connections using netscan."""
stdout, _, rc = run_vol3(memory_dump, "windows.netscan")
connections = []
if rc == 0:
for line in stdout.splitlines()[2:]:
parts = line.split()
if len(parts) >= 7:
connections.append({
"protocol": parts[1] if len(parts) > 1 else "",
"local_addr": parts[2] if len(parts) > 2 else "",
"local_port": parts[3] if len(parts) > 3 else "",
"foreign_addr": parts[4] if len(parts) > 4 else "",
"foreign_port": parts[5] if len(parts) > 5 else "",
"state": parts[6] if len(parts) > 6 else "",
"pid": parts[7] if len(parts) > 7 else "",
"owner": parts[8] if len(parts) > 8 else "",
})
return connections
def get_command_lines(memory_dump):
"""Extract process command lines."""
stdout, _, rc = run_vol3(memory_dump, "windows.cmdline")
cmdlines = []
if rc == 0:
for line in stdout.splitlines()[2:]:
parts = line.split(None, 2)
if len(parts) >= 3 and parts[0].isdigit():
cmdlines.append({
"pid": int(parts[0]),
"process": parts[1],
"cmdline": parts[2],
})
return cmdlines
def dump_credentials(memory_dump):
"""Extract cached credentials using hashdump and lsadump."""
results = {}
stdout, _, rc = run_vol3(memory_dump, "windows.hashdump")
if rc == 0:
results["hashdump"] = stdout
stdout, _, rc = run_vol3(memory_dump, "windows.cachedump")
if rc == 0:
results["cachedump"] = stdout
stdout, _, rc = run_vol3(memory_dump, "windows.lsadump")
if rc == 0:
results["lsadump"] = stdout
return results
def scan_with_yara(memory_dump, yara_file=None, yara_rule=None, pid=None):
"""Scan memory with YARA rules."""
extra = ""
if yara_file:
extra += f"--yara-file {yara_file}"
elif yara_rule:
extra += f'--yara-rules "{yara_rule}"'
if pid:
extra += f" --pid {pid}"
stdout, _, rc = run_vol3(memory_dump, "yarascan.YaraScan", extra)
return stdout if rc == 0 else ""
def check_suspicious_processes(pslist_procs):
"""Check process list for common suspicious indicators."""
findings = []
expected_parents = {
"svchost.exe": ["services.exe"],
"csrss.exe": ["smss.exe"],
"lsass.exe": ["wininit.exe"],
"smss.exe": ["System"],
}
name_counts = {}
for p in pslist_procs:
name = p["name"].lower()
name_counts[name] = name_counts.get(name, 0) + 1
if name_counts.get("lsass.exe", 0) > 1:
findings.append({"severity": "CRITICAL",
"finding": "Multiple lsass.exe instances detected"})
misspellings = {
"scvhost.exe": "svchost.exe", "svch0st.exe": "svchost.exe",
"lssas.exe": "lsass.exe", "csrs.exe": "csrss.exe",
}
for p in pslist_procs:
if p["name"].lower() in misspellings:
findings.append({
"severity": "HIGH",
"finding": f"Misspelled process: {p['name']} (PID {p['pid']}) "
f"mimicking {misspellings[p['name'].lower()]}",
})
return findings
if __name__ == "__main__":
print("=" * 60)
print("Memory Forensics Agent (Volatility 3)")
print("Process analysis, injection detection, credential extraction")
print("=" * 60)
dump_file = sys.argv[1] if len(sys.argv) > 1 else None
if dump_file and os.path.exists(dump_file):
print(f"\n[*] Analyzing memory dump: {dump_file}")
print(f"[*] Size: {os.path.getsize(dump_file) / (1024**3):.1f} GB")
print("\n--- OS Identification ---")
os_info = get_os_info(dump_file)
print(f" OS: {os_info['os']}")
print("\n--- Process Analysis ---")
procs = list_processes(dump_file)
print(f" Active processes: {len(procs)}")
suspicious = check_suspicious_processes(procs)
for s in suspicious:
print(f" [{s['severity']}] {s['finding']}")
print("\n--- Hidden Process Detection ---")
psscan = scan_hidden_processes(dump_file)
hidden = find_hidden_processes(procs, psscan)
if hidden:
for h in hidden:
print(f" [!] Hidden process: {h['name']} PID={h['pid']}")
else:
print(" No hidden processes detected")
print("\n--- Code Injection Detection ---")
injections = detect_code_injection(dump_file)
print(f" Injected regions: {len(injections)}")
for inj in injections[:5]:
print(f" [!] PID {inj['pid']} ({inj.get('process', '')}): {inj.get('protection', '')}")
print("\n--- Network Connections ---")
conns = get_network_connections(dump_file)
established = [c for c in conns if "ESTABLISHED" in c.get("state", "")]
print(f" Total: {len(conns)}, Established: {len(established)}")
for c in established[:10]:
print(f" {c.get('owner', '?')} (PID {c.get('pid', '?')}): "
f"{c['local_addr']}:{c['local_port']} -> "
f"{c['foreign_addr']}:{c['foreign_port']}")
else:
print(f"\n[DEMO] Usage: python agent.py <memory.dmp>")
print("[*] Provide a memory dump for forensic analysis.")
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,52 @@
---
name: analyzing-memory-forensics-with-lime-and-volatility
description: >
Performs Linux memory acquisition using LiME (Linux Memory Extractor) kernel module
and analysis with Volatility 3 framework. Extracts process lists, network connections,
bash history, loaded kernel modules, and injected code from Linux memory images.
Use when performing incident response on compromised Linux systems.
---
# Analyzing Memory Forensics with LiME and Volatility
## Instructions
Acquire Linux memory using LiME kernel module, then analyze with Volatility 3
to extract forensic artifacts from the memory image.
```bash
# LiME acquisition
insmod lime-$(uname -r).ko "path=/evidence/memory.lime format=lime"
# Volatility 3 analysis
vol3 -f /evidence/memory.lime linux.pslist
vol3 -f /evidence/memory.lime linux.bash
vol3 -f /evidence/memory.lime linux.sockstat
```
```python
import volatility3
from volatility3.framework import contexts, automagic
from volatility3.plugins.linux import pslist, bash, sockstat
# Programmatic Volatility 3 usage
context = contexts.Context()
automagics = automagic.available(context)
```
Key analysis steps:
1. Acquire memory with LiME (format=lime or format=raw)
2. List processes with linux.pslist, compare with linux.psscan
3. Extract bash command history with linux.bash
4. List network connections with linux.sockstat
5. Check loaded kernel modules with linux.lsmod for rootkits
## Examples
```bash
# Full forensic workflow
vol3 -f memory.lime linux.pslist | grep -v "\[kthread\]"
vol3 -f memory.lime linux.bash
vol3 -f memory.lime linux.malfind
vol3 -f memory.lime linux.lsmod
```
@@ -0,0 +1,58 @@
# API Reference: Analyzing Memory Forensics with LiME and Volatility
## LiME (Linux Memory Extractor)
```bash
# Build LiME module
cd LiME/src && make
# Acquire memory (lime format - includes metadata)
insmod lime-$(uname -r).ko "path=/evidence/mem.lime format=lime"
# Acquire memory (raw format)
insmod lime-$(uname -r).ko "path=/evidence/mem.raw format=raw"
# Acquire over network
insmod lime.ko "path=tcp:4444 format=lime"
# On forensic workstation: nc target 4444 > mem.lime
```
## Volatility 3 Linux Plugins
| Plugin | Description |
|--------|-------------|
| `linux.pslist` | List processes via task_struct |
| `linux.psscan` | Brute-force scan for task_struct |
| `linux.bash` | Recovered bash command history |
| `linux.sockstat` | Network connections |
| `linux.lsmod` | Loaded kernel modules |
| `linux.malfind` | Detect injected code |
| `linux.check_afinfo` | Detect network hooking |
| `linux.tty_check` | Detect TTY hooking |
| `linux.proc.Maps` | Process memory maps |
## Volatility 3 CLI
```bash
vol3 -f memory.lime linux.pslist
vol3 -f memory.lime linux.bash
vol3 -f memory.lime linux.sockstat
vol3 -f memory.lime linux.malfind
vol3 -f memory.lime linux.lsmod
vol3 -f memory.lime linux.check_afinfo
```
## Hidden Process Detection
```bash
# Compare pslist (linked list) vs psscan (brute force)
vol3 -f mem.lime linux.pslist > pslist.txt
vol3 -f mem.lime linux.psscan > psscan.txt
diff pslist.txt psscan.txt
```
### References
- LiME: https://github.com/504ensicsLabs/LiME
- Volatility 3: https://github.com/volatilityfoundation/volatility3
- Volatility 3 docs: https://volatility3.readthedocs.io/
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""Agent for Linux memory forensics using LiME acquisition and Volatility 3."""
import os
import json
import subprocess
import argparse
from datetime import datetime
from pathlib import Path
def acquire_memory_lime(output_path, lime_format="lime"):
"""Acquire memory using LiME kernel module."""
kernel_version = subprocess.run(
["uname", "-r"], capture_output=True, text=True
).stdout.strip()
lime_module = f"lime-{kernel_version}.ko"
if not Path(lime_module).exists():
lime_module = "lime.ko"
cmd = ["insmod", lime_module, f"path={output_path}", f"format={lime_format}"]
result = subprocess.run(cmd, capture_output=True, text=True)
return {
"status": "success" if result.returncode == 0 else "failed",
"output_path": output_path,
"format": lime_format,
"kernel": kernel_version,
"stderr": result.stderr,
}
def run_vol3_plugin(image_path, plugin_name, extra_args=None):
"""Run a Volatility 3 plugin and capture output."""
cmd = ["vol3", "-f", image_path, plugin_name]
if extra_args:
cmd.extend(extra_args)
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=300,
)
lines = result.stdout.strip().splitlines()
return {"plugin": plugin_name, "output": lines, "error": result.stderr.strip()}
except subprocess.TimeoutExpired:
return {"plugin": plugin_name, "output": [], "error": "Timeout"}
def parse_pslist_output(lines):
"""Parse Volatility linux.pslist output into structured data."""
processes = []
for line in lines:
parts = line.split()
if len(parts) >= 4 and parts[0].isdigit():
processes.append({
"pid": int(parts[0]),
"ppid": int(parts[1]) if parts[1].isdigit() else 0,
"name": parts[-1],
})
return processes
def list_processes(image_path):
"""List all processes from memory image."""
result = run_vol3_plugin(image_path, "linux.pslist")
return parse_pslist_output(result.get("output", []))
def extract_bash_history(image_path):
"""Extract bash command history from memory."""
result = run_vol3_plugin(image_path, "linux.bash")
commands = []
for line in result.get("output", []):
parts = line.split(None, 3)
if len(parts) >= 4 and parts[0].isdigit():
commands.append({
"pid": int(parts[0]),
"name": parts[1],
"timestamp": parts[2] if len(parts) > 2 else "",
"command": parts[3] if len(parts) > 3 else "",
})
return commands
def list_network_connections(image_path):
"""List network connections from memory."""
result = run_vol3_plugin(image_path, "linux.sockstat")
connections = []
for line in result.get("output", []):
if "TCP" in line or "UDP" in line:
connections.append(line.strip())
return connections
def list_kernel_modules(image_path):
"""List loaded kernel modules to detect rootkits."""
result = run_vol3_plugin(image_path, "linux.lsmod")
modules = []
for line in result.get("output", []):
parts = line.split()
if parts and not parts[0].startswith("Offset"):
modules.append({"name": parts[-1] if parts else line.strip()})
return modules
def detect_hidden_processes(image_path):
"""Compare pslist vs psscan to find hidden processes."""
pslist = run_vol3_plugin(image_path, "linux.pslist")
psscan = run_vol3_plugin(image_path, "linux.psscan")
pslist_pids = set()
for line in pslist.get("output", []):
parts = line.split()
if parts and parts[0].isdigit():
pslist_pids.add(int(parts[0]))
hidden = []
for line in psscan.get("output", []):
parts = line.split()
if parts and parts[0].isdigit():
pid = int(parts[0])
if pid not in pslist_pids and pid > 0:
hidden.append({"pid": pid, "line": line.strip()})
return hidden
def detect_suspicious_commands(bash_history):
"""Flag suspicious commands in bash history."""
suspicious_patterns = [
"curl.*|.*sh", "wget.*&&.*chmod", "base64.*-d",
"nc.*-e", "python.*-c.*import.*socket",
"nohup", "rm.*-rf.*/var/log", "history.*-c",
"iptables.*-F", "chmod.*777", "chattr.*-i",
]
import re
findings = []
for entry in bash_history:
cmd = entry.get("command", "")
for pattern in suspicious_patterns:
if re.search(pattern, cmd, re.IGNORECASE):
findings.append({
"pid": entry["pid"],
"command": cmd,
"pattern": pattern,
"severity": "HIGH",
})
break
return findings
def check_malfind(image_path):
"""Run malfind to detect injected code."""
result = run_vol3_plugin(image_path, "linux.malfind")
return result.get("output", [])
def main():
parser = argparse.ArgumentParser(description="LiME + Volatility 3 Forensics Agent")
parser.add_argument("--image", help="Path to memory image")
parser.add_argument("--acquire", help="Output path for LiME acquisition")
parser.add_argument("--output", default="memory_forensics_report.json")
parser.add_argument("--action", choices=[
"acquire", "pslist", "bash", "network", "modules",
"hidden", "malfind", "full_analysis"
], default="full_analysis")
args = parser.parse_args()
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
if args.action == "acquire" and args.acquire:
result = acquire_memory_lime(args.acquire)
report["findings"]["acquisition"] = result
print(f"[+] Memory acquisition: {result['status']}")
return
if not args.image:
print("[-] --image required for analysis actions")
return
if args.action in ("pslist", "full_analysis"):
procs = list_processes(args.image)
report["findings"]["processes"] = procs
print(f"[+] Processes: {len(procs)}")
if args.action in ("bash", "full_analysis"):
history = extract_bash_history(args.image)
report["findings"]["bash_history"] = history
suspicious = detect_suspicious_commands(history)
report["findings"]["suspicious_commands"] = suspicious
print(f"[+] Bash commands: {len(history)}, Suspicious: {len(suspicious)}")
if args.action in ("network", "full_analysis"):
conns = list_network_connections(args.image)
report["findings"]["connections"] = conns
print(f"[+] Network connections: {len(conns)}")
if args.action in ("modules", "full_analysis"):
modules = list_kernel_modules(args.image)
report["findings"]["kernel_modules"] = modules
print(f"[+] Kernel modules: {len(modules)}")
if args.action in ("hidden", "full_analysis"):
hidden = detect_hidden_processes(args.image)
report["findings"]["hidden_processes"] = hidden
print(f"[+] Hidden processes: {len(hidden)}")
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anthropic Agent Skills Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,34 @@
---
name: analyzing-network-flow-data-with-netflow
description: >-
Parse NetFlow v9 and IPFIX records to detect volumetric anomalies, port scanning, data
exfiltration, and C2 beaconing patterns. Uses the Python netflow library to decode flow
records, builds traffic baselines, and applies statistical analysis to identify flows
with abnormal byte counts, connection durations, and periodic timing patterns.
---
## Instructions
1. Install dependencies: `pip install netflow`
2. Collect NetFlow/IPFIX data from routers or use the built-in collector: `python -m netflow.collector -p 9995`
3. Parse captured flow data using `netflow.parse_packet()`.
4. Analyze flows for:
- Port scanning: single source to many destinations on same port
- Data exfiltration: high byte-count outbound flows to unusual destinations
- C2 beaconing: periodic connections with consistent intervals
- Volumetric anomalies: traffic spikes beyond baseline thresholds
5. Generate a prioritized findings report.
```bash
python scripts/agent.py --flow-file captured_flows.json --output netflow_report.json
```
## Examples
### Parse NetFlow v9 Packet
```python
import netflow
data, _ = netflow.parse_packet(raw_bytes, templates={})
for flow in data.flows:
print(flow.IPV4_SRC_ADDR, flow.IPV4_DST_ADDR, flow.IN_BYTES)
```
@@ -0,0 +1,48 @@
# API Reference: NetFlow v9/IPFIX Analysis
## Python netflow Library
```python
import netflow
# Parse a raw NetFlow packet
packet, templates = netflow.parse_packet(raw_bytes, templates={})
# templates must persist between calls for v9/IPFIX
for flow in packet.flows:
flow.IPV4_SRC_ADDR # Source IP
flow.IPV4_DST_ADDR # Destination IP
flow.L4_SRC_PORT # Source port
flow.L4_DST_PORT # Destination port
flow.PROTOCOL # IP protocol (6=TCP, 17=UDP)
flow.IN_BYTES # Bytes transferred
flow.IN_PKTS # Packet count
flow.TCP_FLAGS # TCP flags bitmask
flow.FIRST_SWITCHED # Flow start time
flow.LAST_SWITCHED # Flow end time
```
## CLI Tools
```bash
python -m netflow.collector -p 9995 -D /tmp/flows # Collector
python -m netflow.analyzer -f /tmp/flows/*.json # Analyzer
```
## NetFlow v9 Field Types
| Field | ID | Description |
|-------|-----|-------------|
| IN_BYTES | 1 | Input bytes |
| IN_PKTS | 2 | Input packets |
| PROTOCOL | 4 | IP protocol |
| L4_SRC_PORT | 7 | Source port |
| IPV4_SRC_ADDR | 8 | Source IPv4 |
| L4_DST_PORT | 11 | Destination port |
| IPV4_DST_ADDR | 12 | Destination IPv4 |
| TCP_FLAGS | 6 | TCP flags |
| FIRST_SWITCHED | 22 | Flow start sysUpTime |
| LAST_SWITCHED | 21 | Flow end sysUpTime |
## Detection Algorithms
| Pattern | Method | Threshold |
|---------|--------|-----------|
| Port scan | Unique dst_ports per src-dst pair | >20 ports |
| Network sweep | Unique dst_ips per source | >50 hosts |
| Exfiltration | Total bytes per src-dst pair | >100MB |
| C2 beaconing | Interval jitter ratio | <0.15 |

Some files were not shown because too many files have changed in this diff Show More