
Malware Analysis
- 64 installs
- 321 repo stars
- Updated May 14, 2026
- tsale/awesome-dfir-skills
Helps with ai & agent building tasks.
About
malware-analysis is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- malware-analysis
- AI & Agent Building
- AI-coding skill
Malware Analysis by the numbers
- 64 all-time installs (skills.sh)
- +3 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #6,160 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tsale/awesome-dfir-skills --skill malware-analysisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 321 |
| Last updated | May 14, 2026 |
| Repository | tsale/awesome-dfir-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Malware Analysis Skill
This skill produces analyst-grade threat reports — not data dumps. Every conclusion must be backed by evidence and reasoning.
Core Principles
1. Evidence-based reasoning: Never state a conclusion without explaining WHY 2. Connect the dots: Link indicators to behaviors to capabilities to impact 3. Assess confidence: State how confident you are and why 4. Actionable output: Reports should enable decisions, not just inform
Analysis Workflow
Step 1: Collect Data
Run all scripts to gather raw data:
# Static analysis - get hashes, PE info, strings, APIs, entropy
python3 scripts/static_analysis.py /path/to/sample -f json > static.json
# Threat intelligence - check reputation across sources
python3 scripts/triage.py -t file /path/to/sample -f json > triage.json
# IOC extraction - extract network/host indicators
python3 scripts/extract_iocs.py /path/to/sample -f json > iocs.jsonStep 2: Analyze and Reason (THIS IS THE KEY STEP)
Using the collected data, perform analyst-grade reasoning:
2.1 Threat Intelligence Assessment
Ask yourself:
- Is this sample known? If found in MalwareBazaar/ThreatFox, it's confirmed malware
- What's the VT detection rate?
- 0 detections: New sample, FP, or clean — requires behavioral analysis
- 1-5 detections: Possibly new variant or targeted — suspicious
- 5-15 detections: Confirmed malicious by multiple vendors
- 15+ detections: Well-known malware
- What family is it attributed to? Research that family's typical behavior
- When was it first seen? Recent = active campaign
Always explain your reasoning:
"This sample is identified as RedLine Stealer by MalwareBazaar with 45/70 VT detections. The high detection rate and presence in curated malware repositories confirms this is a known threat, not a false positive."
2.2 Behavioral Analysis from Static Indicators
API Analysis - Map APIs to behaviors:
| API Pattern | Likely Behavior | Reasoning |
|---|---|---|
| VirtualAlloc + VirtualProtect + WriteProcessMemory + CreateRemoteThread | Process Injection | This is the classic injection pattern: allocate memory, make it executable, write code, execute in target |
| CredEnumerate, CryptUnprotectData | Credential Theft | These APIs specifically access Windows credential stores and DPAPI-protected data (browser passwords) |
| InternetOpen + URLDownloadToFile | Downloader | Initializes HTTP and downloads files — classic dropper behavior |
| RegSetValueEx + Run key paths in strings | Persistence | Writing to Run keys ensures execution at startup |
| IsDebuggerPresent, GetTickCount, NtQuerySystemInformation | Anti-Analysis | Multiple evasion checks suggest the malware hides its behavior during analysis |
| CryptEncrypt + file enumeration APIs | Possible Ransomware | Encryption capability combined with file discovery — but could also be secure C2 |
Always explain your reasoning:
"The presence of VirtualAlloc, VirtualProtect, and CreateRemoteThread together strongly suggests process injection capability. Individually these APIs have legitimate uses, but this specific combination is the textbook pattern for injecting code into other processes."
Packing Analysis:
| Indicator | Meaning | Confidence |
|---|---|---|
| Entropy > 7.0 | Compressed/encrypted content | High |
| Section entropy > 7.0 (especially .text) | Packed code section | High |
| UPX0, UPX1, .aspack, .packed sections | Known packer signatures | Very High |
| RWX sections | Self-modifying code | Medium |
| Small import table with GetProcAddress/LoadLibrary only | Dynamic API resolution | High |
If packed, state the implication:
"This sample shows multiple packing indicators (entropy 7.4, UPX sections). The static analysis findings represent the unpacker stub, NOT the actual payload. Dynamic analysis is required to reveal true functionality."
2.3 Capability Assessment
Based on the evidence, determine what the malware CAN DO:
| Capability | Required Evidence | Confidence Level |
|---|---|---|
| Process Injection | 2+ injection APIs | High if 3+, Medium if 2 |
| Credential Theft | Any cred access API | High (these are specific) |
| Keylogging | SetWindowsHookEx | Medium (has legit uses) |
| Network C2 | 2+ network APIs + extracted URLs/IPs | High |
| File Download | URLDownloadToFile or similar | High |
| Persistence | Registry/service APIs + relevant strings | Medium |
| Encryption/Ransomware | Crypto APIs + file enumeration | Medium (needs context) |
State confidence and reasoning:
"Credential Theft Capability: HIGH CONFIDENCE — CryptUnprotectData is present, which specifically decrypts DPAPI-protected data including browser passwords. This API has no legitimate use case in most software."
2.4 Risk Assessment
Determine risk level with justification:
| Risk Level | Criteria |
|---|---|
| CRITICAL | Credential theft APIs, process injection, confirmed malware family known for data theft/ransomware |
| HIGH | Multiple malicious capabilities, network C2, persistence mechanisms |
| MEDIUM | Suspicious indicators but no confirmed malicious capability, or packing hiding true behavior |
| LOW | Few indicators, possibly legitimate software with suspicious patterns |
| UNKNOWN | Insufficient evidence, heavily packed, or no TI hits |
Step 3: Write the Report
Structure your report as follows:
# Threat Analysis Report: [MALWARE_NAME or "Unknown Sample"]
| | |
|---|---|
| **Risk Level** | [CRITICAL/HIGH/MEDIUM/LOW] |
| **Confidence** | [High/Medium/Low] |
| **Analysis Date** | [DATE] |
---
## Executive Summary
[2-3 sentences: What is this? Is it malicious? What can it do? How do we know?]
**Key Finding:** [One sentence bottom line]
---
## Threat Intelligence Assessment
[What do TI sources tell us? Explain what each finding means]
- **VirusTotal:** [X/Y detections] — [what this means]
- **MalwareBazaar:** [Found/Not found] — [what this means]
- **Family Attribution:** [Family name] — [what this family typically does]
**Assessment:** [Your reasoned conclusion based on TI]
---
## Behavioral Analysis
### Identified Capabilities
#### [Capability 1: e.g., "Process Injection"]
- **Confidence:** [High/Medium/Low]
- **Evidence:** [List the specific APIs/strings found]
- **Reasoning:** [Explain WHY this evidence indicates this capability]
#### [Capability 2: e.g., "Credential Theft"]
...
### Packing Assessment
[Is it packed? What does this mean for the analysis?]
### Anti-Analysis Techniques
[What evasion techniques were identified?]
---
## MITRE ATT&CK Mapping
| Tactic | Technique | ID | Evidence |
|--------|-----------|----|---------|
| [Only include techniques you can justify with evidence] |
---
## Indicators of Compromise
### File Indicators
[Hashes]
### Network Indicators
[Defanged IPs, domains, URLs - only if extracted]
### Host Indicators
[Registry keys, file paths, mutexes - only if found]
---
## Risk Assessment
**Overall Risk: [LEVEL]**
This assessment is based on:
1. [Reason 1]
2. [Reason 2]
3. [Reason 3]
**Confidence in Assessment: [High/Medium/Low]**
- [Why this confidence level]
---
## Recommendations
### Immediate Actions
[What should be done RIGHT NOW based on risk level]
### Detection Opportunities
[How to detect this threat]
### Further Analysis Needed
[What questions remain unanswered]Entropy Interpretation
| Entropy | Meaning |
|---|---|
| 0-1 | Highly structured (empty, repetitive) |
| 4-5 | Plain text, readable strings |
| 5-6 | Compiled code (normal .text section) |
| 6-7 | Compressed data, some obfuscation |
| 7-8 | Encrypted/compressed (PACKED) |
File Signatures
| Bytes | Type |
|---|---|
| 4D 5A (MZ) | PE executable |
| 50 4B (PK) | ZIP/Office document |
| 7F 45 4C 46 | ELF executable |
| D0 CF 11 E0 | OLE/Legacy Office |
| 25 50 44 46 |
Example Analysis Reasoning
BAD (data dump):
"Found APIs: VirtualAlloc, CreateRemoteThread, RegSetValueEx. Entropy: 7.2. VT: 34/70."
GOOD (analyst reasoning):
"This sample demonstrates process injection capability (HIGH CONFIDENCE) based on the presence of VirtualAlloc and CreateRemoteThread. These APIs, when used together, form the classic code injection pattern where memory is allocated in a target process and a thread is created to execute the injected code. The high entropy (7.2) suggests the payload is packed, meaning the observed APIs may belong to the unpacker stub rather than the final payload. The 34/70 VirusTotal detection rate confirms this is recognized malware, with multiple vendors identifying it as a variant of Agent Tesla — an info-stealer known for credential harvesting. Given the injection capability and association with a credential-stealing family, this sample poses a CRITICAL risk to credential security on any system where it executes."
Scripts Reference
static_analysis.py
python3 scripts/static_analysis.py <file> -f [text|json]Extracts: hashes, file type, PE headers, sections, entropy, imports, strings, suspicious indicators
triage.py
python3 scripts/triage.py <ioc> -f [text|json]
python3 scripts/triage.py -t file <filepath> -f json
python3 scripts/triage.py --status # Check API configQueries: MalwareBazaar, ThreatFox, URLhaus, VirusTotal, AbuseIPDB
extract_iocs.py
python3 scripts/extract_iocs.py <file> -f [text|json|csv]Extracts: IPs, domains, URLs, emails, hashes, registry keys, file paths, crypto wallets, mutexes
{
"virustotal_api_key": "",
"abuseipdb_api_key": "",
"abusech_auth_key": ""
}MITRE ATT&CK Mapping Reference
Quick reference for mapping malware behaviors to ATT&CK techniques.
Execution
| Behavior | Technique | ID |
|---|---|---|
| PowerShell execution | Command and Scripting Interpreter: PowerShell | T1059.001 |
| cmd.exe execution | Command and Scripting Interpreter: Windows Command Shell | T1059.003 |
| WMI execution | Windows Management Instrumentation | T1047 |
| Scheduled Task | Scheduled Task/Job: Scheduled Task | T1053.005 |
| DLL side-loading | Hijack Execution Flow: DLL Side-Loading | T1574.002 |
| Process injection | Process Injection | T1055 |
| Rundll32 abuse | System Binary Proxy Execution: Rundll32 | T1218.011 |
| Regsvr32 abuse | System Binary Proxy Execution: Regsvr32 | T1218.010 |
Persistence
| Behavior | Technique | ID |
|---|---|---|
| Registry Run keys | Boot or Logon Autostart Execution: Registry Run Keys | T1547.001 |
| Startup folder | Boot or Logon Autostart Execution: Startup Folder | T1547.001 |
| Scheduled task | Scheduled Task/Job: Scheduled Task | T1053.005 |
| Windows Service | Create or Modify System Process: Windows Service | T1543.003 |
| DLL search order hijacking | Hijack Execution Flow: DLL Search Order Hijacking | T1574.001 |
| COM hijacking | Event Triggered Execution: Component Object Model Hijacking | T1546.015 |
| WMI subscription | Event Triggered Execution: WMI Event Subscription | T1546.003 |
| Bootkit | Pre-OS Boot: Bootkit | T1542.003 |
Privilege Escalation
| Behavior | Technique | ID |
|---|---|---|
| UAC bypass | Abuse Elevation Control Mechanism: Bypass UAC | T1548.002 |
| Token manipulation | Access Token Manipulation | T1134 |
| Process injection | Process Injection | T1055 |
| Exploitation | Exploitation for Privilege Escalation | T1068 |
Defense Evasion
| Behavior | Technique | ID |
|---|---|---|
| Process hollowing | Process Injection: Process Hollowing | T1055.012 |
| Timestomping | Indicator Removal: Timestomp | T1070.006 |
| File deletion | Indicator Removal: File Deletion | T1070.004 |
| Disabling AV | Impair Defenses: Disable or Modify Tools | T1562.001 |
| Code signing | Subvert Trust Controls: Code Signing | T1553.002 |
| Packing/obfuscation | Obfuscated Files or Information | T1027 |
| String encryption | Obfuscated Files or Information: Encrypted/Encoded File | T1027.013 |
| Reflective DLL loading | Reflective Code Loading | T1620 |
| AMSI bypass | Impair Defenses: Disable or Modify Tools | T1562.001 |
| ETW patching | Impair Defenses: Disable Windows Event Logging | T1562.002 |
| Unhooking | Impair Defenses: Disable or Modify Tools | T1562.001 |
Credential Access
| Behavior | Technique | ID |
|---|---|---|
| LSASS dump | OS Credential Dumping: LSASS Memory | T1003.001 |
| SAM dump | OS Credential Dumping: SAM | T1003.002 |
| Browser credential theft | Credentials from Password Stores: Browser | T1555.003 |
| Keylogging | Input Capture: Keylogging | T1056.001 |
| Clipboard capture | Clipboard Data | T1115 |
| Credential files | Unsecured Credentials: Credentials in Files | T1552.001 |
Discovery
| Behavior | Technique | ID |
|---|---|---|
| System info enumeration | System Information Discovery | T1082 |
| Process enumeration | Process Discovery | T1057 |
| File search | File and Directory Discovery | T1083 |
| Network enumeration | System Network Configuration Discovery | T1016 |
| Security software detection | Software Discovery: Security Software Discovery | T1518.001 |
| Domain enumeration | Domain Trust Discovery | T1482 |
| Account enumeration | Account Discovery | T1087 |
| Sandbox detection | Virtualization/Sandbox Evasion | T1497 |
| Debugger detection | Virtualization/Sandbox Evasion: System Checks | T1497.001 |
Lateral Movement
| Behavior | Technique | ID |
|---|---|---|
| RDP | Remote Services: Remote Desktop Protocol | T1021.001 |
| SMB/Windows Admin Shares | Remote Services: SMB/Windows Admin Shares | T1021.002 |
| WMI lateral | Windows Management Instrumentation | T1047 |
| PsExec-like | Remote Services: SMB/Windows Admin Shares | T1021.002 |
| SSH | Remote Services: SSH | T1021.004 |
Collection
| Behavior | Technique | ID |
|---|---|---|
| Screenshot | Screen Capture | T1113 |
| Keylogging | Input Capture: Keylogging | T1056.001 |
| Audio capture | Audio Capture | T1123 |
| Video capture | Video Capture | T1125 |
| Clipboard | Clipboard Data | T1115 |
| Archive collection | Archive Collected Data | T1560 |
| Email collection | Email Collection | T1114 |
Command & Control
| Behavior | Technique | ID |
|---|---|---|
| HTTP/HTTPS C2 | Application Layer Protocol: Web Protocols | T1071.001 |
| DNS tunneling | Application Layer Protocol: DNS | T1071.004 |
| Custom protocol | Non-Application Layer Protocol | T1095 |
| Domain fronting | Proxy: Domain Fronting | T1090.004 |
| Dead drop resolver | Web Service: Dead Drop Resolver | T1102.001 |
| Encrypted channel | Encrypted Channel | T1573 |
| Multi-stage payload | Ingress Tool Transfer | T1105 |
| Proxy/redirector | Proxy | T1090 |
| Fast flux DNS | Dynamic Resolution: Fast Flux DNS | T1568.001 |
| DGA | Dynamic Resolution: Domain Generation Algorithms | T1568.002 |
Exfiltration
| Behavior | Technique | ID |
|---|---|---|
| Exfil over C2 | Exfiltration Over C2 Channel | T1041 |
| Exfil over HTTP/S | Exfiltration Over Web Service | T1567 |
| Exfil to cloud storage | Exfiltration Over Web Service: Exfiltration to Cloud Storage | T1567.002 |
| Data compression | Archive Collected Data | T1560 |
| Data encryption | Archive Collected Data: Archive via Custom Method | T1560.003 |
| Exfil over DNS | Exfiltration Over Alternative Protocol | T1048 |
| Exfil size limits | Data Transfer Size Limits | T1030 |
Impact
| Behavior | Technique | ID |
|---|---|---|
| Data encryption (ransomware) | Data Encrypted for Impact | T1486 |
| Data destruction | Data Destruction | T1485 |
| Disk wipe | Disk Wipe | T1561 |
| Service stop | Service Stop | T1489 |
| Inhibit system recovery | Inhibit System Recovery | T1490 |
| Defacement | Defacement | T1491 |
| Resource hijacking (cryptominer) | Resource Hijacking | T1496 |
Common Malware Type Mappings
RAT (Remote Access Trojan)
T1059 (Execution) → T1547 (Persistence) → T1056 (Keylogging) → T1113 (Screenshot) → T1071 (C2) → T1041 (Exfil)
Ransomware
T1059 (Execution) → T1490 (Inhibit Recovery) → T1083 (File Discovery) → T1486 (Encryption) → T1489 (Service Stop)
Stealer
T1059 (Execution) → T1555 (Browser Creds) → T1539 (Cookies) → T1560 (Archive) → T1041 (Exfil)
Loader/Dropper
T1059 (Execution) → T1027 (Obfuscation) → T1105 (Download Payload) → T1055 (Injection) → T1547 (Persistence)
Cryptominer
T1059 (Execution) → T1547 (Persistence) → T1496 (Resource Hijacking) → T1071 (Pool Communication)
Malware Analysis Report Template
Use this structure for professional technical malware analysis reports.
Report Structure
# Malware Analysis Report: [MALWARE_NAME/FAMILY]
**Analysis Date:** YYYY-MM-DD
**Analyst:** [NAME]
**Classification:** [Trojan/Ransomware/RAT/Stealer/Loader/etc.]
**Severity:** [Critical/High/Medium/Low]
---
## Executive Summary
[2-3 sentences: What is this malware, what does it do, who is likely behind it, and what's the impact. Write for non-technical stakeholders.]
---
## Sample Information
| Property | Value |
|----------|-------|
| Filename | |
| File Size | |
| File Type | |
| MD5 | |
| SHA1 | |
| SHA256 | |
| Compile Timestamp | |
| First Seen | |
---
## Threat Intelligence
### Verdict: [MALICIOUS/SUSPICIOUS/UNKNOWN]
### Source Summary
| Source | Status | Key Findings |
|--------|--------|--------------|
| MalwareBazaar | Found/Not Found | [Family name, tags, first seen] |
| ThreatFox | Found/Not Found | [Threat type, malware family, confidence] |
| URLhaus | Found/Not Found | [Associated URLs, payload info] |
| VirusTotal | [X/Y] detections | [Top threat label, tags] |
| AbuseIPDB | [Score]% confidence | [Total reports, ISP, country] |
### Attribution & Classification
| Property | Value |
|----------|-------|
| Malware Family | [From TI sources] |
| Threat Type | [Trojan, Ransomware, Stealer, etc.] |
| First Seen (Wild) | [Earliest date from TI] |
| Last Seen | [Most recent activity] |
| Delivery Method | [If known from MalwareBazaar] |
| Associated Campaigns | [If any] |
### Related IOCs from TI
[List any additional IOCs discovered through TI enrichment - related samples, C2s, etc.]
---
## Key Findings
[Bullet points of the most significant discoveries - behaviors, C2, attribution indicators, etc.]
---
## Technical Analysis
### Static Analysis
#### File Characteristics
[PE headers, sections, entropy, packing detection, imports/exports of interest]
#### Code Analysis
[Key functions, algorithms, obfuscation techniques, notable strings]
### Dynamic Analysis
#### Execution Flow
[Startup behavior, persistence mechanisms, privilege escalation]
#### Network Activity
[C2 communication, protocols, domains/IPs contacted]
#### System Modifications
[Files created/modified, registry changes, processes spawned]
---
## MITRE ATT&CK Mapping
| Tactic | Technique | ID | Description |
|--------|-----------|----|----|
| | | | |
---
## Indicators of Compromise
### Network Indicators[Defanged URLs, domains, IPs]
### Host Indicators[File hashes, paths, registry keys, mutexes]
---
## Detection Opportunities
### YARA Rulerule [MALWARE_NAME] { meta: description = "" author = "" date = "" hash = ""
strings: $s1 = ""
condition: uint16(0) == 0x5A4D and all of them }
### Sigma Rule (Optional)
[For behavioral detection]
---
## Recommendations
1. [Immediate actions]
2. [Detection deployment]
3. [Hunting queries]
---
## References
- [Links to related reports, vendor analysis, etc.]
---
## Appendix
### A. Full Strings List
[If relevant]
### B. Complete IOC List
[Structured format for automated ingestion]
### C. Raw Threat Intelligence Data
[JSON output from triage.py for reference]Writing Guidelines
1. Be precise: Use exact values, timestamps, and hashes 2. Show your work: Include screenshots, code snippets, disassembly where relevant 3. Defang IOCs: Always defang URLs, IPs, and domains in the report body 4. Attribution carefully: State confidence levels (low/medium/high) for any attribution claims 5. Actionable output: Every report should enable defenders to detect and respond 6. TI enrichment: Always run triage.py first and include findings in the Threat Intelligence section
Threat Intelligence Workflow
1. Run triage on the sample hash:
python3 scripts/triage.py -t file /path/to/sample -f json > ti_results.json2. Extract key findings for the report:
- Verdict (malicious/suspicious/unknown)
- Malware family names from all sources
- Detection rates
- First/last seen dates
- Related IOCs
3. Cross-reference findings:
- Do multiple sources agree on family?
- Any conflicting information?
- What's the confidence level?
4. Include raw JSON in Appendix C for reproducibility
#!/usr/bin/env python3
"""
IOC (Indicators of Compromise) extraction and formatting tool.
Extracts, deduplicates, and defangs IOCs from files or text input.
"""
import argparse
import hashlib
import re
import sys
from pathlib import Path
from typing import TextIO
# IOC regex patterns
PATTERNS = {
"ipv4": r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b',
"ipv6": r'\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:\b|\b(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b',
"domain": r'\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+(?:com|net|org|io|info|biz|co|us|uk|de|ru|cn|xyz|top|site|online|club|app|dev|tech|shop|pro|me|tv|cc|pw|tk|ml|ga|cf|gq|onion|bit)\b',
"url": r'https?://[^\s<>"{}|\\^`\[\]]+',
"email": r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
"md5": r'\b[a-fA-F0-9]{32}\b',
"sha1": r'\b[a-fA-F0-9]{40}\b',
"sha256": r'\b[a-fA-F0-9]{64}\b',
"sha512": r'\b[a-fA-F0-9]{128}\b',
"cve": r'CVE-\d{4}-\d{4,}',
"registry": r'(?:HKEY_(?:LOCAL_MACHINE|CURRENT_USER|CLASSES_ROOT|USERS|CURRENT_CONFIG)|HKLM|HKCU|HKCR|HKU|HKCC)\\[^\s"\'<>]+',
"filepath_windows": r'[A-Za-z]:\\(?:[^\s<>"|?*\\]+\\)*[^\s<>"|?*\\]+',
"filepath_unix": r'(?:/(?:usr|etc|var|tmp|home|opt|bin|sbin|lib|root|dev|proc|sys)/[^\s<>"]+)',
"btc_wallet": r'\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b',
"eth_wallet": r'\b0x[a-fA-F0-9]{40}\b',
"xmr_wallet": r'\b4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}\b',
"useragent": r'Mozilla/5\.0[^"\'<>\n]+',
"mutex": r'(?:Global\\|Local\\)[^\s"\'<>]+',
}
# Common false positive patterns to filter
FALSE_POSITIVES = {
"ipv4": {"0.0.0.0", "127.0.0.1", "255.255.255.255", "192.168.0.1", "192.168.1.1", "10.0.0.1"},
"domain": {"example.com", "test.com", "localhost.com", "domain.com"},
"md5": set(), # Will filter hex strings that are too uniform
"sha1": set(),
"sha256": set(),
}
def is_false_positive_hash(hash_str: str) -> bool:
"""Check if a hash is likely a false positive (too uniform)."""
unique_chars = len(set(hash_str.lower()))
return unique_chars < 4 # Filter out strings like "0000...0000" or "aaaa...aaaa"
def defang_ip(ip: str) -> str:
"""Defang IP address: 192.168.1.1 -> 192[.]168[.]1[.]1"""
return ip.replace(".", "[.]")
def defang_url(url: str) -> str:
"""Defang URL: http://evil.com -> hxxp://evil[.]com"""
result = url.replace("http://", "hxxp://").replace("https://", "hxxps://")
# Defang domain part
result = re.sub(r'(?<=://)([^/]+)', lambda m: m.group(1).replace(".", "[.]"), result)
return result
def defang_domain(domain: str) -> str:
"""Defang domain: evil.com -> evil[.]com"""
return domain.replace(".", "[.]")
def defang_email(email: str) -> str:
"""Defang email: bad@evil.com -> bad[@]evil[.]com"""
return email.replace("@", "[@]").replace(".", "[.]")
def refang(text: str) -> str:
"""Refang IOCs in text (reverse defanging)."""
result = text
result = result.replace("[.]", ".")
result = result.replace("[@]", "@")
result = result.replace("hxxp://", "http://")
result = result.replace("hxxps://", "https://")
result = result.replace("[:]", ":")
return result
def extract_iocs(text: str, defang: bool = True, include_types: list = None) -> dict:
"""
Extract IOCs from text.
Args:
text: Input text to extract IOCs from
defang: Whether to defang IOCs (default True)
include_types: List of IOC types to include (default: all)
Returns:
Dictionary of IOC type -> list of unique IOCs
"""
results = {}
types_to_extract = include_types if include_types else list(PATTERNS.keys())
for ioc_type in types_to_extract:
if ioc_type not in PATTERNS:
continue
pattern = PATTERNS[ioc_type]
matches = set(re.findall(pattern, text, re.IGNORECASE if ioc_type not in ["md5", "sha1", "sha256", "sha512"] else 0))
# Filter false positives
fp_set = FALSE_POSITIVES.get(ioc_type, set())
matches = {m for m in matches if m.lower() not in {fp.lower() for fp in fp_set}}
# Filter uniform hashes
if ioc_type in ["md5", "sha1", "sha256", "sha512"]:
matches = {m for m in matches if not is_false_positive_hash(m)}
# Filter private IPs if extracting IPs
if ioc_type == "ipv4":
public_only = set()
for ip in matches:
octets = ip.split(".")
# Skip private ranges
if octets[0] == "10":
continue
if octets[0] == "172" and 16 <= int(octets[1]) <= 31:
continue
if octets[0] == "192" and octets[1] == "168":
continue
if octets[0] == "169" and octets[1] == "254":
continue
public_only.add(ip)
matches = public_only
if matches:
# Apply defanging
if defang:
if ioc_type == "ipv4" or ioc_type == "ipv6":
matches = {defang_ip(m) for m in matches}
elif ioc_type == "url":
matches = {defang_url(m) for m in matches}
elif ioc_type == "domain":
matches = {defang_domain(m) for m in matches}
elif ioc_type == "email":
matches = {defang_email(m) for m in matches}
results[ioc_type] = sorted(matches)
return results
def format_output(iocs: dict, format_type: str = "text", include_stats: bool = True) -> str:
"""Format IOCs for output."""
if format_type == "json":
import json
output = {
"iocs": iocs,
}
if include_stats:
output["statistics"] = {ioc_type: len(values) for ioc_type, values in iocs.items()}
output["total"] = sum(len(v) for v in iocs.values())
return json.dumps(output, indent=2)
elif format_type == "csv":
lines = ["type,value"]
for ioc_type, values in iocs.items():
for value in values:
# Escape quotes in CSV
escaped = value.replace('"', '""')
lines.append(f'{ioc_type},"{escaped}"')
return "\n".join(lines)
elif format_type == "stix":
# Simplified STIX-like output
indicators = []
for ioc_type, values in iocs.items():
for value in values:
indicators.append({
"type": "indicator",
"pattern_type": ioc_type,
"pattern": value,
})
import json
return json.dumps({"type": "bundle", "objects": indicators}, indent=2)
else: # text format
lines = []
lines.append("=" * 60)
lines.append("EXTRACTED INDICATORS OF COMPROMISE")
lines.append("=" * 60)
total = 0
for ioc_type, values in sorted(iocs.items()):
if values:
type_display = ioc_type.upper().replace("_", " ")
lines.append(f"\n[{type_display}] ({len(values)} found)")
lines.append("-" * 40)
for value in values:
lines.append(f" {value}")
total += len(values)
if include_stats:
lines.append("\n" + "=" * 60)
lines.append(f"TOTAL IOCs EXTRACTED: {total}")
lines.append("=" * 60)
return "\n".join(lines)
def extract_from_file(filepath: str, defang: bool = True) -> dict:
"""Extract IOCs from a file (binary or text)."""
path = Path(filepath)
if not path.exists():
raise FileNotFoundError(f"File not found: {filepath}")
# Try to read as text first
try:
text = path.read_text(encoding='utf-8', errors='ignore')
except Exception:
# Fall back to binary read and decode
data = path.read_bytes()
text = data.decode('utf-8', errors='ignore')
# Also try to extract ASCII strings from binary
ascii_strings = re.findall(rb'[\x20-\x7e]{4,}', data)
text += " " + " ".join(s.decode('ascii', errors='ignore') for s in ascii_strings)
return extract_iocs(text, defang=defang)
def main():
parser = argparse.ArgumentParser(description="Extract and defang IOCs from files or stdin")
parser.add_argument("input", nargs="?", help="Input file (reads stdin if not provided)")
parser.add_argument("-f", "--format", choices=["text", "json", "csv", "stix"], default="text",
help="Output format (default: text)")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
parser.add_argument("--no-defang", action="store_true", help="Don't defang IOCs")
parser.add_argument("--refang", action="store_true", help="Refang IOCs in input (reverse defanging)")
parser.add_argument("-t", "--types", nargs="+", choices=list(PATTERNS.keys()),
help="Only extract specific IOC types")
parser.add_argument("--list-types", action="store_true", help="List available IOC types")
args = parser.parse_args()
if args.list_types:
print("Available IOC types:")
for ioc_type in sorted(PATTERNS.keys()):
print(f" - {ioc_type}")
return
# Read input
if args.input:
if args.refang:
text = Path(args.input).read_text()
text = refang(text)
iocs = extract_iocs(text, defang=not args.no_defang, include_types=args.types)
else:
iocs = extract_from_file(args.input, defang=not args.no_defang)
if args.types:
iocs = {k: v for k, v in iocs.items() if k in args.types}
else:
text = sys.stdin.read()
if args.refang:
text = refang(text)
iocs = extract_iocs(text, defang=not args.no_defang, include_types=args.types)
# Format output
output = format_output(iocs, args.format)
if args.output:
Path(args.output).write_text(output)
print(f"Results written to {args.output}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Malware Data Collector
Collects all analysis data into a structured format for Claude to analyze.
Claude performs the reasoning and writes the analyst-grade report.
Usage:
python3 generate_report.py /path/to/sample.exe -f json
python3 generate_report.py /path/to/sample.exe -f text
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
# Import sibling modules
script_dir = Path(__file__).parent
sys.path.insert(0, str(script_dir))
from static_analysis import analyze_file
from triage import MalwareTriage
from extract_iocs import extract_from_file
def collect_analysis_data(
filepath: str,
skip_triage: bool = False,
vt_key: str = None,
abuseipdb_key: str = None,
abusech_key: str = None,
) -> dict:
"""Collect all analysis data into a structured dict."""
path = Path(filepath)
if not path.exists():
return {"error": f"File not found: {filepath}"}
data = {
"sample": {
"filepath": str(path.absolute()),
"filename": path.name,
"analysis_time": datetime.now(timezone.utc).isoformat(),
},
"static_analysis": None,
"threat_intelligence": None,
"extracted_iocs": None,
}
# Static analysis
print("[*] Running static analysis...", file=sys.stderr)
static_results = analyze_file(filepath)
if "error" not in static_results:
data["static_analysis"] = static_results
else:
data["static_analysis"] = {"error": static_results["error"]}
# Threat intelligence
if not skip_triage:
print("[*] Querying threat intelligence...", file=sys.stderr)
triage = MalwareTriage(
vt_key=vt_key,
abuseipdb_key=abuseipdb_key,
abusech_key=abusech_key,
verbose=True
)
data["threat_intelligence"] = triage.triage_file(filepath)
else:
data["threat_intelligence"] = {"skipped": True}
# IOC extraction
print("[*] Extracting IOCs...", file=sys.stderr)
data["extracted_iocs"] = extract_from_file(filepath, defang=True)
return data
def format_as_text(data: dict) -> str:
"""Format collected data as readable text for Claude to analyze."""
lines = []
lines.append("=" * 70)
lines.append("MALWARE ANALYSIS DATA COLLECTION")
lines.append("=" * 70)
lines.append("")
# Sample info
sample = data.get("sample", {})
lines.append(f"Sample: {sample.get('filename', 'unknown')}")
lines.append(f"Analysis Time: {sample.get('analysis_time', 'unknown')}")
lines.append("")
# Static analysis
lines.append("-" * 70)
lines.append("STATIC ANALYSIS")
lines.append("-" * 70)
static = data.get("static_analysis", {})
if static.get("error"):
lines.append(f"Error: {static['error']}")
else:
# Hashes
hashes = static.get("hashes", {})
lines.append(f"MD5: {hashes.get('md5', 'N/A')}")
lines.append(f"SHA1: {hashes.get('sha1', 'N/A')}")
lines.append(f"SHA256: {hashes.get('sha256', 'N/A')}")
lines.append("")
# File info
file_info = static.get("file", {})
lines.append(f"File Type: {file_info.get('type', 'unknown')}")
lines.append(f"File Size: {file_info.get('size', 0):,} bytes")
lines.append("")
# PE analysis
pe = static.get("pe_analysis", {})
if pe:
lines.append(f"Architecture: {pe.get('architecture', 'N/A')} ({pe.get('type', '')})")
lines.append(f"Compile Time: {pe.get('compile_timestamp', 'N/A')}")
lines.append(f"Overall Entropy: {pe.get('entropy', 0):.4f}")
lines.append("")
# Sections
sections = pe.get("sections", [])
if sections:
lines.append("PE Sections:")
for s in sections:
lines.append(f" {s.get('name', '?'):10} entropy={s.get('entropy', 0):.2f} chars={s.get('characteristics', '')}")
lines.append("")
# Suspicious indicators
suspicious = static.get("suspicious_indicators", {})
apis = suspicious.get("suspicious_apis", [])
if apis:
lines.append(f"Suspicious APIs ({len(apis)}):")
for api in apis:
lines.append(f" - {api}")
lines.append("")
urls = suspicious.get("urls", [])
if urls:
lines.append(f"URLs in strings ({len(urls)}):")
for url in urls[:10]:
lines.append(f" - {url}")
if len(urls) > 10:
lines.append(f" ... and {len(urls) - 10} more")
lines.append("")
ips = suspicious.get("ip_addresses", [])
if ips:
lines.append(f"IP Addresses ({len(ips)}):")
for ip in ips[:10]:
lines.append(f" - {ip}")
lines.append("")
registry = suspicious.get("registry_keys", [])
if registry:
lines.append(f"Registry Keys ({len(registry)}):")
for key in registry[:5]:
lines.append(f" - {key}")
lines.append("")
# Threat Intelligence
lines.append("-" * 70)
lines.append("THREAT INTELLIGENCE")
lines.append("-" * 70)
ti = data.get("threat_intelligence", {})
if ti.get("skipped"):
lines.append("Skipped (offline mode)")
else:
verdict = ti.get("verdict", "unknown")
lines.append(f"Overall Verdict: {verdict.upper()}")
lines.append("")
summary = ti.get("summary", {})
if summary.get("malware_families"):
lines.append(f"Malware Families: {', '.join(summary['malware_families'])}")
if summary.get("virustotal_detection"):
lines.append(f"VT Detection: {summary['virustotal_detection']}")
lines.append("")
# Source details
for source in ti.get("sources", []):
src_name = source.get("source", "unknown").upper()
if source.get("error"):
lines.append(f"{src_name}: Error - {source['error']}")
elif source.get("found"):
lines.append(f"{src_name}: FOUND")
if source.get("signature"):
lines.append(f" Family: {source['signature']}")
if source.get("detection_rate"):
lines.append(f" Detection: {source['detection_rate']}")
if source.get("popular_threat_names"):
lines.append(f" Threat: {source['popular_threat_names']}")
if source.get("first_seen"):
lines.append(f" First Seen: {source['first_seen']}")
if source.get("tags"):
lines.append(f" Tags: {', '.join(source['tags'][:5])}")
else:
lines.append(f"{src_name}: Not found")
lines.append("")
# Extracted IOCs
lines.append("-" * 70)
lines.append("EXTRACTED IOCs")
lines.append("-" * 70)
iocs = data.get("extracted_iocs", {})
for ioc_type, values in iocs.items():
if values:
lines.append(f"{ioc_type} ({len(values)}):")
for v in values[:5]:
lines.append(f" - {v}")
if len(values) > 5:
lines.append(f" ... and {len(values) - 5} more")
lines.append("")
lines.append("=" * 70)
lines.append("END OF DATA COLLECTION")
lines.append("=" * 70)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Collect malware analysis data for Claude to analyze",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
This script collects data. Claude performs the analysis and reasoning.
Examples:
%(prog)s sample.exe -f text # Human-readable output
%(prog)s sample.exe -f json # Structured JSON
%(prog)s sample.exe --skip-triage # Offline mode
"""
)
parser.add_argument("file", help="Path to malware sample")
parser.add_argument("-f", "--format", choices=["text", "json"], default="text",
help="Output format (default: text)")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
parser.add_argument("--skip-triage", action="store_true", help="Skip TI lookups")
parser.add_argument("--abusech-key", help="abuse.ch Auth-Key")
parser.add_argument("--vt-key", help="VirusTotal API key")
parser.add_argument("--abuseipdb-key", help="AbuseIPDB API key")
args = parser.parse_args()
data = collect_analysis_data(
filepath=args.file,
skip_triage=args.skip_triage,
vt_key=args.vt_key,
abuseipdb_key=args.abuseipdb_key,
abusech_key=args.abusech_key,
)
if args.format == "json":
output = json.dumps(data, indent=2, default=str)
else:
output = format_as_text(data)
if args.output:
Path(args.output).write_text(output)
print(f"Data written to {args.output}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Static malware analysis script.
Extracts hashes, PE metadata, strings, imports, entropy, and suspicious indicators.
"""
import argparse
import hashlib
import math
import re
import struct
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
def calculate_hashes(data: bytes) -> dict:
"""Calculate MD5, SHA1, SHA256, and imphash placeholder."""
return {
"md5": hashlib.md5(data).hexdigest(),
"sha1": hashlib.sha1(data).hexdigest(),
"sha256": hashlib.sha256(data).hexdigest(),
}
def calculate_entropy(data: bytes) -> float:
"""Calculate Shannon entropy of data."""
if not data:
return 0.0
counter = Counter(data)
length = len(data)
entropy = -sum((count / length) * math.log2(count / length) for count in counter.values())
return round(entropy, 4)
def extract_strings(data: bytes, min_length: int = 4) -> dict:
"""Extract ASCII and Unicode strings."""
ascii_pattern = rb'[\x20-\x7e]{%d,}' % min_length
unicode_pattern = rb'(?:[\x20-\x7e]\x00){%d,}' % min_length
ascii_strings = [s.decode('ascii', errors='ignore') for s in re.findall(ascii_pattern, data)]
unicode_strings = [s.decode('utf-16-le', errors='ignore') for s in re.findall(unicode_pattern, data)]
return {
"ascii": ascii_strings,
"unicode": unicode_strings,
"total_count": len(ascii_strings) + len(unicode_strings)
}
def find_suspicious_strings(strings: list) -> dict:
"""Identify strings that indicate malicious behavior."""
patterns = {
"urls": r'https?://[^\s<>"{}|\\^`\[\]]+',
"ips": r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b',
"emails": r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
"registry_keys": r'(?:HKEY_|HKLM|HKCU|HKCR)[\\a-zA-Z0-9_]+',
"file_paths": r'[A-Za-z]:\\[^\s<>"|?*]+',
"crypto_wallets": {
"btc": r'\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b',
"eth": r'\b0x[a-fA-F0-9]{40}\b',
}
}
suspicious_apis = [
"VirtualAlloc", "VirtualProtect", "WriteProcessMemory", "CreateRemoteThread",
"NtUnmapViewOfSection", "SetWindowsHookEx", "GetAsyncKeyState", "RegSetValueEx",
"InternetOpen", "URLDownloadToFile", "WinExec", "ShellExecute", "CreateProcess",
"LoadLibrary", "GetProcAddress", "IsDebuggerPresent", "CheckRemoteDebuggerPresent",
"CryptEncrypt", "CryptDecrypt", "CryptAcquireContext", "BCryptEncrypt",
"socket", "connect", "send", "recv", "WSAStartup", "gethostbyname",
]
all_strings = " ".join(strings)
results = {}
for name, pattern in patterns.items():
if isinstance(pattern, dict):
results[name] = {}
for subname, subpattern in pattern.items():
matches = list(set(re.findall(subpattern, all_strings)))
if matches:
results[name][subname] = matches
else:
matches = list(set(re.findall(pattern, all_strings)))
if matches:
results[name] = matches
found_apis = [api for api in suspicious_apis if api.lower() in all_strings.lower()]
if found_apis:
results["suspicious_apis"] = found_apis
return results
def parse_pe_header(data: bytes) -> dict | None:
"""Parse PE header for metadata."""
if len(data) < 64:
return None
# Check MZ signature
if data[:2] != b'MZ':
return None
try:
pe_offset = struct.unpack('<I', data[0x3C:0x40])[0]
if len(data) < pe_offset + 4 or data[pe_offset:pe_offset+4] != b'PE\x00\x00':
return None
# COFF header
machine = struct.unpack('<H', data[pe_offset+4:pe_offset+6])[0]
num_sections = struct.unpack('<H', data[pe_offset+6:pe_offset+8])[0]
timestamp = struct.unpack('<I', data[pe_offset+8:pe_offset+12])[0]
characteristics = struct.unpack('<H', data[pe_offset+22:pe_offset+24])[0]
# Optional header
optional_offset = pe_offset + 24
magic = struct.unpack('<H', data[optional_offset:optional_offset+2])[0]
is_64bit = magic == 0x20b
arch = "x64" if is_64bit else "x86"
machine_types = {
0x14c: "i386",
0x8664: "AMD64",
0x1c0: "ARM",
0xaa64: "ARM64",
}
compile_time = datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat() if timestamp else "Unknown"
# Parse sections
optional_header_size = struct.unpack('<H', data[pe_offset+20:pe_offset+22])[0]
section_offset = pe_offset + 24 + optional_header_size
sections = []
for i in range(min(num_sections, 20)): # Limit to 20 sections
sec_start = section_offset + (i * 40)
if sec_start + 40 > len(data):
break
name = data[sec_start:sec_start+8].rstrip(b'\x00').decode('ascii', errors='ignore')
virtual_size = struct.unpack('<I', data[sec_start+8:sec_start+12])[0]
raw_size = struct.unpack('<I', data[sec_start+16:sec_start+20])[0]
raw_offset = struct.unpack('<I', data[sec_start+20:sec_start+24])[0]
sec_chars = struct.unpack('<I', data[sec_start+36:sec_start+40])[0]
# Calculate section entropy
if raw_offset + raw_size <= len(data):
sec_data = data[raw_offset:raw_offset+raw_size]
sec_entropy = calculate_entropy(sec_data)
else:
sec_entropy = 0.0
sections.append({
"name": name,
"virtual_size": virtual_size,
"raw_size": raw_size,
"entropy": sec_entropy,
"executable": bool(sec_chars & 0x20000000),
"writable": bool(sec_chars & 0x80000000),
})
# Detect packing indicators
packing_indicators = []
for sec in sections:
if sec["entropy"] > 7.0:
packing_indicators.append(f"High entropy in {sec['name']}: {sec['entropy']}")
if sec["name"] in ["UPX0", "UPX1", ".packed", ".aspack", ".adata"]:
packing_indicators.append(f"Known packer section: {sec['name']}")
# Check for suspicious section characteristics
for sec in sections:
if sec["executable"] and sec["writable"]:
packing_indicators.append(f"RWX section: {sec['name']}")
return {
"type": "PE32+" if is_64bit else "PE32",
"architecture": arch,
"machine": machine_types.get(machine, f"Unknown (0x{machine:x})"),
"compile_timestamp": compile_time,
"timestamp_raw": timestamp,
"num_sections": num_sections,
"sections": sections,
"is_dll": bool(characteristics & 0x2000),
"is_executable": bool(characteristics & 0x0002),
"packing_indicators": packing_indicators,
}
except (struct.error, ValueError, IndexError) as e:
return {"error": str(e)}
def detect_file_type(data: bytes) -> str:
"""Detect file type based on magic bytes."""
signatures = {
b'MZ': "PE Executable (Windows)",
b'\x7fELF': "ELF Executable (Linux)",
b'\xfe\xed\xfa\xce': "Mach-O (macOS, 32-bit)",
b'\xfe\xed\xfa\xcf': "Mach-O (macOS, 64-bit)",
b'\xca\xfe\xba\xbe': "Mach-O Universal Binary",
b'PK\x03\x04': "ZIP Archive (possibly DOCX/XLSX/JAR)",
b'Rar!\x1a\x07': "RAR Archive",
b'\x1f\x8b': "GZIP",
b'%PDF': "PDF Document",
b'\xd0\xcf\x11\xe0': "OLE Compound (DOC/XLS/PPT)",
b'{\rt': "RTF Document",
}
for sig, filetype in signatures.items():
if data.startswith(sig):
return filetype
# Check for scripts
first_line = data[:100].split(b'\n')[0]
if b'#!/' in first_line:
if b'python' in first_line.lower():
return "Python Script"
elif b'bash' in first_line or b'/sh' in first_line:
return "Shell Script"
elif b'perl' in first_line:
return "Perl Script"
if data[:50].strip().startswith((b'<script', b'<html', b'<!DOCTYPE')):
return "HTML/JavaScript"
return "Unknown"
def analyze_file(filepath: str, output_format: str = "text") -> dict:
"""Perform complete static analysis on a file."""
path = Path(filepath)
if not path.exists():
return {"error": f"File not found: {filepath}"}
data = path.read_bytes()
# Basic info
hashes = calculate_hashes(data)
file_type = detect_file_type(data)
overall_entropy = calculate_entropy(data)
# Strings
strings = extract_strings(data)
all_strings = strings["ascii"] + strings["unicode"]
suspicious = find_suspicious_strings(all_strings)
# PE analysis if applicable
pe_info = None
if file_type.startswith("PE"):
pe_info = parse_pe_header(data)
results = {
"file": {
"name": path.name,
"size": len(data),
"type": file_type,
},
"hashes": hashes,
"entropy": {
"overall": overall_entropy,
"assessment": "Likely packed/encrypted" if overall_entropy > 7.0 else "Normal" if overall_entropy < 6.5 else "Potentially compressed",
},
"strings": {
"total": strings["total_count"],
"ascii_count": len(strings["ascii"]),
"unicode_count": len(strings["unicode"]),
},
"suspicious_indicators": suspicious,
}
if pe_info:
results["pe_analysis"] = pe_info
return results
def format_output(results: dict, format_type: str = "text") -> str:
"""Format analysis results for output."""
if format_type == "json":
import json
return json.dumps(results, indent=2, default=str)
# Text format
lines = []
lines.append("=" * 70)
lines.append("STATIC MALWARE ANALYSIS REPORT")
lines.append("=" * 70)
# File info
f = results["file"]
lines.append(f"\n[FILE INFORMATION]")
lines.append(f" Filename: {f['name']}")
lines.append(f" Size: {f['size']:,} bytes")
lines.append(f" Type: {f['type']}")
# Hashes
h = results["hashes"]
lines.append(f"\n[CRYPTOGRAPHIC HASHES]")
lines.append(f" MD5: {h['md5']}")
lines.append(f" SHA1: {h['sha1']}")
lines.append(f" SHA256: {h['sha256']}")
# Entropy
e = results["entropy"]
lines.append(f"\n[ENTROPY ANALYSIS]")
lines.append(f" Overall: {e['overall']}")
lines.append(f" Assessment: {e['assessment']}")
# PE Analysis
if "pe_analysis" in results and results["pe_analysis"]:
pe = results["pe_analysis"]
if "error" not in pe:
lines.append(f"\n[PE HEADER ANALYSIS]")
lines.append(f" Format: {pe['type']}")
lines.append(f" Architecture: {pe['architecture']} ({pe['machine']})")
lines.append(f" Compile Time: {pe['compile_timestamp']}")
lines.append(f" DLL: {'Yes' if pe['is_dll'] else 'No'}")
lines.append(f" Sections: {pe['num_sections']}")
if pe.get("sections"):
lines.append(f"\n [SECTIONS]")
for sec in pe["sections"]:
flags = []
if sec["executable"]: flags.append("X")
if sec["writable"]: flags.append("W")
flag_str = ",".join(flags) if flags else "-"
lines.append(f" {sec['name']:<10} Size: {sec['raw_size']:<10} Entropy: {sec['entropy']:<6} Flags: {flag_str}")
if pe.get("packing_indicators"):
lines.append(f"\n [PACKING INDICATORS]")
for indicator in pe["packing_indicators"]:
lines.append(f" ⚠ {indicator}")
# Strings summary
s = results["strings"]
lines.append(f"\n[STRINGS ANALYSIS]")
lines.append(f" Total: {s['total']}")
lines.append(f" ASCII: {s['ascii_count']}")
lines.append(f" Unicode: {s['unicode_count']}")
# Suspicious indicators
si = results["suspicious_indicators"]
if si:
lines.append(f"\n[SUSPICIOUS INDICATORS]")
if si.get("urls"):
lines.append(f" URLs ({len(si['urls'])}):")
for url in si["urls"][:10]:
lines.append(f" - {url}")
if len(si["urls"]) > 10:
lines.append(f" ... and {len(si['urls']) - 10} more")
if si.get("ips"):
lines.append(f" IP Addresses ({len(si['ips'])}):")
for ip in si["ips"][:10]:
lines.append(f" - {ip}")
if si.get("registry_keys"):
lines.append(f" Registry Keys ({len(si['registry_keys'])}):")
for key in si["registry_keys"][:5]:
lines.append(f" - {key}")
if si.get("file_paths"):
lines.append(f" File Paths ({len(si['file_paths'])}):")
for path in si["file_paths"][:5]:
lines.append(f" - {path}")
if si.get("suspicious_apis"):
lines.append(f" Suspicious APIs ({len(si['suspicious_apis'])}):")
for api in sorted(si["suspicious_apis"]):
lines.append(f" - {api}")
if si.get("crypto_wallets"):
for wallet_type, addresses in si["crypto_wallets"].items():
lines.append(f" {wallet_type.upper()} Wallets ({len(addresses)}):")
for addr in addresses[:3]:
lines.append(f" - {addr}")
lines.append("\n" + "=" * 70)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Static malware analysis tool")
parser.add_argument("file", help="Path to file to analyze")
parser.add_argument("-f", "--format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
args = parser.parse_args()
results = analyze_file(args.file, args.format)
output = format_output(results, args.format)
if args.output:
Path(args.output).write_text(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Malware Triage - Multi-source threat intelligence lookup.
API keys can be configured via:
1. Config file: ~/.config/malware-triage/config.json
2. Environment variables: VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
3. Command line: --vt-key, --abuseipdb-key
"""
import argparse
import hashlib
import json
import os
import re
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from urllib.parse import quote
# Check for requests library
try:
import requests
except ImportError:
print("Error: requests library required. Install with: pip install requests --break-system-packages")
sys.exit(1)
def load_config() -> dict:
"""
Load API keys from config file.
Config file locations (in order of priority):
1. Skill's own config directory (bundled with skill)
2. /mnt/user-data/uploads/config.json (Claude Desktop uploads)
3. ~/.config/malware-triage/config.json
4. ./config.json (current directory)
"""
# Get the skill's directory (parent of scripts/)
script_dir = Path(__file__).parent
skill_dir = script_dir.parent
config_paths = [
# Bundled with skill
skill_dir / "config" / "config.json",
skill_dir / "config" / "api_keys.json",
skill_dir / "config.json",
# Claude Desktop uploads
Path("/mnt/user-data/uploads/config.json"),
Path("/mnt/user-data/uploads/api_keys.json"),
# User home config
Path.home() / ".config" / "malware-triage" / "config.json",
Path.home() / ".malware-triage.json",
# Current directory
Path("config.json"),
]
for config_path in config_paths:
if config_path.exists():
try:
with open(config_path) as f:
config = json.load(f)
print(f"[*] Loaded config from: {config_path}", file=sys.stderr)
return config
except (json.JSONDecodeError, IOError) as e:
print(f"[!] Failed to load {config_path}: {e}", file=sys.stderr)
continue
return {}
def get_api_key(key_name: str, cli_value: Optional[str] = None) -> Optional[str]:
"""
Get API key from multiple sources (priority order):
1. Command line argument
2. Environment variable
3. Config file
"""
# CLI takes priority
if cli_value:
return cli_value
# Then environment variable
env_key = os.environ.get(key_name)
if env_key:
return env_key
# Finally config file
config = load_config()
config_key_map = {
"VIRUSTOTAL_API_KEY": ["virustotal_api_key", "vt_api_key", "virustotal"],
"ABUSEIPDB_API_KEY": ["abuseipdb_api_key", "abuseipdb"],
"ABUSECH_AUTH_KEY": ["abusech_auth_key", "abuse_ch_auth_key", "malwarebazaar_auth_key", "auth_key"],
}
possible_keys = config_key_map.get(key_name, [key_name.lower()])
for config_key in possible_keys:
if config_key in config:
return config[config_key]
return None
def create_default_config():
"""Create a default config file template."""
config_dir = Path.home() / ".config" / "malware-triage"
config_path = config_dir / "config.json"
if config_path.exists():
print(f"Config already exists: {config_path}")
return
config_dir.mkdir(parents=True, exist_ok=True)
default_config = {
"abusech_auth_key": "YOUR_ABUSECH_AUTH_KEY_HERE",
"virustotal_api_key": "YOUR_VT_API_KEY_HERE",
"abuseipdb_api_key": "YOUR_ABUSEIPDB_API_KEY_HERE",
}
with open(config_path, "w") as f:
json.dump(default_config, f, indent=2)
print(f"Created config file: {config_path}")
print("Edit this file with your API keys.")
print()
print("Get your keys from:")
print(" - abuse.ch (MalwareBazaar/ThreatFox): https://auth.abuse.ch/")
print(" - VirusTotal: https://www.virustotal.com/gui/my-apikey")
print(" - AbuseIPDB: https://www.abuseipdb.com/account/api")
class ThreatIntelClient:
"""Base class for threat intel API clients."""
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "MalwareTriage/1.0 (Threat Intelligence Lookup Tool)",
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
})
def _safe_request(self, method: str, url: str, **kwargs) -> Optional[dict]:
"""Make a request with error handling."""
try:
resp = self.session.request(method, url, timeout=30, **kwargs)
if resp.status_code == 200:
return resp.json()
elif resp.status_code == 404:
return {"error": "not_found", "status": 404}
elif resp.status_code == 429:
return {"error": "rate_limited", "status": 429}
elif resp.status_code == 401:
return {"error": "unauthorized", "status": 401, "detail": resp.text[:200]}
else:
return {"error": f"http_{resp.status_code}", "status": resp.status_code, "detail": resp.text[:200]}
except requests.exceptions.Timeout:
return {"error": "timeout"}
except requests.exceptions.RequestException as e:
return {"error": str(e)}
except json.JSONDecodeError:
return {"error": "invalid_json"}
class MalwareBazaar(ThreatIntelClient):
"""MalwareBazaar API client (abuse.ch) - Requires Auth-Key."""
BASE_URL = "https://mb-api.abuse.ch/api/v1/"
def __init__(self, auth_key: Optional[str] = None):
super().__init__()
self.auth_key = auth_key
if self.auth_key:
self.session.headers.update({"Auth-Key": self.auth_key})
def _is_configured(self) -> bool:
return bool(self.auth_key)
def query_hash(self, hash_value: str) -> dict:
"""Query MalwareBazaar for a file hash."""
if not self._is_configured():
return {"source": "malwarebazaar", "found": False, "error": "no_auth_key"}
data = {"query": "get_info", "hash": hash_value}
result = self._safe_request("POST", self.BASE_URL, data=data)
if not result or "error" in result:
return {"source": "malwarebazaar", "found": False, "error": result.get("error") if result else "request_failed", "detail": result.get("detail") if result else None}
if result.get("query_status") == "hash_not_found":
return {"source": "malwarebazaar", "found": False}
if result.get("query_status") == "ok" and result.get("data"):
sample = result["data"][0]
return {
"source": "malwarebazaar",
"found": True,
"sha256": sample.get("sha256_hash"),
"sha1": sample.get("sha1_hash"),
"md5": sample.get("md5_hash"),
"file_name": sample.get("file_name"),
"file_type": sample.get("file_type"),
"file_size": sample.get("file_size"),
"signature": sample.get("signature"), # Malware family
"first_seen": sample.get("first_seen"),
"last_seen": sample.get("last_seen"),
"tags": sample.get("tags", []),
"intelligence": sample.get("intelligence", {}),
"delivery_method": sample.get("delivery_method"),
"comment": sample.get("comment"),
"reporter": sample.get("reporter"),
"yara_rules": sample.get("yara_rules", []),
"vendor_intel": sample.get("vendor_intel", {}),
}
return {"source": "malwarebazaar", "found": False, "raw_status": result.get("query_status")}
def query_tag(self, tag: str, limit: int = 10) -> dict:
"""Query MalwareBazaar for samples by tag (e.g., malware family)."""
if not self._is_configured():
return {"source": "malwarebazaar", "found": False, "error": "no_auth_key"}
data = {"query": "get_taginfo", "tag": tag, "limit": limit}
result = self._safe_request("POST", self.BASE_URL, data=data)
if not result or "error" in result:
return {"source": "malwarebazaar", "found": False}
if result.get("query_status") == "ok" and result.get("data"):
samples = []
for sample in result["data"][:limit]:
samples.append({
"sha256": sample.get("sha256_hash"),
"file_name": sample.get("file_name"),
"signature": sample.get("signature"),
"first_seen": sample.get("first_seen"),
})
return {"source": "malwarebazaar", "found": True, "tag": tag, "samples": samples, "count": len(samples)}
return {"source": "malwarebazaar", "found": False}
class ThreatFox(ThreatIntelClient):
"""ThreatFox API client (abuse.ch) - Requires Auth-Key."""
BASE_URL = "https://threatfox-api.abuse.ch/api/v1/"
def __init__(self, auth_key: Optional[str] = None):
super().__init__()
self.auth_key = auth_key
if self.auth_key:
self.session.headers.update({"Auth-Key": self.auth_key})
def _is_configured(self) -> bool:
return bool(self.auth_key)
def query_ioc(self, ioc: str) -> dict:
"""Query ThreatFox for an IOC (hash, IP, domain, URL)."""
if not self._is_configured():
return {"source": "threatfox", "found": False, "error": "no_auth_key"}
data = {"query": "search_ioc", "search_term": ioc}
result = self._safe_request("POST", self.BASE_URL, data=data)
if not result or "error" in result:
return {"source": "threatfox", "found": False, "error": result.get("error") if result else "request_failed", "detail": result.get("detail") if result else None}
if result.get("query_status") == "no_result":
return {"source": "threatfox", "found": False}
if result.get("query_status") == "ok" and result.get("data"):
iocs = []
for entry in result["data"]:
iocs.append({
"ioc": entry.get("ioc"),
"ioc_type": entry.get("ioc_type"),
"threat_type": entry.get("threat_type"),
"malware": entry.get("malware"),
"malware_alias": entry.get("malware_alias"),
"malware_malpedia": entry.get("malware_malpedia"),
"confidence": entry.get("confidence_level"),
"first_seen": entry.get("first_seen"),
"last_seen": entry.get("last_seen"),
"reporter": entry.get("reporter"),
"tags": entry.get("tags", []),
})
return {"source": "threatfox", "found": True, "iocs": iocs, "count": len(iocs)}
return {"source": "threatfox", "found": False}
def query_malware(self, malware_name: str, limit: int = 10) -> dict:
"""Query ThreatFox for IOCs associated with a malware family."""
if not self._is_configured():
return {"source": "threatfox", "found": False, "error": "no_auth_key"}
data = {"query": "malwareinfo", "malware": malware_name}
result = self._safe_request("POST", self.BASE_URL, data=data)
if not result or "error" in result:
return {"source": "threatfox", "found": False}
if result.get("query_status") == "ok" and result.get("data"):
return {
"source": "threatfox",
"found": True,
"malware": malware_name,
"ioc_count": len(result["data"]),
"sample_iocs": result["data"][:limit]
}
return {"source": "threatfox", "found": False}
class URLhaus(ThreatIntelClient):
"""URLhaus API client (abuse.ch) - Auth-Key optional but recommended."""
BASE_URL = "https://urlhaus-api.abuse.ch/v1/"
def __init__(self, auth_key: Optional[str] = None):
super().__init__()
self.auth_key = auth_key
if self.auth_key:
self.session.headers.update({"Auth-Key": self.auth_key})
def query_url(self, url: str) -> dict:
"""Query URLhaus for a URL."""
data = {"url": url}
result = self._safe_request("POST", f"{self.BASE_URL}url/", data=data)
if not result or "error" in result:
return {"source": "urlhaus", "found": False, "error": result.get("error") if result else "request_failed"}
if result.get("query_status") == "no_results":
return {"source": "urlhaus", "found": False}
if result.get("query_status") == "ok":
return {
"source": "urlhaus",
"found": True,
"url": result.get("url"),
"url_status": result.get("url_status"), # online/offline
"threat": result.get("threat"),
"tags": result.get("tags", []),
"host": result.get("host"),
"date_added": result.get("date_added"),
"last_online": result.get("last_online"),
"takedown_time_seconds": result.get("takedown_time_seconds"),
"payloads": result.get("payloads", []),
}
return {"source": "urlhaus", "found": False}
def query_host(self, host: str) -> dict:
"""Query URLhaus for a domain or IP."""
data = {"host": host}
result = self._safe_request("POST", f"{self.BASE_URL}host/", data=data)
if not result or "error" in result:
return {"source": "urlhaus", "found": False, "error": result.get("error") if result else "request_failed"}
if result.get("query_status") == "no_results":
return {"source": "urlhaus", "found": False}
if result.get("query_status") == "ok":
return {
"source": "urlhaus",
"found": True,
"host": result.get("host"),
"firstseen": result.get("firstseen"),
"url_count": result.get("url_count"),
"urls": result.get("urls", [])[:10], # Limit URLs returned
}
return {"source": "urlhaus", "found": False}
def query_hash(self, hash_value: str) -> dict:
"""Query URLhaus for a payload hash."""
hash_type = "sha256_hash" if len(hash_value) == 64 else "md5_hash"
data = {hash_type: hash_value}
result = self._safe_request("POST", f"{self.BASE_URL}payload/", data=data)
if not result or "error" in result:
return {"source": "urlhaus", "found": False}
if result.get("query_status") == "no_results":
return {"source": "urlhaus", "found": False}
if result.get("query_status") == "ok":
return {
"source": "urlhaus",
"found": True,
"md5": result.get("md5_hash"),
"sha256": result.get("sha256_hash"),
"file_type": result.get("file_type"),
"file_size": result.get("file_size"),
"signature": result.get("signature"),
"firstseen": result.get("firstseen"),
"lastseen": result.get("lastseen"),
"download_count": result.get("url_count"),
"urls": result.get("urls", [])[:10],
}
return {"source": "urlhaus", "found": False}
class VirusTotal(ThreatIntelClient):
"""VirusTotal API client - Requires API key."""
BASE_URL = "https://www.virustotal.com/api/v3/"
def __init__(self, api_key: Optional[str] = None):
super().__init__(api_key)
if self.api_key:
self.session.headers.update({"x-apikey": self.api_key})
def _is_configured(self) -> bool:
return bool(self.api_key)
def query_hash(self, hash_value: str) -> dict:
"""Query VirusTotal for a file hash."""
if not self._is_configured():
return {"source": "virustotal", "found": False, "error": "no_api_key"}
result = self._safe_request("GET", f"{self.BASE_URL}files/{hash_value}")
if not result or "error" in result:
error = result.get("error") if result else "request_failed"
if error == "not_found":
return {"source": "virustotal", "found": False}
return {"source": "virustotal", "found": False, "error": error}
if "data" in result:
attrs = result["data"].get("attributes", {})
stats = attrs.get("last_analysis_stats", {})
return {
"source": "virustotal",
"found": True,
"sha256": attrs.get("sha256"),
"sha1": attrs.get("sha1"),
"md5": attrs.get("md5"),
"file_name": attrs.get("meaningful_name") or attrs.get("names", ["unknown"])[0] if attrs.get("names") else "unknown",
"file_type": attrs.get("type_description"),
"file_size": attrs.get("size"),
"magic": attrs.get("magic"),
"detections": {
"malicious": stats.get("malicious", 0),
"suspicious": stats.get("suspicious", 0),
"undetected": stats.get("undetected", 0),
"total": sum(stats.values()) if stats else 0,
},
"detection_rate": f"{stats.get('malicious', 0)}/{sum(stats.values())}" if stats else "0/0",
"popular_threat_names": attrs.get("popular_threat_classification", {}).get("suggested_threat_label"),
"tags": attrs.get("tags", []),
"first_submission": attrs.get("first_submission_date"),
"last_analysis": attrs.get("last_analysis_date"),
"reputation": attrs.get("reputation"),
"signature_info": attrs.get("signature_info"),
}
return {"source": "virustotal", "found": False}
def query_ip(self, ip: str) -> dict:
"""Query VirusTotal for an IP address."""
if not self._is_configured():
return {"source": "virustotal", "found": False, "error": "no_api_key"}
result = self._safe_request("GET", f"{self.BASE_URL}ip_addresses/{ip}")
if not result or "error" in result:
return {"source": "virustotal", "found": False, "error": result.get("error") if result else "request_failed"}
if "data" in result:
attrs = result["data"].get("attributes", {})
stats = attrs.get("last_analysis_stats", {})
return {
"source": "virustotal",
"found": True,
"ip": ip,
"asn": attrs.get("asn"),
"as_owner": attrs.get("as_owner"),
"country": attrs.get("country"),
"reputation": attrs.get("reputation"),
"detections": {
"malicious": stats.get("malicious", 0),
"suspicious": stats.get("suspicious", 0),
"harmless": stats.get("harmless", 0),
},
"tags": attrs.get("tags", []),
"last_analysis": attrs.get("last_analysis_date"),
}
return {"source": "virustotal", "found": False}
def query_domain(self, domain: str) -> dict:
"""Query VirusTotal for a domain."""
if not self._is_configured():
return {"source": "virustotal", "found": False, "error": "no_api_key"}
result = self._safe_request("GET", f"{self.BASE_URL}domains/{domain}")
if not result or "error" in result:
return {"source": "virustotal", "found": False, "error": result.get("error") if result else "request_failed"}
if "data" in result:
attrs = result["data"].get("attributes", {})
stats = attrs.get("last_analysis_stats", {})
return {
"source": "virustotal",
"found": True,
"domain": domain,
"registrar": attrs.get("registrar"),
"creation_date": attrs.get("creation_date"),
"reputation": attrs.get("reputation"),
"detections": {
"malicious": stats.get("malicious", 0),
"suspicious": stats.get("suspicious", 0),
"harmless": stats.get("harmless", 0),
},
"categories": attrs.get("categories", {}),
"tags": attrs.get("tags", []),
"last_analysis": attrs.get("last_analysis_date"),
}
return {"source": "virustotal", "found": False}
class AbuseIPDB(ThreatIntelClient):
"""AbuseIPDB API client - Requires API key."""
BASE_URL = "https://api.abuseipdb.com/api/v2/"
def __init__(self, api_key: Optional[str] = None):
super().__init__(api_key)
if self.api_key:
self.session.headers.update({"Key": self.api_key, "Accept": "application/json"})
def _is_configured(self) -> bool:
return bool(self.api_key)
def query_ip(self, ip: str) -> dict:
"""Query AbuseIPDB for an IP address."""
if not self._is_configured():
return {"source": "abuseipdb", "found": False, "error": "no_api_key"}
params = {"ipAddress": ip, "maxAgeInDays": 90, "verbose": True}
result = self._safe_request("GET", f"{self.BASE_URL}check", params=params)
if not result or "error" in result:
return {"source": "abuseipdb", "found": False, "error": result.get("error") if result else "request_failed"}
if "data" in result:
data = result["data"]
return {
"source": "abuseipdb",
"found": True,
"ip": data.get("ipAddress"),
"is_public": data.get("isPublic"),
"abuse_confidence_score": data.get("abuseConfidenceScore"),
"country": data.get("countryCode"),
"isp": data.get("isp"),
"domain": data.get("domain"),
"usage_type": data.get("usageType"),
"is_tor": data.get("isTor"),
"is_whitelisted": data.get("isWhitelisted"),
"total_reports": data.get("totalReports"),
"num_distinct_users": data.get("numDistinctUsers"),
"last_reported": data.get("lastReportedAt"),
}
return {"source": "abuseipdb", "found": False}
# IOC type detection
def detect_ioc_type(ioc: str) -> str:
"""Detect the type of IOC."""
ioc = ioc.strip()
# Hash detection
if re.match(r'^[a-fA-F0-9]{32}$', ioc):
return "md5"
if re.match(r'^[a-fA-F0-9]{40}$', ioc):
return "sha1"
if re.match(r'^[a-fA-F0-9]{64}$', ioc):
return "sha256"
# URL detection
if re.match(r'^https?://', ioc, re.IGNORECASE):
return "url"
# IP detection
if re.match(r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', ioc):
return "ipv4"
# Domain detection (simple)
if re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$', ioc):
return "domain"
return "unknown"
def calculate_file_hashes(filepath: str) -> dict:
"""Calculate hashes for a file."""
path = Path(filepath)
if not path.exists():
return {"error": f"File not found: {filepath}"}
data = path.read_bytes()
return {
"md5": hashlib.md5(data).hexdigest(),
"sha1": hashlib.sha1(data).hexdigest(),
"sha256": hashlib.sha256(data).hexdigest(),
"size": len(data),
}
class MalwareTriage:
"""Main triage orchestrator."""
def __init__(self, vt_key: Optional[str] = None, abuseipdb_key: Optional[str] = None,
abusech_key: Optional[str] = None, verbose: bool = True):
# Get API keys from multiple sources
vt_api_key = get_api_key("VIRUSTOTAL_API_KEY", vt_key)
abuseipdb_api_key = get_api_key("ABUSEIPDB_API_KEY", abuseipdb_key)
abusech_auth_key = get_api_key("ABUSECH_AUTH_KEY", abusech_key)
if verbose:
if abusech_auth_key:
print(f"[*] abuse.ch Auth-Key loaded (ends with ...{abusech_auth_key[-4:]})", file=sys.stderr)
else:
print("[!] abuse.ch Auth-Key NOT found (MalwareBazaar/ThreatFox will fail)", file=sys.stderr)
if vt_api_key:
print(f"[*] VirusTotal API key loaded (ends with ...{vt_api_key[-4:]})", file=sys.stderr)
else:
print("[!] VirusTotal API key NOT found", file=sys.stderr)
if abuseipdb_api_key:
print(f"[*] AbuseIPDB API key loaded (ends with ...{abuseipdb_api_key[-4:]})", file=sys.stderr)
else:
print("[!] AbuseIPDB API key NOT found", file=sys.stderr)
# Initialize clients with auth keys
self.malwarebazaar = MalwareBazaar(abusech_auth_key)
self.threatfox = ThreatFox(abusech_auth_key)
self.urlhaus = URLhaus(abusech_auth_key)
self.virustotal = VirusTotal(vt_api_key)
self.abuseipdb = AbuseIPDB(abuseipdb_api_key)
self.api_status = {
"malwarebazaar": bool(abusech_auth_key),
"threatfox": bool(abusech_auth_key),
"urlhaus": True, # Works without key but better with
"virustotal": bool(vt_api_key),
"abuseipdb": bool(abuseipdb_api_key),
}
def triage_hash(self, hash_value: str) -> dict:
"""Triage a file hash across all sources."""
results = {
"query": hash_value,
"type": detect_ioc_type(hash_value),
"timestamp": datetime.now(timezone.utc).isoformat(),
"sources": [],
"verdict": "unknown",
"summary": {},
}
# Query all sources
sources_to_query = [
("malwarebazaar", self.malwarebazaar.query_hash),
("urlhaus", self.urlhaus.query_hash),
("threatfox", self.threatfox.query_ioc),
("virustotal", self.virustotal.query_hash),
]
for source_name, query_func in sources_to_query:
try:
result = query_func(hash_value)
results["sources"].append(result)
time.sleep(0.5) # Rate limiting
except Exception as e:
results["sources"].append({"source": source_name, "found": False, "error": str(e)})
# Aggregate verdict
results["verdict"], results["summary"] = self._aggregate_hash_verdict(results["sources"])
return results
def triage_ip(self, ip: str) -> dict:
"""Triage an IP address across all sources."""
results = {
"query": ip,
"type": "ipv4",
"timestamp": datetime.now(timezone.utc).isoformat(),
"sources": [],
"verdict": "unknown",
"summary": {},
}
# Query all sources
sources_to_query = [
("urlhaus", self.urlhaus.query_host),
("threatfox", self.threatfox.query_ioc),
("virustotal", self.virustotal.query_ip),
("abuseipdb", self.abuseipdb.query_ip),
]
for source_name, query_func in sources_to_query:
try:
result = query_func(ip)
results["sources"].append(result)
time.sleep(0.5)
except Exception as e:
results["sources"].append({"source": source_name, "found": False, "error": str(e)})
results["verdict"], results["summary"] = self._aggregate_ip_verdict(results["sources"])
return results
def triage_domain(self, domain: str) -> dict:
"""Triage a domain across all sources."""
results = {
"query": domain,
"type": "domain",
"timestamp": datetime.now(timezone.utc).isoformat(),
"sources": [],
"verdict": "unknown",
"summary": {},
}
sources_to_query = [
("urlhaus", self.urlhaus.query_host),
("threatfox", self.threatfox.query_ioc),
("virustotal", self.virustotal.query_domain),
]
for source_name, query_func in sources_to_query:
try:
result = query_func(domain)
results["sources"].append(result)
time.sleep(0.5)
except Exception as e:
results["sources"].append({"source": source_name, "found": False, "error": str(e)})
results["verdict"], results["summary"] = self._aggregate_domain_verdict(results["sources"])
return results
def triage_url(self, url: str) -> dict:
"""Triage a URL across all sources."""
results = {
"query": url,
"type": "url",
"timestamp": datetime.now(timezone.utc).isoformat(),
"sources": [],
"verdict": "unknown",
"summary": {},
}
sources_to_query = [
("urlhaus", self.urlhaus.query_url),
("threatfox", self.threatfox.query_ioc),
]
for source_name, query_func in sources_to_query:
try:
result = query_func(url)
results["sources"].append(result)
time.sleep(0.5)
except Exception as e:
results["sources"].append({"source": source_name, "found": False, "error": str(e)})
results["verdict"], results["summary"] = self._aggregate_url_verdict(results["sources"])
return results
def triage_file(self, filepath: str) -> dict:
"""Triage a file by calculating hashes and querying."""
hashes = calculate_file_hashes(filepath)
if "error" in hashes:
return {"error": hashes["error"]}
results = self.triage_hash(hashes["sha256"])
results["file_info"] = {
"path": filepath,
"hashes": hashes,
}
return results
def triage_auto(self, ioc: str) -> dict:
"""Auto-detect IOC type and triage."""
ioc_type = detect_ioc_type(ioc)
if ioc_type in ["md5", "sha1", "sha256"]:
return self.triage_hash(ioc)
elif ioc_type == "ipv4":
return self.triage_ip(ioc)
elif ioc_type == "domain":
return self.triage_domain(ioc)
elif ioc_type == "url":
return self.triage_url(ioc)
else:
return {"error": f"Unknown IOC type: {ioc}", "query": ioc}
def _aggregate_hash_verdict(self, sources: list) -> tuple:
"""Aggregate verdict from hash query results."""
malware_names = set()
tags = set()
found_in = []
vt_detection = None
for source in sources:
if source.get("found"):
found_in.append(source["source"])
# Extract malware family names
if source.get("signature"):
malware_names.add(source["signature"])
if source.get("popular_threat_names"):
malware_names.add(source["popular_threat_names"])
if source.get("malware"):
malware_names.add(source["malware"])
# Extract tags
if source.get("tags"):
tags.update(source["tags"])
# Get VT detection rate
if source["source"] == "virustotal" and source.get("detections"):
vt_detection = source["detections"]
# Determine verdict
if not found_in:
verdict = "clean_or_unknown"
elif vt_detection and vt_detection.get("malicious", 0) > 5:
verdict = "malicious"
elif found_in:
verdict = "suspicious"
else:
verdict = "unknown"
summary = {
"found_in_sources": found_in,
"malware_families": list(malware_names),
"tags": list(tags),
}
if vt_detection:
summary["virustotal_detection"] = f"{vt_detection.get('malicious', 0)}/{vt_detection.get('total', 0)}"
return verdict, summary
def _aggregate_ip_verdict(self, sources: list) -> tuple:
"""Aggregate verdict from IP query results."""
found_in = []
abuse_score = None
vt_malicious = 0
url_count = 0
for source in sources:
if source.get("found"):
found_in.append(source["source"])
if source["source"] == "abuseipdb":
abuse_score = source.get("abuse_confidence_score")
if source["source"] == "virustotal" and source.get("detections"):
vt_malicious = source["detections"].get("malicious", 0)
if source["source"] == "urlhaus":
url_count = source.get("url_count", 0)
# Determine verdict
if abuse_score and abuse_score > 50:
verdict = "malicious"
elif vt_malicious > 3:
verdict = "malicious"
elif url_count > 0 or found_in:
verdict = "suspicious"
else:
verdict = "clean_or_unknown"
summary = {
"found_in_sources": found_in,
}
if abuse_score is not None:
summary["abuse_confidence_score"] = abuse_score
if vt_malicious:
summary["virustotal_malicious"] = vt_malicious
if url_count:
summary["urlhaus_url_count"] = url_count
return verdict, summary
def _aggregate_domain_verdict(self, sources: list) -> tuple:
"""Aggregate verdict from domain query results."""
found_in = []
vt_malicious = 0
url_count = 0
for source in sources:
if source.get("found"):
found_in.append(source["source"])
if source["source"] == "virustotal" and source.get("detections"):
vt_malicious = source["detections"].get("malicious", 0)
if source["source"] == "urlhaus":
url_count = source.get("url_count", 0)
if vt_malicious > 3:
verdict = "malicious"
elif url_count > 0 or found_in:
verdict = "suspicious"
else:
verdict = "clean_or_unknown"
return verdict, {"found_in_sources": found_in, "virustotal_malicious": vt_malicious, "urlhaus_url_count": url_count}
def _aggregate_url_verdict(self, sources: list) -> tuple:
"""Aggregate verdict from URL query results."""
found_in = []
threat_type = None
for source in sources:
if source.get("found"):
found_in.append(source["source"])
if source.get("threat"):
threat_type = source["threat"]
if found_in:
verdict = "malicious"
else:
verdict = "clean_or_unknown"
return verdict, {"found_in_sources": found_in, "threat_type": threat_type}
def format_triage_report(results: dict, format_type: str = "text") -> str:
"""Format triage results for output."""
if format_type == "json":
return json.dumps(results, indent=2, default=str)
# Text format
lines = []
lines.append("=" * 70)
lines.append("MALWARE TRIAGE REPORT")
lines.append("=" * 70)
lines.append(f"\n[QUERY]")
lines.append(f" IOC: {results.get('query', 'N/A')}")
lines.append(f" Type: {results.get('type', 'N/A')}")
lines.append(f" Timestamp: {results.get('timestamp', 'N/A')}")
# File info if present
if results.get("file_info"):
fi = results["file_info"]
lines.append(f"\n[FILE INFO]")
lines.append(f" Path: {fi['path']}")
lines.append(f" MD5: {fi['hashes'].get('md5')}")
lines.append(f" SHA1: {fi['hashes'].get('sha1')}")
lines.append(f" SHA256: {fi['hashes'].get('sha256')}")
lines.append(f" Size: {fi['hashes'].get('size'):,} bytes")
# Verdict
verdict = results.get("verdict", "unknown")
verdict_emoji = {"malicious": "🔴", "suspicious": "🟡", "clean_or_unknown": "🟢", "unknown": "⚪"}.get(verdict, "⚪")
lines.append(f"\n[VERDICT] {verdict_emoji} {verdict.upper()}")
# Summary
summary = results.get("summary", {})
if summary:
lines.append(f"\n[SUMMARY]")
if summary.get("found_in_sources"):
lines.append(f" Found in: {', '.join(summary['found_in_sources'])}")
if summary.get("malware_families"):
lines.append(f" Malware: {', '.join(summary['malware_families'])}")
if summary.get("virustotal_detection"):
lines.append(f" VT Score: {summary['virustotal_detection']}")
if summary.get("abuse_confidence_score") is not None:
lines.append(f" AbuseIPDB Score: {summary['abuse_confidence_score']}%")
if summary.get("tags"):
lines.append(f" Tags: {', '.join(summary['tags'][:10])}")
# Source details
lines.append(f"\n[SOURCE DETAILS]")
for source in results.get("sources", []):
source_name = source.get("source", "unknown").upper()
found = "✓" if source.get("found") else "✗"
lines.append(f"\n [{source_name}] {found}")
if source.get("error"):
lines.append(f" Error: {source['error']}")
continue
if not source.get("found"):
lines.append(f" Not found in database")
continue
# Source-specific details
if source.get("signature"):
lines.append(f" Malware: {source['signature']}")
if source.get("popular_threat_names"):
lines.append(f" Threat: {source['popular_threat_names']}")
if source.get("detection_rate"):
lines.append(f" Detection: {source['detection_rate']}")
if source.get("first_seen"):
lines.append(f" First seen: {source['first_seen']}")
if source.get("abuse_confidence_score") is not None:
lines.append(f" Abuse score: {source['abuse_confidence_score']}%")
if source.get("total_reports"):
lines.append(f" Reports: {source['total_reports']}")
if source.get("url_count"):
lines.append(f" Malicious URLs: {source['url_count']}")
if source.get("tags"):
lines.append(f" Tags: {', '.join(source['tags'][:5])}")
lines.append("\n" + "=" * 70)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Malware triage - Multi-source threat intelligence lookup",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s 44d88612fea8a8f36de82e1278abb02f # MD5 hash
%(prog)s -t hash e3b0c44298fc1c149afbf4c8996fb924... # SHA256 hash
%(prog)s -t ip 45.33.32.156 # IP address
%(prog)s -t domain evil.com # Domain
%(prog)s -t url http://evil.com/malware.exe # URL
%(prog)s -t file /path/to/sample.exe # File
%(prog)s --batch iocs.txt # Batch mode
API Key Configuration (in priority order):
1. Command line: --abusech-key, --vt-key, --abuseipdb-key
2. Environment variables: ABUSECH_AUTH_KEY, VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
3. Config file: ~/.config/malware-triage/config.json
Create config file:
%(prog)s --init-config
Get your API keys from:
- abuse.ch (MalwareBazaar/ThreatFox): https://auth.abuse.ch/
- VirusTotal: https://www.virustotal.com/gui/my-apikey
- AbuseIPDB: https://www.abuseipdb.com/account/api
"""
)
parser.add_argument("ioc", nargs="?", help="IOC to triage (hash, IP, domain, URL)")
parser.add_argument("-t", "--type", choices=["hash", "ip", "domain", "url", "file", "auto"],
default="auto", help="IOC type (default: auto-detect)")
parser.add_argument("-f", "--format", choices=["text", "json"], default="text",
help="Output format (default: text)")
parser.add_argument("-o", "--output", help="Output file")
parser.add_argument("--batch", help="File containing IOCs (one per line)")
parser.add_argument("--status", action="store_true", help="Show API configuration status")
parser.add_argument("--init-config", action="store_true", help="Create default config file")
parser.add_argument("--abusech-key", help="abuse.ch Auth-Key (for MalwareBazaar/ThreatFox)")
parser.add_argument("--vt-key", help="VirusTotal API key")
parser.add_argument("--abuseipdb-key", help="AbuseIPDB API key")
args = parser.parse_args()
if args.init_config:
create_default_config()
return
triage = MalwareTriage(
vt_key=args.vt_key,
abuseipdb_key=args.abuseipdb_key,
abusech_key=args.abusech_key
)
if args.status:
print("API Configuration Status:")
print("-" * 40)
for api, configured in triage.api_status.items():
status = "✓ Configured" if configured else "✗ Not configured"
print(f" {api:<15} {status}")
# Show config file location
print()
print("Config file locations (in priority order):")
config_paths = [
Path.home() / ".config" / "malware-triage" / "config.json",
Path.home() / ".malware-triage.json",
Path("config.json"),
]
for p in config_paths:
exists = "✓" if p.exists() else "✗"
print(f" {exists} {p}")
print()
print("Run with --init-config to create a config file.")
return
if not args.ioc and not args.batch:
parser.print_help()
return
# Process IOCs
results_list = []
if args.batch:
iocs = Path(args.batch).read_text().strip().split("\n")
for ioc in iocs:
ioc = ioc.strip()
if not ioc or ioc.startswith("#"):
continue
print(f"Triaging: {ioc}...", file=sys.stderr)
result = triage.triage_auto(ioc)
results_list.append(result)
else:
if args.type == "file":
result = triage.triage_file(args.ioc)
elif args.type == "hash":
result = triage.triage_hash(args.ioc)
elif args.type == "ip":
result = triage.triage_ip(args.ioc)
elif args.type == "domain":
result = triage.triage_domain(args.ioc)
elif args.type == "url":
result = triage.triage_url(args.ioc)
else:
result = triage.triage_auto(args.ioc)
results_list.append(result)
# Format output
if args.batch and args.format == "json":
output = json.dumps(results_list, indent=2, default=str)
elif args.batch:
output = "\n\n".join(format_triage_report(r, args.format) for r in results_list)
else:
output = format_triage_report(results_list[0], args.format)
if args.output:
Path(args.output).write_text(output)
print(f"Results written to {args.output}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()