diff --git a/skills/analyzing-threat-actor-ttps-with-mitre-navigator/LICENSE b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/LICENSE new file mode 100644 index 00000000..09d37ad6 --- /dev/null +++ b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/analyzing-threat-actor-ttps-with-mitre-navigator/SKILL.md b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/SKILL.md new file mode 100644 index 00000000..abe646b3 --- /dev/null +++ b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/SKILL.md @@ -0,0 +1,51 @@ +--- +name: analyzing-threat-actor-ttps-with-mitre-navigator +description: > + Map advanced persistent threat (APT) group tactics, techniques, and procedures (TTPs) to + the MITRE ATT&CK framework using the ATT&CK Navigator and attackcti Python library. The + analyst queries STIX/TAXII data for group-technique associations, generates Navigator layer + files for visualization, and compares defensive coverage against adversary profiles. + Activates for requests involving APT TTP mapping, ATT&CK Navigator layers, threat actor + profiling, or MITRE technique coverage analysis. +domain: cybersecurity +subdomain: threat-intelligence +tags: [mitre-attack, navigator, threat-intelligence, apt, ttp-mapping, stix, attackcti] +version: "1.0" +author: mahipal +license: Apache-2.0 +--- +# Analyzing Threat Actor TTPs with MITRE Navigator + +## Overview + +The MITRE ATT&CK Navigator is a web application for annotating and visualizing ATT&CK matrices. +Combined with the attackcti Python library (which queries ATT&CK STIX data via TAXII), analysts +can programmatically generate Navigator layer files mapping specific threat group TTPs, compare +multiple groups, and assess detection coverage gaps against known adversaries. + +## Prerequisites + +- Python 3.8+ with attackcti and stix2 libraries installed +- MITRE ATT&CK Navigator (web UI or local instance) +- Understanding of STIX 2.1 objects and relationships + +## Steps + +1. Query ATT&CK STIX data for target threat group using attackcti +2. Extract techniques associated with the group via STIX relationships +3. Generate ATT&CK Navigator layer JSON with technique annotations +4. Overlay detection coverage to identify gaps +5. Export layer for team review and defensive planning + +## Expected Output + +```json +{ + "name": "APT29 TTPs", + "domain": "enterprise-attack", + "techniques": [ + {"techniqueID": "T1566.001", "score": 1, "comment": "Spearphishing Attachment"}, + {"techniqueID": "T1059.001", "score": 1, "comment": "PowerShell"} + ] +} +``` diff --git a/skills/analyzing-threat-actor-ttps-with-mitre-navigator/references/api-reference.md b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/references/api-reference.md new file mode 100644 index 00000000..03ac762a --- /dev/null +++ b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/references/api-reference.md @@ -0,0 +1,84 @@ +# Analyzing Threat Actor TTPs with MITRE Navigator — API Reference + +## attackcti Python Library + +| Method | Description | +|--------|-------------| +| `attack_client()` | Initialize STIX/TAXII client for ATT&CK data | +| `client.get_groups()` | Retrieve all threat groups from ATT&CK | +| `client.get_techniques()` | Retrieve all techniques from ATT&CK | +| `client.get_techniques_used_by_group(group)` | Get techniques linked to a specific group | +| `client.get_software()` | Retrieve all software/tools from ATT&CK | +| `client.get_software_used_by_group(group)` | Get software used by a specific group | +| `client.get_mitigations()` | Retrieve all mitigations from ATT&CK | +| `client.get_data_sources()` | Retrieve all data sources from ATT&CK | + +## STIX 2.1 Group Object Fields + +| Field | Description | +|-------|-------------| +| `id` | STIX object ID (e.g., `intrusion-set--abc123`) | +| `name` | Group name (e.g., APT29) | +| `aliases` | Alternative names for the group | +| `description` | Group description and background | +| `external_references` | List of references including ATT&CK ID | +| `created` | Object creation timestamp | +| `modified` | Last modification timestamp | + +## STIX 2.1 Technique Object Fields + +| Field | Description | +|-------|-------------| +| `name` | Technique name (e.g., Spearphishing Attachment) | +| `external_references[].external_id` | ATT&CK technique ID (e.g., T1566.001) | +| `x_mitre_platforms` | Target platforms (Windows, Linux, macOS) | +| `kill_chain_phases` | Associated tactics in the kill chain | +| `x_mitre_detection` | Detection guidance for the technique | +| `x_mitre_is_subtechnique` | Whether this is a sub-technique | + +## ATT&CK Navigator Layer JSON Schema + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Layer display name | +| `versions.attack` | string | ATT&CK version (e.g., "15") | +| `versions.navigator` | string | Navigator version (e.g., "5.0") | +| `versions.layer` | string | Layer format version (e.g., "4.5") | +| `domain` | string | `enterprise-attack`, `mobile-attack`, or `ics-attack` | +| `techniques[].techniqueID` | string | ATT&CK technique ID | +| `techniques[].score` | integer | Numeric score for coloring (0-100) | +| `techniques[].color` | string | Hex color override (e.g., `#ff6666`) | +| `techniques[].comment` | string | Annotation text for the technique | +| `techniques[].enabled` | boolean | Whether technique cell is enabled | +| `gradient.colors` | array | Color gradient from min to max score | +| `gradient.minValue` | integer | Minimum score value | +| `gradient.maxValue` | integer | Maximum score value | +| `filters.platforms` | array | Platforms to display in the matrix | +| `legendItems[].label` | string | Legend entry label | +| `legendItems[].color` | string | Legend entry color | + +## CLI Usage + +```bash +# List all ATT&CK threat groups +python agent.py --list-groups + +# Analyze a specific group +python agent.py --group "APT29" + +# Generate Navigator layer file +python agent.py --group "APT29" --layer-output apt29_layer.json + +# Compare multiple groups +python agent.py --compare "APT29" "APT28" "Lazarus Group" + +# Save full report as JSON +python agent.py --group "APT29" --layer-output apt29.json --output report.json +``` + +## External References + +- [ATT&CK Navigator GitHub](https://github.com/mitre-attack/attack-navigator) +- [attackcti Documentation](https://attackcti.readthedocs.io/) +- [MITRE ATT&CK Groups](https://attack.mitre.org/groups/) +- [STIX 2.1 Specification](https://docs.oasis-open.org/cti/stix/v2.1/stix-v2.1.html) diff --git a/skills/analyzing-threat-actor-ttps-with-mitre-navigator/scripts/agent.py b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/scripts/agent.py new file mode 100644 index 00000000..ae13330b --- /dev/null +++ b/skills/analyzing-threat-actor-ttps-with-mitre-navigator/scripts/agent.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""MITRE ATT&CK Navigator layer generation and threat actor TTP mapping agent.""" + +import json +import sys +import argparse +from datetime import datetime + +try: + from attackcti import attack_client +except ImportError: + print("Install: pip install attackcti") + sys.exit(1) + + +def get_attack_client(): + """Initialize ATT&CK STIX/TAXII client.""" + return attack_client() + + +def list_threat_groups(client): + """List all threat groups in ATT&CK.""" + groups = client.get_groups() + results = [] + for g in groups: + aliases = g.get("aliases", []) + results.append({ + "name": g.get("name", ""), + "id": g.get("external_references", [{}])[0].get("external_id", "") + if g.get("external_references") else "", + "aliases": aliases, + "description": g.get("description", "")[:200], + }) + return sorted(results, key=lambda x: x["name"]) + + +def get_group_techniques(client, group_name): + """Get all techniques used by a specific threat group.""" + groups = client.get_groups() + target_group = None + for g in groups: + if g.get("name", "").lower() == group_name.lower(): + target_group = g + break + aliases = [a.lower() for a in g.get("aliases", [])] + if group_name.lower() in aliases: + target_group = g + break + + if not target_group: + return {"error": f"Group '{group_name}' not found"} + + group_stix_id = target_group["id"] + techniques = client.get_techniques_used_by_group(target_group) + + results = [] + for tech in techniques: + ext_refs = tech.get("external_references", []) + tech_id = "" + url = "" + for ref in ext_refs: + if ref.get("source_name") == "mitre-attack": + tech_id = ref.get("external_id", "") + url = ref.get("url", "") + break + results.append({ + "technique_id": tech_id, + "name": tech.get("name", ""), + "description": tech.get("description", "")[:150], + "url": url, + "platforms": tech.get("x_mitre_platforms", []), + }) + + return { + "group_name": target_group.get("name", ""), + "group_id": target_group.get("external_references", [{}])[0].get("external_id", ""), + "technique_count": len(results), + "techniques": results, + } + + +def generate_navigator_layer(group_data, color="#ff6666"): + """Generate ATT&CK Navigator layer JSON from group technique data.""" + techniques = [] + for tech in group_data.get("techniques", []): + tid = tech.get("technique_id", "") + if not tid: + continue + techniques.append({ + "techniqueID": tid, + "score": 1, + "color": color, + "comment": tech.get("name", ""), + "enabled": True, + }) + + layer = { + "name": f"{group_data.get('group_name', 'Unknown')} TTPs", + "versions": { + "attack": "15", + "navigator": "5.0", + "layer": "4.5", + }, + "domain": "enterprise-attack", + "description": f"Techniques used by {group_data.get('group_name', '')} " + f"({group_data.get('group_id', '')})", + "filters": {"platforms": ["Windows", "Linux", "macOS", "Cloud"]}, + "sorting": 0, + "layout": {"layout": "side", "showID": True, "showName": True}, + "hideDisabled": False, + "techniques": techniques, + "gradient": { + "colors": ["#ffffff", color], + "minValue": 0, + "maxValue": 1, + }, + "legendItems": [ + {"label": f"{group_data.get('group_name', '')} techniques", "color": color}, + ], + "metadata": [], + "showTacticRowBackground": True, + "tacticRowBackground": "#dddddd", + } + return layer + + +def compare_groups(client, group_names): + """Compare techniques across multiple threat groups.""" + all_techniques = {} + group_techs = {} + for name in group_names: + data = get_group_techniques(client, name) + if "error" in data: + continue + techs = {t["technique_id"] for t in data.get("techniques", [])} + group_techs[data.get("group_name", name)] = techs + for t in data.get("techniques", []): + all_techniques[t["technique_id"]] = t["name"] + + shared = set.intersection(*group_techs.values()) if group_techs else set() + unique_per_group = {} + for name, techs in group_techs.items(): + unique_per_group[name] = techs - set.union(*(v for k, v in group_techs.items() if k != name)) + + return { + "groups_compared": list(group_techs.keys()), + "total_unique_techniques": len(set.union(*group_techs.values())) if group_techs else 0, + "shared_techniques": [{"id": t, "name": all_techniques.get(t, "")} for t in shared], + "shared_count": len(shared), + "unique_per_group": {k: len(v) for k, v in unique_per_group.items()}, + } + + +def run_audit(args): + """Execute threat actor TTP mapping audit.""" + print(f"\n{'='*60}") + print(f" MITRE ATT&CK THREAT ACTOR TTP ANALYSIS") + print(f" Generated: {datetime.utcnow().isoformat()} UTC") + print(f"{'='*60}\n") + + client = get_attack_client() + report = {} + + if args.list_groups: + groups = list_threat_groups(client) + report["groups"] = groups + print(f"--- THREAT GROUPS ({len(groups)}) ---") + for g in groups[:30]: + print(f" {g['id']}: {g['name']}") + + if args.group: + data = get_group_techniques(client, args.group) + report["group_techniques"] = data + print(f"--- {data.get('group_name','')} ({data.get('group_id','')}) ---") + print(f" Techniques: {data.get('technique_count', 0)}") + for t in data.get("techniques", [])[:20]: + print(f" {t['technique_id']}: {t['name']}") + + if args.layer_output: + layer = generate_navigator_layer(data) + with open(args.layer_output, "w") as f: + json.dump(layer, f, indent=2) + report["layer_file"] = args.layer_output + print(f"\n Navigator layer saved to {args.layer_output}") + + if args.compare: + comparison = compare_groups(client, args.compare) + report["comparison"] = comparison + print(f"\n--- GROUP COMPARISON ---") + print(f" Groups: {comparison['groups_compared']}") + print(f" Total unique techniques: {comparison['total_unique_techniques']}") + print(f" Shared: {comparison['shared_count']}") + for t in comparison["shared_techniques"][:10]: + print(f" {t['id']}: {t['name']}") + + return report + + +def main(): + parser = argparse.ArgumentParser(description="MITRE ATT&CK TTP Mapping Agent") + parser.add_argument("--group", help="Threat group name to analyze (e.g., APT29)") + parser.add_argument("--list-groups", action="store_true", help="List all ATT&CK groups") + parser.add_argument("--compare", nargs="+", help="Compare multiple groups") + parser.add_argument("--layer-output", help="Save Navigator layer JSON to file") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + report = run_audit(args) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/detecting-malicious-scheduled-tasks-with-sysmon/LICENSE b/skills/detecting-malicious-scheduled-tasks-with-sysmon/LICENSE new file mode 100644 index 00000000..09d37ad6 --- /dev/null +++ b/skills/detecting-malicious-scheduled-tasks-with-sysmon/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/detecting-malicious-scheduled-tasks-with-sysmon/SKILL.md b/skills/detecting-malicious-scheduled-tasks-with-sysmon/SKILL.md new file mode 100644 index 00000000..3949b3ae --- /dev/null +++ b/skills/detecting-malicious-scheduled-tasks-with-sysmon/SKILL.md @@ -0,0 +1,53 @@ +--- +name: detecting-malicious-scheduled-tasks-with-sysmon +description: > + Detect malicious scheduled task creation and modification using Sysmon Event IDs 1 (Process + Create for schtasks.exe), 11 (File Create for task XML), and Windows Security Event 4698/4702. + The analyst correlates task creation with suspicious parent processes, public directory paths, + and encoded command arguments to identify persistence and lateral movement via scheduled tasks. + Activates for requests involving scheduled task detection, Sysmon persistence hunting, or + T1053.005 Scheduled Task/Job analysis. +domain: cybersecurity +subdomain: threat-hunting +tags: [sysmon, scheduled-tasks, persistence, detection, threat-hunting, windows-security] +version: "1.0" +author: mahipal +license: Apache-2.0 +--- +# Detecting Malicious Scheduled Tasks with Sysmon + +## Overview + +Adversaries abuse Windows Task Scheduler (schtasks.exe, at.exe) for persistence (T1053.005) +and lateral movement. Sysmon Event ID 1 captures schtasks.exe process creation with full +command-line arguments, while Event ID 11 captures task XML files written to +C:\Windows\System32\Tasks\. Windows Security Event 4698 logs task registration details. +This skill covers building detection rules that correlate these events to identify +malicious scheduled tasks created from suspicious paths, with encoded payloads, or +targeting remote systems. + +## Prerequisites + +- Sysmon installed with a detection-focused configuration (e.g., SwiftOnSecurity or Olaf Hartong) +- Windows Event Log forwarding to SIEM (Splunk, Elastic, or Sentinel) +- PowerShell ScriptBlock Logging enabled (Event 4104) + +## Steps + +1. Configure Sysmon to log Event IDs 1, 11, 12, 13 with task-related filters +2. Build detection rules for schtasks.exe /create with suspicious arguments +3. Correlate Event 4698 (task registered) with Sysmon Event 1 (process create) +4. Hunt for tasks executing from public directories or with encoded commands +5. Alert on remote task creation (schtasks /s) for lateral movement detection + +## Expected Output + +``` +[CRITICAL] Suspicious Scheduled Task Detected + Task: \Microsoft\Windows\UpdateCheck + Command: powershell.exe -enc SQBuAHYAbwBrAGUALQBXAGUAYgBSAGU... + Created By: DOMAIN\compromised_user + Parent Process: cmd.exe (PID 4532) + Source: \\192.168.1.50 (remote creation) + MITRE: T1053.005 - Scheduled Task/Job +``` diff --git a/skills/detecting-malicious-scheduled-tasks-with-sysmon/references/api-reference.md b/skills/detecting-malicious-scheduled-tasks-with-sysmon/references/api-reference.md new file mode 100644 index 00000000..7d80e45d --- /dev/null +++ b/skills/detecting-malicious-scheduled-tasks-with-sysmon/references/api-reference.md @@ -0,0 +1,64 @@ +# Detecting Malicious Scheduled Tasks with Sysmon — API Reference + +## Relevant Event IDs + +| Event ID | Source | Description | +|----------|--------|-------------| +| 1 | Sysmon | Process Create — captures schtasks.exe with full command line | +| 11 | Sysmon | File Create — task XML written to System32\Tasks | +| 12/13 | Sysmon | Registry Create/Set — task registry modifications | +| 4698 | Security | Scheduled task registered (includes task XML content) | +| 4702 | Security | Scheduled task updated | +| 4699 | Security | Scheduled task deleted | + +## schtasks.exe Suspicious Flags + +| Flag | Description | Detection Value | +|------|-------------|----------------| +| `/create` | Create new task | Baseline detection | +| `/s ` | Remote system target | Lateral movement indicator | +| `/ru SYSTEM` | Run as SYSTEM | Privilege escalation | +| `/sc onstart` | Run at system boot | Persistence | +| `/tr "powershell -enc"` | Encoded PowerShell payload | Obfuscation | +| `/tn \Microsoft\Windows\*` | Masquerade as Microsoft task | Evasion | + +## Splunk Detection Queries + +```spl +index=sysmon EventCode=1 Image="*\\schtasks.exe" CommandLine="*/create*" +| eval suspicious=if(match(CommandLine,"(?i)(\\\\users\\\\public|\\\\temp\\\\|\\-enc)"),"YES","NO") +| where suspicious="YES" +``` + +```spl +index=wineventlog EventCode=4698 +| spath input=TaskContent +| search Command="*powershell*" OR Command="*cmd.exe*" +``` + +## Sysmon Configuration (Task Monitoring) + +```xml + + + schtasks.exe + at.exe + + + \Windows\System32\Tasks\ + + +``` + +## MITRE ATT&CK + +| Technique | ID | Description | +|-----------|----|-------------| +| Scheduled Task/Job | T1053.005 | Create/modify scheduled tasks for persistence | +| Lateral Movement | T1021 | Remote task creation via schtasks /s | + +## External References + +- [Sysmon Configuration Guide](https://github.com/SwiftOnSecurity/sysmon-config) +- [Splunk Scheduled Task Detection](https://research.splunk.com/endpoint/7feb7972-7ac3-11eb-bac8-acde48001122/) +- [Red Canary: Scheduled Task](https://redcanary.com/threat-detection-report/techniques/scheduled-task/) diff --git a/skills/detecting-malicious-scheduled-tasks-with-sysmon/scripts/agent.py b/skills/detecting-malicious-scheduled-tasks-with-sysmon/scripts/agent.py new file mode 100644 index 00000000..a06d1339 --- /dev/null +++ b/skills/detecting-malicious-scheduled-tasks-with-sysmon/scripts/agent.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Sysmon scheduled task detection agent for hunting malicious persistence.""" + +import json +import sys +import argparse +import re +import base64 +import xml.etree.ElementTree as ET +from datetime import datetime +from collections import defaultdict + + +SUSPICIOUS_PATHS = [ + r"\\users\\public\\", r"\\programdata\\", r"\\windows\\temp\\", + r"\\appdata\\local\\temp\\", r"\\downloads\\", r"\\desktop\\", + r"c:\\temp\\", r"\\recycle", +] + +SUSPICIOUS_COMMANDS = [ + r"powershell.*-enc", r"powershell.*-e\s+", r"powershell.*downloadstring", + r"powershell.*iex", r"powershell.*invoke-expression", + r"cmd.*/c\s+", r"mshta\s+", r"certutil.*-urlcache", + r"bitsadmin.*/transfer", r"regsvr32.*/s.*/u", + r"rundll32.*javascript", r"wscript.*\.vbs", +] + + +def parse_evtx_xml(xml_path): + """Parse exported Windows Event Log XML for Sysmon and Security events.""" + events = [] + try: + tree = ET.parse(xml_path) + root = tree.getroot() + ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"} + for event_el in root.findall(".//e:Event", ns): + system = event_el.find("e:System", ns) + event_data = event_el.find("e:EventData", ns) + if system is None: + continue + event_id = int(system.findtext("e:EventID", "0", ns)) + data = {} + if event_data is not None: + for d in event_data.findall("e:Data", ns): + name = d.get("Name", "") + data[name] = d.text or "" + events.append({ + "event_id": event_id, + "timestamp": system.findtext("e:TimeCreated/@SystemTime", "", ns) + or system.find("e:TimeCreated", ns).get("SystemTime", "") if system.find("e:TimeCreated", ns) is not None else "", + "computer": system.findtext("e:Computer", "", ns), + "data": data, + }) + except ET.ParseError as e: + return [{"error": f"XML parse error: {e}"}] + return events + + +def detect_schtasks_creation(events): + """Detect suspicious schtasks.exe process creation (Sysmon Event 1).""" + findings = [] + for evt in events: + if evt["event_id"] != 1: + continue + image = evt["data"].get("Image", "").lower() + cmdline = evt["data"].get("CommandLine", "") + parent = evt["data"].get("ParentImage", "") + + if "schtasks" not in image and "at.exe" not in image: + continue + if "/create" not in cmdline.lower() and "/change" not in cmdline.lower(): + continue + + severity = "MEDIUM" + reasons = [] + + for pattern in SUSPICIOUS_PATHS: + if re.search(pattern, cmdline, re.IGNORECASE): + severity = "HIGH" + reasons.append(f"Task executes from suspicious path: {pattern}") + + for pattern in SUSPICIOUS_COMMANDS: + if re.search(pattern, cmdline, re.IGNORECASE): + severity = "CRITICAL" + reasons.append(f"Suspicious command pattern: {pattern}") + + if "/s " in cmdline.lower() or "/s\t" in cmdline: + severity = "CRITICAL" + reasons.append("Remote task creation detected (lateral movement)") + + if "-enc" in cmdline.lower() or "-e " in cmdline.lower(): + encoded = re.search(r'-[eE](?:nc)?\s+([A-Za-z0-9+/=]{20,})', cmdline) + if encoded: + try: + decoded = base64.b64decode(encoded.group(1)).decode("utf-16-le", errors="replace") + reasons.append(f"Decoded command: {decoded[:150]}") + except Exception: + pass + + if not reasons: + reasons.append("Scheduled task creation detected") + + findings.append({ + "timestamp": evt["timestamp"], + "computer": evt["computer"], + "image": image, + "command_line": cmdline[:300], + "parent_process": parent, + "user": evt["data"].get("User", ""), + "severity": severity, + "reasons": reasons, + "mitre": "T1053.005", + }) + return findings + + +def detect_task_file_creation(events): + """Detect task XML file creation in System32\\Tasks (Sysmon Event 11).""" + findings = [] + for evt in events: + if evt["event_id"] != 11: + continue + target = evt["data"].get("TargetFilename", "") + if "\\windows\\system32\\tasks\\" not in target.lower(): + continue + process = evt["data"].get("Image", "") + findings.append({ + "timestamp": evt["timestamp"], + "task_file": target, + "created_by": process, + "severity": "MEDIUM", + "detail": "New scheduled task XML file created", + }) + return findings + + +def detect_event_4698(events): + """Detect Security Event 4698 — scheduled task registered.""" + findings = [] + for evt in events: + if evt["event_id"] != 4698: + continue + task_name = evt["data"].get("TaskName", "") + task_content = evt["data"].get("TaskContent", "") + user = evt["data"].get("SubjectUserName", "") + severity = "MEDIUM" + reasons = [] + + for pattern in SUSPICIOUS_COMMANDS: + if re.search(pattern, task_content, re.IGNORECASE): + severity = "CRITICAL" + reasons.append(f"Task content contains: {pattern}") + + findings.append({ + "timestamp": evt["timestamp"], + "task_name": task_name, + "registered_by": user, + "severity": severity, + "reasons": reasons or ["New task registered"], + "task_content_preview": task_content[:200], + }) + return findings + + +def run_audit(args): + """Execute scheduled task detection audit.""" + print(f"\n{'='*60}") + print(f" MALICIOUS SCHEDULED TASK DETECTION") + print(f" Generated: {datetime.utcnow().isoformat()} UTC") + print(f"{'='*60}\n") + + report = {} + + if args.evtx_xml: + events = parse_evtx_xml(args.evtx_xml) + report["total_events"] = len(events) + print(f"Parsed {len(events)} events from {args.evtx_xml}\n") + + schtask_findings = detect_schtasks_creation(events) + report["schtasks_findings"] = schtask_findings + print(f"--- SCHTASKS CREATION (Event 1) — {len(schtask_findings)} findings ---") + for f in schtask_findings[:15]: + print(f" [{f['severity']}] {f['computer']}: {f['command_line'][:80]}") + for r in f["reasons"]: + print(f" -> {r[:100]}") + + file_findings = detect_task_file_creation(events) + report["task_file_findings"] = file_findings + print(f"\n--- TASK FILE CREATION (Event 11) — {len(file_findings)} findings ---") + for f in file_findings[:10]: + print(f" [{f['severity']}] {f['task_file']}") + + reg_findings = detect_event_4698(events) + report["event_4698_findings"] = reg_findings + print(f"\n--- TASK REGISTRATION (Event 4698) — {len(reg_findings)} findings ---") + for f in reg_findings[:10]: + print(f" [{f['severity']}] {f['task_name']} by {f['registered_by']}") + + return report + + +def main(): + parser = argparse.ArgumentParser(description="Sysmon Scheduled Task Detection Agent") + parser.add_argument("--evtx-xml", required=True, + help="Exported event log XML file to analyze") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + report = run_audit(args) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/hunting-for-anomalous-powershell-execution/LICENSE b/skills/hunting-for-anomalous-powershell-execution/LICENSE new file mode 100644 index 00000000..09d37ad6 --- /dev/null +++ b/skills/hunting-for-anomalous-powershell-execution/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/hunting-for-anomalous-powershell-execution/SKILL.md b/skills/hunting-for-anomalous-powershell-execution/SKILL.md new file mode 100644 index 00000000..bba98df3 --- /dev/null +++ b/skills/hunting-for-anomalous-powershell-execution/SKILL.md @@ -0,0 +1,54 @@ +--- +name: hunting-for-anomalous-powershell-execution +description: > + Hunt for malicious PowerShell activity by analyzing Script Block Logging (Event 4104), + Module Logging (Event 4103), and process creation events. The analyst parses Windows + Event Log EVTX files to detect obfuscated commands, AMSI bypass attempts, encoded + payloads, credential dumping keywords, and suspicious download cradles. Activates for + requests involving PowerShell threat hunting, script block analysis, encoded command + detection, or AMSI bypass identification. +domain: cybersecurity +subdomain: threat-hunting +tags: [powershell, script-block-logging, event-4104, amsi, threat-hunting, evtx, obfuscation] +version: "1.0" +author: mahipal +license: MIT +--- +# Hunting for Anomalous PowerShell Execution + +## Overview + +PowerShell Script Block Logging (Event ID 4104) records the full deobfuscated script text +executed on a Windows endpoint, making it the primary data source for hunting malicious +PowerShell. Combined with Module Logging (4103) and process creation events, analysts can +detect encoded commands, AMSI bypass patterns, download cradles, credential theft tools, +and fileless attack techniques even when the attacker uses obfuscation layers. + +## Prerequisites + +- Windows Event Log exports (.evtx) from Microsoft-Windows-PowerShell/Operational +- Python 3.8+ with python-evtx and lxml libraries +- Script Block Logging enabled via Group Policy +- Understanding of common PowerShell attack techniques + +## Steps + +1. Parse EVTX files extracting Event 4104 script block text and metadata +2. Reassemble multi-part script blocks using ScriptBlock ID correlation +3. Scan script text for AMSI bypass indicators and obfuscation patterns +4. Detect encoded command execution and base64 payloads +5. Identify download cradles, credential dumping, and lateral movement commands +6. Score and prioritize findings by threat severity + +## Expected Output + +```json +{ + "total_events": 1247, + "suspicious_events": 23, + "amsi_bypass_attempts": 2, + "encoded_commands": 8, + "download_cradles": 5, + "credential_access": 3 +} +``` diff --git a/skills/hunting-for-anomalous-powershell-execution/references/api-reference.md b/skills/hunting-for-anomalous-powershell-execution/references/api-reference.md new file mode 100644 index 00000000..46e0d327 --- /dev/null +++ b/skills/hunting-for-anomalous-powershell-execution/references/api-reference.md @@ -0,0 +1,106 @@ +# Hunting for Anomalous PowerShell Execution — API Reference + +## Windows Event Log IDs + +| Event ID | Log Source | Description | +|----------|-----------|-------------| +| 4104 | Microsoft-Windows-PowerShell/Operational | Script Block Logging — full deobfuscated script text | +| 4103 | Microsoft-Windows-PowerShell/Operational | Module Logging — pipeline execution details | +| 4688 | Security | Process Creation with command line auditing | +| 800 | Windows PowerShell | Pipeline execution (classic log) | + +## Event 4104 XML Fields + +| Field | Path | Description | +|-------|------|-------------| +| ScriptBlockText | EventData/Data[@Name='ScriptBlockText'] | Full script block content | +| ScriptBlockId | EventData/Data[@Name='ScriptBlockId'] | GUID linking multi-part blocks | +| MessageNumber | EventData/Data[@Name='MessageNumber'] | Part number for split blocks | +| MessageTotal | EventData/Data[@Name='MessageTotal'] | Total parts in split block | +| Path | EventData/Data[@Name='Path'] | Script file path (if applicable) | + +## AMSI Bypass Indicators + +| Indicator | Context | +|-----------|---------| +| `System.Management.Automation.AmsiUtils` | Reflection access to AMSI internals | +| `amsiInitFailed` | Setting AMSI init flag to bypass scanning | +| `AmsiScanBuffer` | Patching the scan buffer function | +| `amsi.dll` | Direct DLL manipulation | +| `VirtualProtect` | Memory protection change for AMSI patching | +| `Marshal::Copy` | Overwriting AMSI function bytes in memory | + +## Suspicious PowerShell Keywords + +| Keyword | Category | +|---------|----------| +| `Invoke-Mimikatz` | Credential Dumping | +| `Invoke-Kerberoast` | Credential Access | +| `Invoke-ShellCode` | Code Injection | +| `Invoke-ReflectivePEInjection` | Process Injection | +| `PowerView` | Active Directory Enumeration | +| `SharpHound` / `BloodHound` | AD Attack Path Mapping | +| `Rubeus` | Kerberos Ticket Manipulation | +| `Out-Minidump` | LSASS Memory Dumping | + +## Download Cradle Patterns + +| Pattern | Example | +|---------|---------| +| `Net.WebClient` | `(New-Object Net.WebClient).DownloadString(...)` | +| `Invoke-WebRequest` | `IWR -Uri http://... -OutFile ...` | +| `DownloadString` | `$wc.DownloadString('http://...')` | +| `Start-BitsTransfer` | `Start-BitsTransfer -Source http://...` | +| `Invoke-RestMethod` | `IRM http://... \| IEX` | + +## Obfuscation Indicators + +| Pattern | Description | +|---------|-------------| +| `-EncodedCommand` / `-enc` | Base64-encoded PowerShell command | +| `IEX` / `Invoke-Expression` | Dynamic execution of string content | +| `[Convert]::FromBase64String` | Base64 decoding in script | +| `-join [char[]]` | Character array concatenation obfuscation | +| `.Replace()` chaining | String substitution for keyword evasion | + +## python-evtx Library Usage + +```python +import Evtx.Evtx as evtx +from lxml import etree + +with evtx.Evtx("PowerShell-Operational.evtx") as log: + for record in log.records(): + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + # Extract EventID, EventData fields +``` + +## CLI Usage + +```bash +# Hunt for suspicious PowerShell in EVTX file +python agent.py --evtx /path/to/PowerShell-Operational.evtx + +# Limit events parsed +python agent.py --evtx logs.evtx --max-events 5000 + +# Save report to JSON +python agent.py --evtx logs.evtx --output hunt_report.json +``` + +## Group Policy Settings for Script Block Logging + +``` +Computer Configuration > Administrative Templates > Windows Components + > Windows PowerShell > Turn on PowerShell Script Block Logging + -> Enabled + -> Log script block invocation start / stop events: Checked +``` + +## External References + +- [Splunk: Hunting for Malicious PowerShell using Script Block Logging](https://www.splunk.com/en_us/blog/security/hunting-for-malicious-powershell-using-script-block-logging.html) +- [block-parser: PowerShell Script Block Log Parser](https://github.com/matthewdunwoody/block-parser) +- [Windows Forensic Artifacts: EVTX 4104](https://github.com/Psmths/windows-forensic-artifacts/blob/main/execution/evtx-4104-script-block-logging.md) +- [Elastic: AMSI Bypass via PowerShell Detection Rule](https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/windows/defense_evasion_amsi_bypass_powershell) diff --git a/skills/hunting-for-anomalous-powershell-execution/scripts/agent.py b/skills/hunting-for-anomalous-powershell-execution/scripts/agent.py new file mode 100644 index 00000000..82acc182 --- /dev/null +++ b/skills/hunting-for-anomalous-powershell-execution/scripts/agent.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""PowerShell Script Block Logging threat hunting agent.""" + +import json +import sys +import argparse +import base64 +import re +from datetime import datetime +from collections import defaultdict + +try: + import Evtx.Evtx as evtx + from lxml import etree +except ImportError: + print("Install: pip install python-evtx lxml") + sys.exit(1) + +NS = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"} + +AMSI_INDICATORS = [ + "amsiutils", "amsiinitfailed", "amsicontext", "amsisession", + "amsiinitialize", "amsi.dll", "amsiScanBuffer", + "System.Management.Automation.AmsiUtils", +] + +SUSPICIOUS_KEYWORDS = [ + "Invoke-Mimikatz", "Invoke-Kerberoast", "Invoke-ShellCode", + "Invoke-ReflectivePEInjection", "Invoke-TokenManipulation", + "Get-GPPPassword", "Get-Keystrokes", "Get-TimedScreenshot", + "Out-Minidump", "Invoke-NinjaCopy", "Invoke-CredentialInjection", + "Invoke-DllInjection", "Invoke-WMICommand", "PowerSploit", + "Empire", "BloodHound", "Rubeus", "SharpHound", + "Invoke-PSInject", "Invoke-RunAs", "PowerView", +] + +DOWNLOAD_PATTERNS = [ + r"Net\.WebClient", r"Invoke-WebRequest", r"wget\s", r"curl\s", + r"DownloadString", r"DownloadFile", r"DownloadData", + r"Start-BitsTransfer", r"Invoke-RestMethod", + r"New-Object\s+IO\.MemoryStream", +] + +OBFUSCATION_PATTERNS = [ + r"-[Ee]nc(?:oded)?[Cc]ommand", + r"\-e\s+[A-Za-z0-9+/=]{20,}", + r"IEX\s*\(", + r"Invoke-Expression", + r"\[Convert\]::FromBase64String", + r"\[System\.Text\.Encoding\]::", + r"\.Replace\(['\"][^'\"]+['\"],\s*['\"][^'\"]+['\"]\)", + r"-join\s*\[char\[\]\]", + r"\$env:comspec", +] + + +def parse_evtx_4104(evtx_path, max_events=10000): + """Parse Event 4104 script block logging entries from EVTX.""" + events = [] + count = 0 + with evtx.Evtx(evtx_path) as log: + for record in log.records(): + if count >= max_events: + break + xml = record.xml() + root = etree.fromstring(xml.encode("utf-8")) + event_id_el = root.find(".//e:System/e:EventID", NS) + if event_id_el is None or event_id_el.text != "4104": + continue + count += 1 + time_el = root.find(".//e:System/e:TimeCreated", NS) + timestamp = time_el.get("SystemTime", "") if time_el is not None else "" + data = {} + for el in root.findall(".//e:EventData/e:Data", NS): + name = el.get("Name", "") + data[name] = el.text or "" + events.append({ + "timestamp": timestamp, + "script_block_id": data.get("ScriptBlockId", ""), + "script_block_text": data.get("ScriptBlockText", ""), + "message_number": data.get("MessageNumber", "1"), + "message_total": data.get("MessageTotal", "1"), + "path": data.get("Path", ""), + }) + return events + + +def reassemble_script_blocks(events): + """Reassemble multi-part script blocks by ScriptBlockId.""" + blocks = defaultdict(list) + for ev in events: + sb_id = ev.get("script_block_id", "") + if sb_id: + blocks[sb_id].append(ev) + assembled = [] + for sb_id, parts in blocks.items(): + parts.sort(key=lambda x: int(x.get("message_number", "1"))) + full_text = "".join(p.get("script_block_text", "") for p in parts) + assembled.append({ + "script_block_id": sb_id, + "timestamp": parts[0].get("timestamp", ""), + "path": parts[0].get("path", ""), + "parts": len(parts), + "full_text": full_text, + }) + return assembled + + +def detect_amsi_bypass(script_text): + """Check script text for AMSI bypass indicators.""" + findings = [] + lower = script_text.lower() + for indicator in AMSI_INDICATORS: + if indicator.lower() in lower: + findings.append({"type": "amsi_bypass", "indicator": indicator}) + return findings + + +def detect_suspicious_keywords(script_text): + """Check for known offensive tool keywords.""" + findings = [] + for kw in SUSPICIOUS_KEYWORDS: + if kw.lower() in script_text.lower(): + findings.append({"type": "credential_or_offensive_tool", "keyword": kw}) + return findings + + +def detect_download_cradles(script_text): + """Detect download cradle patterns in script text.""" + findings = [] + for pattern in DOWNLOAD_PATTERNS: + if re.search(pattern, script_text, re.IGNORECASE): + findings.append({"type": "download_cradle", "pattern": pattern}) + return findings + + +def detect_obfuscation(script_text): + """Detect obfuscation and encoded command patterns.""" + findings = [] + for pattern in OBFUSCATION_PATTERNS: + if re.search(pattern, script_text, re.IGNORECASE): + findings.append({"type": "obfuscation", "pattern": pattern}) + b64_match = re.search(r"[A-Za-z0-9+/=]{40,}", script_text) + if b64_match: + try: + decoded = base64.b64decode(b64_match.group()).decode("utf-16-le", errors="ignore") + if any(c.isalpha() for c in decoded[:20]): + findings.append({ + "type": "encoded_payload", + "decoded_preview": decoded[:200], + }) + except Exception: + pass + return findings + + +def hunt_scripts(assembled_blocks): + """Run all detection checks on assembled script blocks.""" + results = [] + for block in assembled_blocks: + text = block.get("full_text", "") + if not text.strip(): + continue + findings = [] + findings.extend(detect_amsi_bypass(text)) + findings.extend(detect_suspicious_keywords(text)) + findings.extend(detect_download_cradles(text)) + findings.extend(detect_obfuscation(text)) + if findings: + results.append({ + "script_block_id": block["script_block_id"], + "timestamp": block["timestamp"], + "path": block["path"], + "text_preview": text[:300], + "findings": findings, + "severity": "high" if any( + f["type"] in ("amsi_bypass", "credential_or_offensive_tool") + for f in findings + ) else "medium", + }) + return results + + +def run_audit(args): + """Execute PowerShell script block hunting.""" + print(f"\n{'='*60}") + print(f" POWERSHELL SCRIPT BLOCK HUNTING") + print(f" Generated: {datetime.utcnow().isoformat()} UTC") + print(f"{'='*60}\n") + + report = {} + events = parse_evtx_4104(args.evtx, args.max_events) + report["total_4104_events"] = len(events) + print(f"Parsed {len(events)} Event 4104 records\n") + + blocks = reassemble_script_blocks(events) + report["unique_script_blocks"] = len(blocks) + print(f"Reassembled {len(blocks)} unique script blocks\n") + + results = hunt_scripts(blocks) + report["suspicious_blocks"] = len(results) + report["findings"] = results + + amsi = sum(1 for r in results if any(f["type"] == "amsi_bypass" for f in r["findings"])) + cred = sum(1 for r in results if any(f["type"] == "credential_or_offensive_tool" for f in r["findings"])) + dl = sum(1 for r in results if any(f["type"] == "download_cradle" for f in r["findings"])) + obf = sum(1 for r in results if any(f["type"] == "obfuscation" for f in r["findings"])) + report["summary"] = { + "amsi_bypass_attempts": amsi, + "credential_access": cred, + "download_cradles": dl, + "obfuscation_detected": obf, + } + + print(f"--- HUNT RESULTS ---") + print(f" AMSI bypass attempts: {amsi}") + print(f" Credential/offensive tools: {cred}") + print(f" Download cradles: {dl}") + print(f" Obfuscation detected: {obf}") + print(f"\n--- HIGH SEVERITY ---") + for r in results[:15]: + if r["severity"] == "high": + print(f" [{r['timestamp']}] {r['script_block_id']}") + for f in r["findings"]: + print(f" {f['type']}: {f.get('keyword', f.get('indicator', ''))}") + + return report + + +def main(): + parser = argparse.ArgumentParser(description="PowerShell Script Block Hunting Agent") + parser.add_argument("--evtx", required=True, + help="Path to PowerShell Operational .evtx file") + parser.add_argument("--max-events", type=int, default=10000, + help="Max events to parse (default: 10000)") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + report = run_audit(args) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/implementing-web-application-logging-with-modsecurity/LICENSE b/skills/implementing-web-application-logging-with-modsecurity/LICENSE new file mode 100644 index 00000000..09d37ad6 --- /dev/null +++ b/skills/implementing-web-application-logging-with-modsecurity/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/implementing-web-application-logging-with-modsecurity/SKILL.md b/skills/implementing-web-application-logging-with-modsecurity/SKILL.md new file mode 100644 index 00000000..31027c23 --- /dev/null +++ b/skills/implementing-web-application-logging-with-modsecurity/SKILL.md @@ -0,0 +1,45 @@ +--- +name: implementing-web-application-logging-with-modsecurity +description: > + Configure ModSecurity WAF with OWASP Core Rule Set (CRS) for web application logging, + tune rules to reduce false positives, analyze audit logs for attack detection, and + implement custom SecRules for application-specific threats. The analyst configures + SecRuleEngine, SecAuditEngine, and CRS paranoia levels to balance security coverage + with operational stability. Activates for requests involving WAF configuration, + ModSecurity rule tuning, web application audit logging, or CRS deployment. +domain: cybersecurity +subdomain: web-application-security +tags: [modsecurity, waf, crs, owasp, web-security, audit-logging, rule-tuning] +version: "1.0" +author: mahipal +license: Apache-2.0 +--- +# Implementing Web Application Logging with ModSecurity + +## Overview + +ModSecurity is an open-source WAF engine that works with Apache, Nginx, and IIS. The OWASP +Core Rule Set (CRS) provides generic attack detection rules covering SQL injection, XSS, +RCE, LFI, and other OWASP Top 10 attacks. ModSecurity logs full request/response data in +audit logs for forensic analysis and generates alerts that feed into SIEM platforms. + +## Prerequisites + +- Web server (Apache 2.4+ or Nginx) with ModSecurity v3 module +- OWASP CRS v4.x installed +- Log aggregation infrastructure (ELK, Splunk, or Wazuh) + +## Steps + +1. Install ModSecurity and configure SecRuleEngine in DetectionOnly mode +2. Deploy OWASP CRS v4 and set paranoia level (PL1-PL4) +3. Configure SecAuditEngine for relevant-only logging +4. Tune false positives with SecRuleRemoveById and rule exclusions +5. Switch to blocking mode (SecRuleEngine On) after tuning period +6. Forward audit logs to SIEM for correlation and alerting + +## Expected Output + +``` +ModSecurity: Warning. Pattern match "(?:union\s+select)" [file "/etc/modsecurity/crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "45"] [id "942100"] [msg "SQL Injection Attack Detected via libinjection"] [severity "CRITICAL"] +``` diff --git a/skills/implementing-web-application-logging-with-modsecurity/references/api-reference.md b/skills/implementing-web-application-logging-with-modsecurity/references/api-reference.md new file mode 100644 index 00000000..e8f84b9b --- /dev/null +++ b/skills/implementing-web-application-logging-with-modsecurity/references/api-reference.md @@ -0,0 +1,65 @@ +# ModSecurity WAF Logging — API Reference + +## Key ModSecurity Directives + +| Directive | Description | +|-----------|-------------| +| `SecRuleEngine On/Off/DetectionOnly` | Enable/disable rule engine | +| `SecAuditEngine On/Off/RelevantOnly` | Configure audit logging scope | +| `SecAuditLog /path/to/modsec_audit.log` | Audit log file path | +| `SecAuditLogParts ABCDEFHZ` | Audit log sections to include | +| `SecRequestBodyAccess On` | Inspect request bodies | +| `SecResponseBodyAccess On` | Inspect response bodies | +| `SecRuleRemoveById ` | Disable specific rule by ID | +| `SecRuleUpdateTargetById "!ARGS:param"` | Exclude parameter from rule | + +## Audit Log Sections + +| Section | Contents | +|---------|----------| +| A | Audit log header (timestamp, transaction ID) | +| B | Request headers | +| C | Request body | +| E | Response body | +| F | Response headers | +| H | Audit log trailer (rule matches, scores) | +| Z | End of entry marker | + +## OWASP CRS Rule ID Ranges + +| Range | Category | +|-------|----------| +| 911xxx | Method Enforcement | +| 920xxx | Protocol Enforcement | +| 930xxx | Local File Inclusion | +| 932xxx | Remote Code Execution | +| 941xxx | Cross-Site Scripting (XSS) | +| 942xxx | SQL Injection | +| 944xxx | Java/Spring Attack | +| 949xxx | Inbound Anomaly Score Blocking | + +## CRS Paranoia Levels + +| Level | Description | +|-------|-------------| +| PL1 | Default — low false positives, covers common attacks | +| PL2 | Moderate — adds more patterns, some tuning needed | +| PL3 | High — aggressive detection, significant tuning needed | +| PL4 | Extreme — maximum coverage, heavy tuning required | + +## Configuration Example + +```apache +SecRuleEngine DetectionOnly +SecAuditEngine RelevantOnly +SecAuditLogRelevantStatus "^(?:5|4(?!04))" +SecAuditLogParts ABCDEFHZ +SecAuditLogType Serial +SecAuditLog /var/log/modsec_audit.log +``` + +## External References + +- [ModSecurity v3 Reference Manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) +- [OWASP CRS Documentation](https://coreruleset.org/docs/) +- [CRS Tuning Guide](https://coreruleset.org/docs/concepts/false_positives_tuning/) diff --git a/skills/implementing-web-application-logging-with-modsecurity/scripts/agent.py b/skills/implementing-web-application-logging-with-modsecurity/scripts/agent.py new file mode 100644 index 00000000..2eb6adea --- /dev/null +++ b/skills/implementing-web-application-logging-with-modsecurity/scripts/agent.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""ModSecurity WAF audit log analysis and rule tuning agent.""" + +import json +import sys +import argparse +import re +from datetime import datetime +from collections import defaultdict + + +SECTION_PATTERN = re.compile(r'^--([a-f0-9]+)-([A-Z])--$') + +CRS_CATEGORIES = { + "911": "Method Enforcement", + "913": "Scanner Detection", + "920": "Protocol Enforcement", + "921": "Protocol Attack", + "930": "Local File Inclusion", + "931": "Remote File Inclusion", + "932": "Remote Code Execution", + "933": "PHP Injection", + "934": "Node.js Injection", + "941": "XSS Attack", + "942": "SQL Injection", + "943": "Session Fixation", + "944": "Java Attack", + "949": "Inbound Blocking", + "959": "Outbound Blocking", +} + + +def parse_audit_log(log_path, max_entries=5000): + """Parse ModSecurity serial audit log format.""" + entries = [] + current = {} + current_section = None + + with open(log_path, "r", errors="replace") as f: + for line in f: + match = SECTION_PATTERN.match(line.strip()) + if match: + tx_id = match.group(1) + section = match.group(2) + if section == "A": + if current and current.get("tx_id"): + entries.append(current) + if len(entries) >= max_entries: + break + current = {"tx_id": tx_id, "sections": {}} + current_section = section + current["sections"][section] = "" + elif current_section and current_section in current.get("sections", {}): + current["sections"][current_section] += line + + if current and current.get("tx_id"): + entries.append(current) + + parsed = [] + for entry in entries: + record = {"tx_id": entry["tx_id"]} + section_a = entry["sections"].get("A", "") + if section_a: + parts = section_a.strip().split() + if len(parts) >= 3: + record["timestamp"] = parts[0] if parts else "" + record["client_ip"] = parts[1] if len(parts) > 1 else "" + + section_b = entry["sections"].get("B", "") + if section_b: + first_line = section_b.strip().split("\n")[0] + req_parts = first_line.split() + if len(req_parts) >= 2: + record["method"] = req_parts[0] + record["uri"] = req_parts[1] + + section_h = entry["sections"].get("H", "") + record["rules_matched"] = [] + for rule_match in re.finditer( + r'\[id "(\d+)"\].*?\[msg "([^"]+)"\].*?\[severity "([^"]+)"\]', + section_h + ): + record["rules_matched"].append({ + "rule_id": rule_match.group(1), + "message": rule_match.group(2), + "severity": rule_match.group(3), + }) + + anomaly = re.search(r'Inbound Anomaly Score.*?(\d+)', section_h) + if anomaly: + record["anomaly_score"] = int(anomaly.group(1)) + + parsed.append(record) + return parsed + + +def analyze_rule_frequency(entries): + """Analyze which rules fire most frequently for tuning.""" + rule_counts = defaultdict(int) + rule_msgs = {} + for entry in entries: + for rule in entry.get("rules_matched", []): + rid = rule["rule_id"] + rule_counts[rid] += 1 + rule_msgs[rid] = rule["message"] + + sorted_rules = sorted(rule_counts.items(), key=lambda x: x[1], reverse=True) + results = [] + for rid, count in sorted_rules: + category = CRS_CATEGORIES.get(rid[:3], "Other") + results.append({ + "rule_id": rid, + "count": count, + "message": rule_msgs.get(rid, ""), + "category": category, + }) + return results + + +def identify_false_positive_candidates(entries, threshold=50): + """Identify rules that may be false positives based on frequency and pattern.""" + rule_ips = defaultdict(set) + rule_uris = defaultdict(set) + rule_counts = defaultdict(int) + + for entry in entries: + for rule in entry.get("rules_matched", []): + rid = rule["rule_id"] + rule_counts[rid] += 1 + rule_ips[rid].add(entry.get("client_ip", "")) + rule_uris[rid].add(entry.get("uri", "")) + + candidates = [] + for rid, count in rule_counts.items(): + if count >= threshold and len(rule_ips[rid]) > 10: + candidates.append({ + "rule_id": rid, + "hit_count": count, + "unique_ips": len(rule_ips[rid]), + "unique_uris": len(rule_uris[rid]), + "recommendation": f"SecRuleRemoveById {rid}", + "reason": "High frequency across many IPs — likely false positive", + }) + return candidates + + +def generate_exclusion_rules(candidates): + """Generate ModSecurity rule exclusion configuration.""" + lines = ["# Auto-generated false positive exclusions"] + for c in candidates: + lines.append(f"# Rule {c['rule_id']}: {c['hit_count']} hits, " + f"{c['unique_ips']} unique IPs") + lines.append(f"SecRuleRemoveById {c['rule_id']}") + return "\n".join(lines) + + +def analyze_attack_summary(entries): + """Summarize detected attacks by category and severity.""" + category_counts = defaultdict(int) + severity_counts = defaultdict(int) + top_attackers = defaultdict(int) + + for entry in entries: + for rule in entry.get("rules_matched", []): + cat = CRS_CATEGORIES.get(rule["rule_id"][:3], "Other") + category_counts[cat] += 1 + severity_counts[rule["severity"]] += 1 + if entry.get("anomaly_score", 0) >= 5: + top_attackers[entry.get("client_ip", "")] += 1 + + return { + "by_category": dict(sorted(category_counts.items(), key=lambda x: x[1], reverse=True)), + "by_severity": dict(severity_counts), + "top_attackers": dict(sorted(top_attackers.items(), key=lambda x: x[1], reverse=True)[:20]), + } + + +def run_audit(args): + """Execute ModSecurity audit log analysis.""" + print(f"\n{'='*60}") + print(f" MODSECURITY AUDIT LOG ANALYSIS") + print(f" Generated: {datetime.utcnow().isoformat()} UTC") + print(f"{'='*60}\n") + + report = {} + + entries = parse_audit_log(args.audit_log, args.max_entries) + report["total_entries"] = len(entries) + print(f"Parsed {len(entries)} audit log entries\n") + + attack_summary = analyze_attack_summary(entries) + report["attack_summary"] = attack_summary + print(f"--- ATTACK SUMMARY ---") + for cat, count in list(attack_summary["by_category"].items())[:10]: + print(f" {cat}: {count}") + print(f"\n Severity: {attack_summary['by_severity']}") + print(f"\n--- TOP ATTACKERS ---") + for ip, count in list(attack_summary["top_attackers"].items())[:10]: + print(f" {ip}: {count} alerts") + + rule_freq = analyze_rule_frequency(entries) + report["rule_frequency"] = rule_freq[:20] + print(f"\n--- TOP FIRING RULES ---") + for r in rule_freq[:15]: + print(f" [{r['rule_id']}] {r['count']}x — {r['message'][:60]}") + + if args.tune: + fp_candidates = identify_false_positive_candidates(entries, args.fp_threshold) + report["false_positive_candidates"] = fp_candidates + print(f"\n--- FALSE POSITIVE CANDIDATES ({len(fp_candidates)}) ---") + for c in fp_candidates[:10]: + print(f" Rule {c['rule_id']}: {c['hit_count']} hits, " + f"{c['unique_ips']} IPs — {c['reason']}") + if fp_candidates: + exclusions = generate_exclusion_rules(fp_candidates) + report["exclusion_config"] = exclusions + + return report + + +def main(): + parser = argparse.ArgumentParser(description="ModSecurity Audit Log Agent") + parser.add_argument("--audit-log", required=True, + help="Path to ModSecurity audit log file") + parser.add_argument("--max-entries", type=int, default=5000, + help="Max log entries to parse (default: 5000)") + parser.add_argument("--tune", action="store_true", + help="Identify false positive candidates for tuning") + parser.add_argument("--fp-threshold", type=int, default=50, + help="Minimum hits for false positive candidate (default: 50)") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + report = run_audit(args) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/performing-fuzzing-with-aflplusplus/LICENSE b/skills/performing-fuzzing-with-aflplusplus/LICENSE new file mode 100644 index 00000000..09d37ad6 --- /dev/null +++ b/skills/performing-fuzzing-with-aflplusplus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/performing-fuzzing-with-aflplusplus/SKILL.md b/skills/performing-fuzzing-with-aflplusplus/SKILL.md new file mode 100644 index 00000000..cfdcfd18 --- /dev/null +++ b/skills/performing-fuzzing-with-aflplusplus/SKILL.md @@ -0,0 +1,55 @@ +--- +name: performing-fuzzing-with-aflplusplus +description: > + Perform coverage-guided fuzzing of compiled binaries using AFL++ (American Fuzzy Lop Plus Plus) + to discover memory corruption, crashes, and security vulnerabilities. The tester instruments + target binaries with afl-cc/afl-clang-fast, manages input corpora with afl-cmin and afl-tmin, + runs parallel fuzzing campaigns with afl-fuzz, and triages crashes using CASR or GDB scripts. + Activates for requests involving binary fuzzing, crash discovery, coverage-guided testing, or + AFL++ fuzzing campaigns. +domain: cybersecurity +subdomain: application-security +tags: [fuzzing, aflplusplus, coverage-guided, crash-triage, binary-analysis, security-testing] +version: "1.0" +author: mahipal +license: Apache-2.0 +--- +# Performing Fuzzing with AFL++ + +## Overview + +AFL++ is a community-maintained fork of American Fuzzy Lop (AFL) that provides coverage-guided +fuzzing for compiled binaries. It instruments targets at compile time or via QEMU/Unicorn mode +for binary-only fuzzing, then mutates input corpora to discover new code paths. AFL++ includes +advanced scheduling (MOpt, rare), custom mutators, CMPLOG for input-to-state comparison solving, +and persistent mode for high-throughput fuzzing. + +## Prerequisites + +- AFL++ installed (`apt install afl++` or build from source) +- Target binary source code (for compile-time instrumentation) or QEMU mode for binary-only +- Initial seed corpus of valid inputs for the target format +- Linux system with /proc/sys/kernel/core_pattern configured + +## Steps + +1. Instrument the target binary with `afl-cc` or `afl-clang-fast` +2. Prepare seed corpus directory with minimal valid inputs +3. Minimize corpus with `afl-cmin` to remove redundant seeds +4. Run `afl-fuzz` with appropriate flags (-i input -o output) +5. Monitor fuzzing progress via afl-whatsup and UI stats +6. Triage crashes with `afl-tmin` minimization and CASR/GDB analysis +7. Report unique crashes with reproduction steps + +## Expected Output + +``` ++++ Findings +++ + unique crashes: 12 + unique hangs: 3 + last crash: 00:02:15 ago ++++ Coverage +++ + map density: 4.23% / 8.41% + paths found: 1847 + exec speed: 2145/sec +``` diff --git a/skills/performing-fuzzing-with-aflplusplus/references/api-reference.md b/skills/performing-fuzzing-with-aflplusplus/references/api-reference.md new file mode 100644 index 00000000..7321d221 --- /dev/null +++ b/skills/performing-fuzzing-with-aflplusplus/references/api-reference.md @@ -0,0 +1,64 @@ +# AFL++ Fuzzing — API Reference + +## Installation + +```bash +apt install afl++ # Ubuntu/Debian +# Or build from source: +git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus && make all +``` + +## AFL++ CLI Tools + +| Tool | Description | +|------|-------------| +| `afl-cc` / `afl-clang-fast` | Compile-time instrumentation compiler wrapper | +| `afl-fuzz` | Main fuzzer — coverage-guided mutation engine | +| `afl-cmin` | Corpus minimization — remove redundant seeds | +| `afl-tmin` | Test case minimization — shrink individual inputs | +| `afl-whatsup` | Multi-instance campaign status summary | +| `afl-plot` | Generate fuzzing progress plots | +| `afl-showmap` | Display coverage map for a single input | + +## afl-fuzz Key Flags + +| Flag | Description | +|------|-------------| +| `-i ` | Input seed corpus directory | +| `-o ` | Output directory for findings | +| `-m ` | Memory limit (use `none` for ASAN) | +| `-t ` | Execution timeout per test case | +| `-x ` | Optional fuzzing dictionary | +| `-p ` | Power schedule: fast, coe, explore, rare, mmopt | +| `-l ` | CMPLOG instrumentation level (2=transforms, 3=all) | +| `-c ` | CMPLOG binary for input-to-state | +| `-M ` | Main fuzzer instance (parallel mode) | +| `-S ` | Secondary fuzzer instance (parallel mode) | +| `-Q` | QEMU mode (binary-only fuzzing) | +| `-U` | Unicorn mode | + +## fuzzer_stats File Fields + +| Field | Description | +|-------|-------------| +| `execs_done` | Total executions completed | +| `execs_per_sec` | Current execution speed | +| `corpus_count` | Total paths in corpus | +| `saved_crashes` | Unique crashes discovered | +| `saved_hangs` | Unique hangs discovered | +| `stability` | Execution stability percentage | +| `bitmap_cvg` | Code coverage bitmap density | + +## Crash Triage Tools + +| Tool | Purpose | +|------|---------| +| `casr-afl` | CASR crash severity analysis for AFL++ | +| `afl-tmin` | Minimize crash inputs | +| `gdb --batch -ex run` | Reproduce crash under debugger | + +## External References + +- [AFL++ Documentation](https://aflplus.plus/docs/) +- [AFL++ GitHub](https://github.com/AFLplusplus/AFLplusplus) +- [CASR Crash Triage](https://github.com/ispras/casr) diff --git a/skills/performing-fuzzing-with-aflplusplus/scripts/agent.py b/skills/performing-fuzzing-with-aflplusplus/scripts/agent.py new file mode 100644 index 00000000..4c9c149e --- /dev/null +++ b/skills/performing-fuzzing-with-aflplusplus/scripts/agent.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# For authorized testing only +"""AFL++ fuzzing campaign management and crash triage agent.""" + +import json +import sys +import argparse +import os +import subprocess +import glob +from datetime import datetime + + +def instrument_target(source_path, output_path, compiler="afl-clang-fast", + sanitizer=None): + """Compile target with AFL++ instrumentation.""" + cmd = [compiler, "-o", output_path, source_path] + if sanitizer == "asan": + cmd.insert(1, "-fsanitize=address") + elif sanitizer == "ubsan": + cmd.insert(1, "-fsanitize=undefined") + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + return { + "compiler": compiler, + "source": source_path, + "output": output_path, + "sanitizer": sanitizer, + "success": result.returncode == 0, + "stderr": result.stderr[:500] if result.stderr else "", + } + + +def minimize_corpus(afl_cmin_path, target_binary, input_dir, output_dir): + """Minimize seed corpus using afl-cmin.""" + cmd = [afl_cmin_path or "afl-cmin", "-i", input_dir, "-o", output_dir, + "--", target_binary] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + before = len(os.listdir(input_dir)) if os.path.isdir(input_dir) else 0 + after = len(os.listdir(output_dir)) if os.path.isdir(output_dir) else 0 + return { + "before": before, + "after": after, + "reduction_pct": round((1 - after / max(before, 1)) * 100, 1), + "success": result.returncode == 0, + } + + +def parse_fuzzer_stats(output_dir): + """Parse afl-fuzz fuzzer_stats file for campaign metrics.""" + stats_path = os.path.join(output_dir, "default", "fuzzer_stats") + if not os.path.exists(stats_path): + stats_path = os.path.join(output_dir, "fuzzer_stats") + if not os.path.exists(stats_path): + return {"error": f"fuzzer_stats not found in {output_dir}"} + + stats = {} + with open(stats_path, "r") as f: + for line in f: + if ":" in line: + key, val = line.split(":", 1) + stats[key.strip()] = val.strip() + + return { + "start_time": stats.get("start_time", ""), + "last_update": stats.get("last_update", ""), + "execs_done": int(stats.get("execs_done", 0)), + "execs_per_sec": float(stats.get("execs_per_sec", 0)), + "paths_total": int(stats.get("corpus_count", stats.get("paths_total", 0))), + "paths_found": int(stats.get("paths_found", 0)), + "unique_crashes": int(stats.get("saved_crashes", stats.get("unique_crashes", 0))), + "unique_hangs": int(stats.get("saved_hangs", stats.get("unique_hangs", 0))), + "stability": stats.get("stability", ""), + "bitmap_cvg": stats.get("bitmap_cvg", ""), + "command_line": stats.get("command_line", ""), + } + + +def triage_crashes(output_dir): + """Enumerate and classify crash files from AFL++ output.""" + crash_dirs = [ + os.path.join(output_dir, "default", "crashes"), + os.path.join(output_dir, "crashes"), + ] + crash_dir = None + for d in crash_dirs: + if os.path.isdir(d): + crash_dir = d + break + if not crash_dir: + return {"crashes": [], "total": 0} + + crashes = [] + for filename in sorted(os.listdir(crash_dir)): + if filename.startswith("README") or filename == ".state": + continue + filepath = os.path.join(crash_dir, filename) + size = os.path.getsize(filepath) + sig_parts = filename.split(",") + signal = "" + for part in sig_parts: + if part.startswith("sig:"): + signal = part.split(":")[1] + crashes.append({ + "filename": filename, + "size_bytes": size, + "signal": signal, + "path": filepath, + }) + return {"crashes": crashes, "total": len(crashes)} + + +def minimize_crash(afl_tmin_path, target_binary, crash_file, output_file): + """Minimize a crash test case with afl-tmin.""" + cmd = [afl_tmin_path or "afl-tmin", "-i", crash_file, "-o", output_file, + "--", target_binary] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + orig_size = os.path.getsize(crash_file) if os.path.exists(crash_file) else 0 + min_size = os.path.getsize(output_file) if os.path.exists(output_file) else 0 + return { + "original_size": orig_size, + "minimized_size": min_size, + "reduction_pct": round((1 - min_size / max(orig_size, 1)) * 100, 1), + "success": result.returncode == 0, + } + + +def run_whatsup(output_dir): + """Run afl-whatsup to get multi-instance campaign summary.""" + cmd = ["afl-whatsup", "-s", output_dir] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + return {"output": result.stdout[:2000] if result.stdout else result.stderr[:500]} + + +def run_audit(args): + """Execute AFL++ fuzzing campaign audit.""" + print(f"\n{'='*60}") + print(f" AFL++ FUZZING CAMPAIGN AUDIT") + print(f" Generated: {datetime.utcnow().isoformat()} UTC") + print(f"{'='*60}\n") + + report = {} + + if args.output_dir: + stats = parse_fuzzer_stats(args.output_dir) + report["fuzzer_stats"] = stats + print(f"--- FUZZER STATS ---") + print(f" Executions: {stats.get('execs_done', 0):,}") + print(f" Exec/sec: {stats.get('execs_per_sec', 0)}") + print(f" Paths: {stats.get('paths_total', 0)}") + print(f" Crashes: {stats.get('unique_crashes', 0)}") + print(f" Hangs: {stats.get('unique_hangs', 0)}") + print(f" Stability: {stats.get('stability', '')}") + print(f" Coverage: {stats.get('bitmap_cvg', '')}") + + crash_data = triage_crashes(args.output_dir) + report["crash_triage"] = crash_data + print(f"\n--- CRASH TRIAGE ({crash_data['total']} crashes) ---") + for c in crash_data["crashes"][:20]: + print(f" {c['filename']} ({c['size_bytes']}B) signal={c['signal']}") + + if args.instrument_src and args.instrument_out: + inst = instrument_target(args.instrument_src, args.instrument_out, + sanitizer=args.sanitizer) + report["instrumentation"] = inst + print(f"\n--- INSTRUMENTATION ---") + print(f" {'SUCCESS' if inst['success'] else 'FAILED'}: {inst['source']}") + + return report + + +def main(): + parser = argparse.ArgumentParser(description="AFL++ Fuzzing Campaign Agent") + parser.add_argument("--output-dir", help="AFL++ output directory to analyze") + parser.add_argument("--instrument-src", help="Source file to instrument") + parser.add_argument("--instrument-out", help="Output path for instrumented binary") + parser.add_argument("--sanitizer", choices=["asan", "ubsan"], + help="Address or undefined behavior sanitizer") + parser.add_argument("--output", help="Save report to JSON file") + args = parser.parse_args() + + report = run_audit(args) + if args.output: + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main()