Add 5 new cybersecurity skills with full implementations

- implementing-vulnerability-management-with-greenbone: python-gvm GMP API, scan task creation, XML report parsing
- detecting-email-account-compromise: Microsoft Graph inbox rules, impossible travel detection, OAuth grant analysis
- performing-threat-intelligence-sharing-with-misp: PyMISP event creation, attribute management, sharing validation
- analyzing-cobaltstrike-malleable-c2-profiles: dissect.cobaltstrike C2Profile parsing, Suricata rule generation
- hunting-for-registry-run-key-persistence: Sysmon Event 13 analysis, T1547.001 detection, Sigma rule generation
This commit is contained in:
mukul975
2026-03-11 00:40:23 +01:00
parent e77d55ad50
commit 757f1c8eae
16 changed files with 1439 additions and 0 deletions
@@ -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.
@@ -0,0 +1,43 @@
---
name: analyzing-network-packets-with-scapy
description: Craft, send, sniff, and dissect network packets using Scapy for protocol analysis, network reconnaissance, and traffic anomaly detection in authorized security testing
domain: cybersecurity
subdomain: network-security
tags:
- scapy
- packet-analysis
- network-forensics
- protocol-dissection
- pcap
- traffic-analysis
version: "1.0"
author: mahipal
license: Apache-2.0
---
# Analyzing Network Packets with Scapy
## Overview
Scapy is a Python packet manipulation library that enables crafting, sending, sniffing, and dissecting network packets at granular protocol layers. This skill covers using Scapy for security-relevant tasks including TCP/UDP/ICMP packet crafting, pcap file analysis, protocol field extraction, SYN scan implementation, DNS query analysis, and detecting anomalous traffic patterns such as unusually fragmented packets or malformed headers.
## Prerequisites
- Python 3.8+ with `scapy` library installed (`pip install scapy`)
- Root/administrator privileges for raw socket operations (sniffing, sending)
- Npcap (Windows) or libpcap (Linux) for packet capture
- Authorization to perform packet operations on target network
## Steps
1. Read and parse pcap/pcapng files with `rdpcap()` for offline analysis
2. Extract protocol layers (IP, TCP, UDP, DNS, HTTP) and field values
3. Compute traffic statistics: top talkers, protocol distribution, port frequency
4. Detect SYN flood patterns by analyzing TCP flag ratios
5. Identify DNS exfiltration indicators via query length and entropy analysis
6. Craft custom probe packets for authorized network testing
7. Export findings as structured JSON report
## Expected Output
JSON report containing packet statistics, protocol distribution, top source/destination IPs, detected anomalies (SYN floods, DNS tunneling indicators, fragmentation attacks), and per-flow summaries.
@@ -0,0 +1,90 @@
# Scapy Network Packet Analysis API Reference
## Core Scapy Functions
### Reading Packets
```python
from scapy.all import rdpcap, sniff, wrpcap
# Read pcap file
packets = rdpcap("capture.pcap")
# Live sniff with BPF filter (requires root)
packets = sniff(filter="tcp port 80", count=100, iface="eth0")
# Write packets to pcap
wrpcap("output.pcap", packets)
```
### Packet Layer Access
```python
from scapy.all import IP, TCP, UDP, DNS, DNSQR, ICMP
pkt = packets[0]
pkt.haslayer(IP) # Check if layer exists
pkt[IP].src # Source IP
pkt[IP].dst # Destination IP
pkt[TCP].sport # Source port
pkt[TCP].dport # Destination port
pkt[TCP].flags # TCP flags: S, SA, A, FA, R, PA
pkt[DNS].qd.qname # DNS query name
pkt[ICMP].type # ICMP type (8=echo request, 0=echo reply)
```
### Packet Crafting
```python
from scapy.all import IP, TCP, sr1, send
# SYN probe (authorized testing only)
syn = IP(dst="192.168.1.1") / TCP(dport=80, flags="S")
response = sr1(syn, timeout=2, verbose=0)
# ICMP ping
ping = IP(dst="192.168.1.1") / ICMP()
send(ping, verbose=0)
# Custom DNS query
dns = IP(dst="8.8.8.8") / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname="example.com"))
```
## Protocol Fields Reference
### TCP Flags
| Flag | Value | Meaning |
|------|-------|---------|
| S | 0x02 | SYN |
| SA | 0x12 | SYN-ACK |
| A | 0x10 | ACK |
| F | 0x01 | FIN |
| R | 0x04 | RST |
| P | 0x08 | PSH |
### ICMP Types
| Type | Meaning |
|------|---------|
| 0 | Echo Reply |
| 3 | Destination Unreachable |
| 8 | Echo Request |
| 11 | Time Exceeded |
## BPF Filter Syntax
```
tcp port 443 # TCP traffic on port 443
host 10.0.0.1 # All traffic to/from IP
src net 192.168.0.0/24 # Source from subnet
udp and port 53 # DNS traffic
tcp[tcpflags] & tcp-syn != 0 # SYN packets only
```
## CLI Usage
```bash
# Analyze pcap file for anomalies
python agent.py --pcap capture.pcap --output report.json
# Custom thresholds
python agent.py --pcap traffic.pcapng --syn-threshold 50 --dns-length 30
# Port scan detection sensitivity
python agent.py --pcap scan.pcap --scan-threshold 10
```
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Network packet analysis agent using Scapy for pcap parsing and anomaly detection."""
import json
import math
import argparse
from collections import defaultdict, Counter
from datetime import datetime
from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, ICMP, Raw
def load_pcap(filepath):
"""Load packets from a pcap/pcapng file."""
packets = rdpcap(filepath)
print(f"[+] Loaded {len(packets)} packets from {filepath}")
return packets
def extract_packet_info(packets):
"""Extract structured info from each packet with IP layer."""
records = []
for pkt in packets:
if not pkt.haslayer(IP):
continue
info = {
"src_ip": pkt[IP].src,
"dst_ip": pkt[IP].dst,
"proto": pkt[IP].proto,
"ttl": pkt[IP].ttl,
"length": len(pkt),
"flags": str(pkt[IP].flags),
"timestamp": float(pkt.time),
}
if pkt.haslayer(TCP):
info["src_port"] = pkt[TCP].sport
info["dst_port"] = pkt[TCP].dport
info["tcp_flags"] = str(pkt[TCP].flags)
info["protocol"] = "TCP"
elif pkt.haslayer(UDP):
info["src_port"] = pkt[UDP].sport
info["dst_port"] = pkt[UDP].dport
info["protocol"] = "UDP"
elif pkt.haslayer(ICMP):
info["icmp_type"] = pkt[ICMP].type
info["icmp_code"] = pkt[ICMP].code
info["protocol"] = "ICMP"
else:
info["protocol"] = str(pkt[IP].proto)
if pkt.haslayer(DNS) and pkt.haslayer(DNSQR):
info["dns_query"] = pkt[DNSQR].qname.decode("utf-8", errors="ignore").rstrip(".")
info["dns_type"] = pkt[DNSQR].qtype
records.append(info)
return records
def compute_traffic_stats(records):
"""Compute overall traffic statistics."""
src_ips = Counter(r["src_ip"] for r in records)
dst_ips = Counter(r["dst_ip"] for r in records)
protocols = Counter(r["protocol"] for r in records)
dst_ports = Counter(r.get("dst_port", 0) for r in records if r.get("dst_port"))
total_bytes = sum(r["length"] for r in records)
return {
"total_packets": len(records),
"total_bytes": total_bytes,
"unique_src_ips": len(src_ips),
"unique_dst_ips": len(dst_ips),
"top_src_ips": src_ips.most_common(10),
"top_dst_ips": dst_ips.most_common(10),
"protocol_distribution": dict(protocols),
"top_dst_ports": dst_ports.most_common(10),
}
def detect_syn_flood(records, threshold=100):
"""Detect SYN flood by counting SYN-only packets per destination IP."""
syn_counts = defaultdict(int)
synack_counts = defaultdict(int)
for r in records:
if r.get("tcp_flags") == "S":
syn_counts[r["dst_ip"]] += 1
elif r.get("tcp_flags") == "SA":
synack_counts[r["dst_ip"]] += 1
alerts = []
for ip, count in syn_counts.items():
ack_count = synack_counts.get(ip, 0)
ratio = ack_count / count if count > 0 else 1.0
if count >= threshold and ratio < 0.3:
alerts.append({
"detection": "SYN Flood",
"target_ip": ip,
"syn_count": count,
"synack_count": ack_count,
"synack_ratio": round(ratio, 4),
"severity": "critical",
})
return alerts
def calculate_entropy(data):
"""Calculate Shannon entropy of a string."""
if not data:
return 0.0
freq = Counter(data)
length = len(data)
return -sum((c / length) * math.log2(c / length) for c in freq.values())
def detect_dns_tunneling(records, length_threshold=50, entropy_threshold=3.5):
"""Detect DNS tunneling via long/high-entropy query names."""
alerts = []
for r in records:
query = r.get("dns_query", "")
if not query:
continue
subdomain = query.split(".")[0] if "." in query else query
if len(subdomain) >= length_threshold or calculate_entropy(subdomain) >= entropy_threshold:
alerts.append({
"detection": "DNS Tunneling Indicator",
"query": query,
"subdomain_length": len(subdomain),
"entropy": round(calculate_entropy(subdomain), 4),
"src_ip": r["src_ip"],
"severity": "high",
})
return alerts
def detect_port_scan(records, threshold=20):
"""Detect port scanning by counting unique destination ports per source IP."""
src_ports = defaultdict(set)
for r in records:
if r.get("tcp_flags") == "S" and r.get("dst_port"):
src_ports[r["src_ip"]].add(r["dst_port"])
alerts = []
for ip, ports in src_ports.items():
if len(ports) >= threshold:
alerts.append({
"detection": "Port Scan",
"source_ip": ip,
"unique_ports_probed": len(ports),
"sample_ports": sorted(list(ports))[:20],
"severity": "high",
})
return alerts
def main():
parser = argparse.ArgumentParser(description="Network Packet Analysis Agent (Scapy)")
parser.add_argument("--pcap", required=True, help="Path to pcap/pcapng file")
parser.add_argument("--syn-threshold", type=int, default=100, help="SYN flood detection threshold")
parser.add_argument("--dns-length", type=int, default=50, help="DNS tunneling subdomain length threshold")
parser.add_argument("--scan-threshold", type=int, default=20, help="Port scan unique ports threshold")
parser.add_argument("--output", default="packet_analysis_report.json", help="Output report path")
args = parser.parse_args()
packets = load_pcap(args.pcap)
records = extract_packet_info(packets)
print(f"[+] Extracted {len(records)} IP-layer records")
stats = compute_traffic_stats(records)
syn_alerts = detect_syn_flood(records, args.syn_threshold)
dns_alerts = detect_dns_tunneling(records, args.dns_length)
scan_alerts = detect_port_scan(records, args.scan_threshold)
report = {
"analysis_time": datetime.utcnow().isoformat() + "Z",
"pcap_file": args.pcap,
"traffic_stats": stats,
"anomalies": {
"syn_flood": syn_alerts,
"dns_tunneling": dns_alerts,
"port_scan": scan_alerts,
},
"total_anomalies": len(syn_alerts) + len(dns_alerts) + len(scan_alerts),
}
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] SYN flood alerts: {len(syn_alerts)}")
print(f"[+] DNS tunneling indicators: {len(dns_alerts)}")
print(f"[+] Port scan detections: {len(scan_alerts)}")
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -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.
@@ -0,0 +1,44 @@
---
name: detecting-credential-dumping-techniques
description: Detect LSASS credential dumping, SAM database extraction, and NTDS.dit theft using Sysmon Event ID 10, Windows Security logs, and SIEM correlation rules
domain: cybersecurity
subdomain: threat-detection
tags:
- credential-dumping
- lsass
- mimikatz
- sysmon
- active-directory
- windows-security
- defense-evasion
version: "1.0"
author: mahipal
license: Apache-2.0
---
# Detecting Credential Dumping Techniques
## Overview
Credential dumping (MITRE ATT&CK T1003) is a post-exploitation technique where adversaries extract authentication credentials from OS memory, registry hives, or domain controller databases. This skill covers detection of LSASS memory access via Sysmon Event ID 10 (ProcessAccess), SAM registry hive export via reg.exe, NTDS.dit extraction via ntdsutil/vssadmin, and comsvcs.dll MiniDump abuse. Detection rules analyze GrantedAccess bitmasks, suspicious calling processes, and known tool signatures.
## Prerequisites
- Sysmon v14+ deployed with ProcessAccess logging (Event ID 10) for lsass.exe
- Windows Security audit policy enabling process creation (Event ID 4688) with command line logging
- Splunk or Elastic SIEM ingesting Sysmon and Windows Security logs
- Python 3.8+ for log analysis
## Steps
1. Configure Sysmon to log ProcessAccess events targeting lsass.exe
2. Forward Sysmon Event ID 10 and Windows Event ID 4688 to SIEM
3. Create detection rules for known GrantedAccess patterns (0x1010, 0x1FFFFF)
4. Detect comsvcs.dll MiniDump and procdump.exe targeting LSASS PID
5. Alert on reg.exe SAM/SECURITY/SYSTEM hive export commands
6. Detect ntdsutil/vssadmin shadow copy creation for NTDS.dit theft
7. Correlate detections with user/host context for risk scoring
## Expected Output
JSON report containing detected credential dumping indicators with technique classification, severity ratings, process details, MITRE ATT&CK mapping, and Splunk/Elastic detection queries.
@@ -0,0 +1,98 @@
# Credential Dumping Detection API Reference
## Sysmon Event ID 10 - ProcessAccess
### Key Fields
```
SourceImage - Process accessing LSASS
SourceProcessId - PID of accessing process
TargetImage - Should be C:\Windows\System32\lsass.exe
GrantedAccess - Access rights bitmask
CallTrace - DLL call stack of the access
```
### Suspicious GrantedAccess Values
| Value | Meaning | Tool Association |
|-------|---------|-----------------|
| 0x1010 | VM_READ + QUERY_LIMITED | Mimikatz |
| 0x1410 | VM_READ + QUERY_INFO | ProcDump |
| 0x1FFFFF | PROCESS_ALL_ACCESS | Various dumpers |
| 0x1438 | VM_READ + QUERY + DUP_HANDLE | Cobalt Strike |
| 0x40 | DUP_HANDLE only | Handle duplication |
## Sysmon Event ID 1 - Process Creation
### Command Line Patterns for Credential Theft
```
# SAM hive export
reg save hklm\sam C:\temp\sam.hiv
reg save hklm\security C:\temp\security.hiv
reg save hklm\system C:\temp\system.hiv
# comsvcs.dll LSASS dump
rundll32.exe C:\Windows\System32\comsvcs.dll, MiniDump <lsass_pid> dump.bin full
# NTDS.dit extraction
ntdsutil "activate instance ntds" ifm "create full C:\temp"
vssadmin create shadow /for=C:
```
## Splunk SPL Queries
### LSASS Access Detection
```spl
index=sysmon EventCode=10 TargetImage="*\\lsass.exe"
GrantedAccess IN ("0x1010","0x1FFFFF","0x1410","0x1438")
SourceImage!="*\\csrss.exe" SourceImage!="*\\svchost.exe"
| stats count by SourceImage, GrantedAccess, Computer, User
| sort -count
```
### comsvcs.dll MiniDump Detection
```spl
index=sysmon EventCode=1
(CommandLine="*comsvcs*MiniDump*" OR CommandLine="*comsvcs*#24*")
| table _time, Computer, User, ParentImage, CommandLine
```
### SAM/SECURITY Hive Export
```spl
index=sysmon EventCode=1 Image="*\\reg.exe"
(CommandLine="*save*hklm\\sam*" OR CommandLine="*save*hklm\\security*")
| table _time, Computer, User, CommandLine
```
## Elastic / KQL Queries
### LSASS Access in Elastic
```kql
event.code: "10" AND
winlog.event_data.TargetImage: *lsass.exe AND
winlog.event_data.GrantedAccess: ("0x1010" OR "0x1FFFFF")
```
### Process Creation with Credential Theft Commands
```kql
event.code: "1" AND
(process.command_line: *comsvcs*MiniDump* OR
process.command_line: *reg*save*hklm\\sam*)
```
## MITRE ATT&CK Mapping
| Sub-technique | ID | Detection Method |
|---|---|---|
| LSASS Memory | T1003.001 | Sysmon EID 10 GrantedAccess |
| Security Account Manager | T1003.002 | reg.exe save commands |
| NTDS | T1003.003 | ntdsutil / vssadmin commands |
| DCSync | T1003.006 | Event ID 4662 with replication GUIDs |
## CLI Usage
```bash
# Analyze Sysmon XML export
python agent.py --sysmon-xml sysmon_events.xml --output cred_report.json
# Print Splunk detection queries
python agent.py --show-splunk
```
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Detect credential dumping techniques via Sysmon/Windows event log analysis."""
import json
import re
import argparse
import xml.etree.ElementTree as ET
from collections import defaultdict
from datetime import datetime
LSASS_GRANTED_ACCESS_SUSPICIOUS = {
"0x1010": "PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION (Mimikatz-style)",
"0x1410": "PROCESS_VM_READ | PROCESS_QUERY_INFORMATION (procdump-style)",
"0x1FFFFF": "PROCESS_ALL_ACCESS (full access to LSASS)",
"0x1438": "PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE",
"0x40": "PROCESS_DUP_HANDLE (handle duplication for indirect access)",
}
SUSPICIOUS_CALLERS = [
"mimikatz", "procdump", "rundll32.exe", "taskmgr.exe",
"powershell.exe", "cmd.exe", "wmic.exe", "cscript.exe", "wscript.exe",
]
SAM_EXPORT_PATTERNS = [
r"reg\s+save\s+hklm\\sam",
r"reg\s+save\s+hklm\\security",
r"reg\s+save\s+hklm\\system",
r"esentutl.*ntds\.dit",
r"ntdsutil.*\"activate instance ntds\"",
r"vssadmin\s+create\s+shadow",
r"copy\s+\\\\.*\\c\$.*ntds\.dit",
r"secretsdump",
]
COMSVCS_PATTERNS = [
r"comsvcs\.dll.*MiniDump",
r"comsvcs\.dll.*#24",
r"rundll32.*comsvcs",
]
def parse_sysmon_xml(xml_path):
"""Parse Sysmon event log XML export for Event ID 10 (ProcessAccess)."""
tree = ET.parse(xml_path)
root = tree.getroot()
ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
events = []
for event_el in root.findall(".//e:Event", ns):
sys_el = event_el.find("e:System", ns)
event_id = int(sys_el.find("e:EventID", ns).text)
time_created = sys_el.find("e:TimeCreated", ns).attrib.get("SystemTime", "")
data_el = event_el.find("e:EventData", ns)
fields = {}
for d in data_el.findall("e:Data", ns):
fields[d.attrib.get("Name", "")] = d.text or ""
events.append({"event_id": event_id, "timestamp": time_created, **fields})
return events
def detect_lsass_access(events):
"""Detect suspicious ProcessAccess (Event ID 10) targeting lsass.exe."""
alerts = []
for ev in events:
if ev["event_id"] != 10:
continue
target = ev.get("TargetImage", "").lower()
if "lsass.exe" not in target:
continue
granted = ev.get("GrantedAccess", "")
source_image = ev.get("SourceImage", "").lower()
source_name = source_image.split("\\")[-1] if source_image else ""
severity = "medium"
reasons = []
if granted in LSASS_GRANTED_ACCESS_SUSPICIOUS:
reasons.append(f"Suspicious GrantedAccess {granted}: {LSASS_GRANTED_ACCESS_SUSPICIOUS[granted]}")
severity = "critical" if granted == "0x1FFFFF" else "high"
if any(s in source_name for s in SUSPICIOUS_CALLERS):
reasons.append(f"Suspicious calling process: {source_name}")
severity = "critical"
if not reasons:
continue
alerts.append({
"detection": "LSASS Memory Access",
"mitre_technique": "T1003.001",
"timestamp": ev["timestamp"],
"source_process": ev.get("SourceImage", ""),
"source_pid": ev.get("SourceProcessId", ""),
"target_process": ev.get("TargetImage", ""),
"granted_access": granted,
"call_trace": ev.get("CallTrace", "")[:200],
"severity": severity,
"reasons": reasons,
"user": ev.get("SourceUser", ev.get("User", "")),
"host": ev.get("Computer", ""),
})
return alerts
def detect_credential_commands(events):
"""Detect SAM/NTDS.dit export and comsvcs.dll dump commands from Event ID 1 or 4688."""
alerts = []
for ev in events:
if ev["event_id"] not in (1, 4688):
continue
cmdline = ev.get("CommandLine", ev.get("ProcessCommandLine", "")).lower()
if not cmdline:
continue
for pattern in SAM_EXPORT_PATTERNS:
if re.search(pattern, cmdline, re.IGNORECASE):
technique = "T1003.002" if "sam" in cmdline or "security" in cmdline else "T1003.003"
alerts.append({
"detection": "Registry Hive / NTDS.dit Export",
"mitre_technique": technique,
"timestamp": ev["timestamp"],
"command_line": ev.get("CommandLine", ev.get("ProcessCommandLine", "")),
"process": ev.get("Image", ev.get("NewProcessName", "")),
"user": ev.get("User", ev.get("SubjectUserName", "")),
"severity": "critical",
})
break
for pattern in COMSVCS_PATTERNS:
if re.search(pattern, cmdline, re.IGNORECASE):
alerts.append({
"detection": "LSASS Dump via comsvcs.dll",
"mitre_technique": "T1003.001",
"timestamp": ev["timestamp"],
"command_line": ev.get("CommandLine", ev.get("ProcessCommandLine", "")),
"process": ev.get("Image", ev.get("NewProcessName", "")),
"user": ev.get("User", ev.get("SubjectUserName", "")),
"severity": "critical",
})
break
return alerts
def detect_ntdll_access(events):
"""Detect suspicious ntdll.dll access patterns in CallTrace (Mimikatz signature)."""
alerts = []
for ev in events:
if ev["event_id"] != 10:
continue
call_trace = ev.get("CallTrace", "")
if "ntdll.dll" in call_trace.lower() and "UNKNOWN" in call_trace:
target = ev.get("TargetImage", "").lower()
if "lsass.exe" in target:
alerts.append({
"detection": "NTDLL Suspicious CallTrace (Mimikatz Signature)",
"mitre_technique": "T1003.001",
"timestamp": ev["timestamp"],
"source_process": ev.get("SourceImage", ""),
"call_trace_snippet": call_trace[:300],
"severity": "critical",
})
return alerts
def generate_splunk_queries():
"""Return SPL detection queries for credential dumping."""
return {
"lsass_access": (
'index=sysmon EventCode=10 TargetImage="*\\\\lsass.exe" '
'GrantedAccess IN ("0x1010","0x1FFFFF","0x1410","0x1438") '
'| stats count by SourceImage, GrantedAccess, Computer'
),
"comsvcs_dump": (
'index=sysmon EventCode=1 CommandLine="*comsvcs*MiniDump*" '
'OR CommandLine="*comsvcs*#24*" | table _time, Computer, User, CommandLine'
),
"sam_export": (
'index=sysmon EventCode=1 (CommandLine="*reg*save*hklm\\\\sam*" '
'OR CommandLine="*reg*save*hklm\\\\security*") '
'| table _time, Computer, User, CommandLine'
),
"ntds_extraction": (
'index=sysmon EventCode=1 (CommandLine="*ntdsutil*" '
'OR CommandLine="*vssadmin*create*shadow*") '
'| table _time, Computer, User, CommandLine'
),
}
def main():
parser = argparse.ArgumentParser(description="Credential Dumping Detection Agent")
parser.add_argument("--sysmon-xml", help="Path to Sysmon event log XML export")
parser.add_argument("--output", default="credential_dump_report.json", help="Output report path")
parser.add_argument("--show-splunk", action="store_true", help="Print Splunk detection queries")
args = parser.parse_args()
if args.show_splunk:
for name, spl in generate_splunk_queries().items():
print(f"\n--- {name} ---\n{spl}")
return
if not args.sysmon_xml:
print("[!] Provide --sysmon-xml path or use --show-splunk for detection queries")
return
events = parse_sysmon_xml(args.sysmon_xml)
print(f"[+] Parsed {len(events)} Sysmon/Security events")
lsass_alerts = detect_lsass_access(events)
cmd_alerts = detect_credential_commands(events)
ntdll_alerts = detect_ntdll_access(events)
report = {
"analysis_time": datetime.utcnow().isoformat() + "Z",
"total_events": len(events),
"detections": {
"lsass_memory_access": lsass_alerts,
"credential_export_commands": cmd_alerts,
"ntdll_suspicious_calltrace": ntdll_alerts,
},
"total_alerts": len(lsass_alerts) + len(cmd_alerts) + len(ntdll_alerts),
"mitre_techniques": ["T1003.001", "T1003.002", "T1003.003"],
"splunk_queries": generate_splunk_queries(),
}
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[+] LSASS access alerts: {len(lsass_alerts)}")
print(f"[+] Credential export commands: {len(cmd_alerts)}")
print(f"[+] NTDLL suspicious traces: {len(ntdll_alerts)}")
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -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.
@@ -0,0 +1,44 @@
---
name: detecting-pass-the-ticket-attacks
description: Detect Kerberos Pass-the-Ticket (PtT) attacks by analyzing Windows Event IDs 4768, 4769, and 4771 for anomalous ticket usage patterns in Splunk and Elastic SIEM
domain: cybersecurity
subdomain: threat-detection
tags:
- kerberos
- pass-the-ticket
- active-directory
- splunk
- elastic
- credential-theft
- windows-security
version: "1.0"
author: mahipal
license: Apache-2.0
---
# Detecting Pass-the-Ticket Attacks
## Overview
Pass-the-Ticket (PtT) is a credential theft technique (MITRE ATT&CK T1550.003) where adversaries steal Kerberos tickets (TGT or TGS) from one system and replay them on another to authenticate without knowing the user's password. This skill teaches detection of PtT attacks by correlating Windows Security Event IDs 4768 (TGT request), 4769 (TGS request), and 4771 (pre-authentication failure) for anomalies such as ticket reuse across different hosts, RC4 encryption downgrades, and unusual service ticket request volumes.
## Prerequisites
- Windows Domain Controller with advanced audit policy enabled (Audit Kerberos Authentication Service, Audit Kerberos Service Ticket Operations)
- Splunk or Elastic SIEM ingesting Windows Security event logs
- Sysmon deployed on endpoints for supplementary process telemetry
- Python 3.8+ with `requests` library
## Steps
1. Enable Kerberos audit logging on Domain Controllers via Group Policy
2. Forward Event IDs 4768, 4769, and 4771 to SIEM platform
3. Deploy detection rules for RC4 encryption downgrade (TicketEncryptionType 0x17)
4. Create correlation rule for ticket reuse across multiple source IPs
5. Build baseline of normal TGS request volume per user/host
6. Alert on standard deviation anomalies in ticket request patterns
7. Investigate flagged events with enrichment from Active Directory
## Expected Output
JSON report containing detected PtT indicators including anomalous ticket requests, RC4 downgrades, cross-host ticket reuse events, and risk-scored users with MITRE ATT&CK technique mapping.
@@ -0,0 +1,104 @@
# Pass-the-Ticket Detection API Reference
## Windows Security Event IDs
### Event ID 4768 - TGT Requested
```
Key Fields:
TargetUserName - Account requesting TGT
TargetDomainName - Domain of account
IpAddress - Source IP of request
TicketEncryptionType - 0x12 (AES256), 0x17 (RC4-HMAC)
PreAuthType - 15 (PA-ENC-TIMESTAMP)
```
### Event ID 4769 - TGS Requested
```
Key Fields:
TargetUserName - Account using the ticket
ServiceName - SPN of requested service
IpAddress - Source IP
TicketEncryptionType - 0x17 indicates RC4 downgrade
TicketOptions - Kerberos ticket flags
```
### Event ID 4771 - Kerberos Pre-Authentication Failed
```
Key Fields:
TargetUserName - Account that failed
IpAddress - Source of failure
Status - 0x18 (wrong password), 0x12 (expired)
```
## Splunk SPL Queries
### RC4 Encryption Downgrade Detection
```spl
index=wineventlog sourcetype="WinEventLog:Security" EventCode=4769
TicketEncryptionType=0x17
| stats count by TargetUserName, IpAddress, ServiceName
| where count > 3
```
### Cross-Host Ticket Reuse
```spl
index=wineventlog EventCode=4769
| stats dc(IpAddress) as ip_count, values(IpAddress) as ips
by TargetUserName
| where ip_count > 1
| sort -ip_count
```
### TGS Volume Anomaly
```spl
index=wineventlog EventCode=4769
| bin _time span=1h
| stats count by TargetUserName, _time
| eventstats avg(count) as avg_count, stdev(count) as sd by TargetUserName
| where count > avg_count + (3 * sd)
```
## Elastic / KQL Queries
### RC4 Downgrade in Elastic
```kql
event.code: "4769" AND winlog.event_data.TicketEncryptionType: "0x17"
```
### Cross-Host Reuse in Elastic
```json
POST security-*/_search
{
"size": 0,
"query": { "term": { "event.code": "4769" } },
"aggs": {
"by_user": {
"terms": { "field": "winlog.event_data.TargetUserName" },
"aggs": {
"unique_ips": { "cardinality": { "field": "source.ip" } }
}
}
}
}
```
## MITRE ATT&CK Mapping
| Technique | ID | Detection |
|---|---|---|
| Use Alternate Authentication Material: Pass the Ticket | T1550.003 | RC4 downgrade, cross-host reuse |
| Steal or Forge Kerberos Tickets: Kerberoasting | T1558.003 | High TGS volume for SPNs |
| Brute Force: Password Spraying | T1110.003 | Pre-auth failure spikes |
## CLI Usage
```bash
# Parse exported event log XML and detect PtT indicators
python agent.py --evtx-xml security_events.xml --output report.json
# Show Splunk detection queries
python agent.py --show-splunk
# Custom thresholds
python agent.py --evtx-xml events.xml --tgs-threshold 30 --preauth-threshold 5
```
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Detect Kerberos Pass-the-Ticket attacks via Windows Event ID 4768/4769/4771 analysis."""
import json
import argparse
import xml.etree.ElementTree as ET
from collections import defaultdict
from datetime import datetime
def parse_evtx_xml(xml_path):
"""Parse exported Windows Security event log XML for Kerberos events."""
tree = ET.parse(xml_path)
root = tree.getroot()
ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
events = []
for event_el in root.findall(".//e:Event", ns):
sys_el = event_el.find("e:System", ns)
event_id = int(sys_el.find("e:EventID", ns).text)
if event_id not in (4768, 4769, 4771):
continue
time_created = sys_el.find("e:TimeCreated", ns).attrib.get("SystemTime", "")
data_el = event_el.find("e:EventData", ns)
fields = {}
for d in data_el.findall("e:Data", ns):
fields[d.attrib.get("Name", "")] = d.text or ""
events.append({
"event_id": event_id,
"timestamp": time_created,
"target_user": fields.get("TargetUserName", ""),
"target_domain": fields.get("TargetDomainName", ""),
"ip_address": fields.get("IpAddress", ""),
"service_name": fields.get("ServiceName", ""),
"ticket_encryption_type": fields.get("TicketEncryptionType", ""),
"status": fields.get("Status", ""),
"pre_auth_type": fields.get("PreAuthType", ""),
})
return events
def detect_rc4_downgrade(events):
"""Detect RC4 encryption downgrade (TicketEncryptionType 0x17) in TGS requests."""
alerts = []
for ev in events:
enc_type = ev["ticket_encryption_type"]
if enc_type in ("0x17", "23"):
alerts.append({
"detection": "RC4 Encryption Downgrade",
"mitre_technique": "T1550.003",
"event_id": ev["event_id"],
"timestamp": ev["timestamp"],
"user": ev["target_user"],
"domain": ev["target_domain"],
"service": ev["service_name"],
"ip_address": ev["ip_address"],
"encryption_type": enc_type,
"severity": "high",
"description": "RC4 (0x17) ticket encryption detected; may indicate Pass-the-Ticket or Kerberoasting",
})
return alerts
def detect_cross_host_ticket_reuse(events):
"""Detect same user TGS requests from multiple source IPs within short window."""
user_ips = defaultdict(set)
user_events = defaultdict(list)
for ev in events:
if ev["event_id"] == 4769 and ev["target_user"] and ev["ip_address"]:
key = f"{ev['target_user']}@{ev['target_domain']}"
user_ips[key].add(ev["ip_address"])
user_events[key].append(ev)
alerts = []
for user, ips in user_ips.items():
if len(ips) >= 2:
sample = user_events[user][:5]
alerts.append({
"detection": "Cross-Host Ticket Reuse",
"mitre_technique": "T1550.003",
"user": user,
"source_ips": list(ips),
"ip_count": len(ips),
"request_count": len(user_events[user]),
"severity": "critical",
"sample_timestamps": [e["timestamp"] for e in sample],
"description": "Same user ticket used from multiple IPs, indicating stolen ticket replay",
})
return alerts
def detect_anomalous_tgs_volume(events, threshold=50):
"""Detect users requesting abnormally high number of TGS tickets."""
user_tgs = defaultdict(int)
for ev in events:
if ev["event_id"] == 4769 and ev["target_user"]:
user_tgs[f"{ev['target_user']}@{ev['target_domain']}"] += 1
alerts = []
for user, count in user_tgs.items():
if count >= threshold:
alerts.append({
"detection": "Anomalous TGS Volume",
"mitre_technique": "T1550.003",
"user": user,
"tgs_request_count": count,
"threshold": threshold,
"severity": "high",
"description": f"User requested {count} service tickets (threshold: {threshold})",
})
return alerts
def detect_preauth_failures(events, threshold=10):
"""Detect excessive Kerberos pre-authentication failures (Event ID 4771)."""
user_failures = defaultdict(int)
for ev in events:
if ev["event_id"] == 4771:
user_failures[f"{ev['target_user']}@{ev['target_domain']}"] += 1
alerts = []
for user, count in user_failures.items():
if count >= threshold:
alerts.append({
"detection": "Excessive Pre-Auth Failures",
"mitre_technique": "T1110.003",
"user": user,
"failure_count": count,
"severity": "medium",
"description": f"{count} Kerberos pre-authentication failures detected",
})
return alerts
def generate_splunk_queries():
"""Return SPL queries for Splunk-based PtT detection."""
return {
"rc4_downgrade": (
'index=wineventlog sourcetype="WinEventLog:Security" EventCode=4769 '
'TicketEncryptionType=0x17 | stats count by TargetUserName, IpAddress, ServiceName'
),
"cross_host_reuse": (
'index=wineventlog EventCode=4769 | stats dc(IpAddress) as ip_count, '
'values(IpAddress) as source_ips by TargetUserName | where ip_count > 1'
),
"tgs_volume_anomaly": (
'index=wineventlog EventCode=4769 | stats count by TargetUserName '
'| where count > 50 | sort -count'
),
"preauth_failures": (
'index=wineventlog EventCode=4771 | stats count by TargetUserName, IpAddress '
'| where count > 10'
),
}
def main():
parser = argparse.ArgumentParser(description="Pass-the-Ticket Attack Detector")
parser.add_argument("--evtx-xml", help="Path to exported Security event log XML")
parser.add_argument("--tgs-threshold", type=int, default=50, help="TGS volume alert threshold")
parser.add_argument("--preauth-threshold", type=int, default=10, help="Pre-auth failure threshold")
parser.add_argument("--output", default="ptt_detection_report.json", help="Output report path")
parser.add_argument("--show-splunk", action="store_true", help="Print Splunk SPL queries")
args = parser.parse_args()
if args.show_splunk:
queries = generate_splunk_queries()
for name, spl in queries.items():
print(f"\n--- {name} ---\n{spl}")
return
if not args.evtx_xml:
print("[!] Provide --evtx-xml path to exported Windows Security event log XML")
print("[*] Or use --show-splunk to get Splunk detection queries")
return
events = parse_evtx_xml(args.evtx_xml)
print(f"[+] Parsed {len(events)} Kerberos events (4768/4769/4771)")
rc4_alerts = detect_rc4_downgrade(events)
reuse_alerts = detect_cross_host_ticket_reuse(events)
volume_alerts = detect_anomalous_tgs_volume(events, args.tgs_threshold)
preauth_alerts = detect_preauth_failures(events, args.preauth_threshold)
report = {
"analysis_time": datetime.utcnow().isoformat() + "Z",
"total_kerberos_events": len(events),
"detections": {
"rc4_downgrade": rc4_alerts,
"cross_host_ticket_reuse": reuse_alerts,
"anomalous_tgs_volume": volume_alerts,
"preauth_failures": preauth_alerts,
},
"total_alerts": len(rc4_alerts) + len(reuse_alerts) + len(volume_alerts) + len(preauth_alerts),
"mitre_techniques": ["T1550.003", "T1558.003", "T1110.003"],
"splunk_queries": generate_splunk_queries(),
}
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[+] Alerts: RC4={len(rc4_alerts)}, Reuse={len(reuse_alerts)}, "
f"Volume={len(volume_alerts)}, PreAuth={len(preauth_alerts)}")
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()
@@ -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.
@@ -0,0 +1,44 @@
---
name: implementing-siem-use-case-tuning
description: Tune SIEM detection rules to reduce false positives by analyzing alert volumes, creating whitelists, adjusting thresholds, and measuring detection efficacy metrics in Splunk and Elastic
domain: cybersecurity
subdomain: security-operations
tags:
- siem
- detection-engineering
- false-positive-reduction
- splunk
- elastic
- alert-tuning
- soc
version: "1.0"
author: mahipal
license: Apache-2.0
---
# Implementing SIEM Use Case Tuning
## Overview
SIEM use case tuning reduces alert fatigue by systematically analyzing detection rules for false positive rates, adjusting thresholds based on environmental baselines, creating context-aware whitelists, and measuring detection efficacy through precision/recall metrics. This skill covers tuning workflows for Splunk correlation searches and Elastic detection rules, including statistical baselining, exclusion list management, and alert-to-incident conversion tracking.
## Prerequisites
- Splunk Enterprise/Cloud with ES or Elastic SIEM with detection rules enabled
- Historical alert data (minimum 30 days) for baseline analysis
- Python 3.8+ with `requests` library
- SIEM admin credentials or API tokens
## Steps
1. Export current alert volumes per detection rule from SIEM
2. Calculate false positive rate per rule using analyst disposition data
3. Identify top noise-generating rules by volume and FP rate
4. Build environmental baselines for thresholds (e.g., login counts, process spawns)
5. Create whitelist entries for known-good entities (service accounts, scanners)
6. Adjust rule thresholds using statistical analysis (mean + N standard deviations)
7. Measure tuning impact via before/after precision and alert-to-incident ratio
## Expected Output
JSON report with per-rule tuning recommendations including current FP rate, suggested threshold adjustments, whitelist entries, and projected alert reduction percentages.
@@ -0,0 +1,82 @@
# SIEM Use Case Tuning API Reference
## Splunk Notable Event Export
### Export Notables via SPL
```spl
| inputlookup notable_events
| search status_label IN ("New", "In Progress", "Resolved")
| table rule_name, _time, status_label, src, dest, user, urgency
| rename status_label as disposition, _time as timestamp
| outputlookup alert_export.csv
```
### Splunk ES Correlation Search Tuning
```spl
# Measure FP rate per correlation search over 30 days
| inputlookup notable_events where earliest=-30d
| eval is_fp=if(status_label="Resolved" AND disposition="False Positive", 1, 0)
| stats count as total, sum(is_fp) as fp_count by rule_name
| eval fp_rate=round(fp_count/total, 4)
| sort -fp_rate
```
### Update Correlation Search Threshold
```
POST /servicesNS/nobody/SplunkEnterpriseSecuritySuite/saved/searches/{search_name}
Content-Type: application/x-www-form-urlencoded
search=<updated_spl_with_new_threshold>
```
## Elastic Detection Rule Tuning
### List Detection Rules
```
GET /_security/detection_engine/rules/_find?per_page=100
Authorization: ApiKey <base64_api_key>
```
### Add Exception to Rule
```json
POST /_security/detection_engine/rules/exceptions
{
"rule_id": "rule-uuid",
"name": "Whitelist scanner IPs",
"entries": [
{
"field": "source.ip",
"operator": "is_one_of",
"value": ["10.0.1.50", "10.0.1.51"],
"type": "match_any"
}
]
}
```
### Query Rule Execution Stats (Kibana)
```kql
event.kind: "signal" AND kibana.alert.rule.name: "Brute Force Detection"
| stats count by kibana.alert.workflow_status
```
## Alert Tuning Metrics
| Metric | Formula | Target |
|---|---|---|
| False Positive Rate | FP / (FP + TP) | < 30% |
| Precision | TP / (TP + FP) | > 70% |
| Alert-to-Incident Ratio | Incidents / Total Alerts | > 20% |
| Mean Time to Triage | avg(triage_end - alert_time) | < 15 min |
## CLI Usage
```bash
# Analyze alert CSV export
python agent.py --alert-csv notable_export.csv --output tuning.json
# Adjust FP threshold for whitelist candidates
python agent.py --alert-csv alerts.csv --fp-threshold 0.9 --top-rules 10
# CSV format: rule_name,timestamp,disposition,source,user,severity
```
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""SIEM use case tuning agent - analyzes alert data to reduce false positives and optimize detection rules."""
import json
import csv
import math
import argparse
from collections import defaultdict
from datetime import datetime
def load_alert_data(filepath):
"""Load alert/notable event export (CSV with columns: rule_name, timestamp, disposition, source, user)."""
alerts = []
with open(filepath, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
alerts.append({
"rule_name": row.get("rule_name", row.get("search_name", "")),
"timestamp": row.get("timestamp", row.get("_time", "")),
"disposition": row.get("disposition", row.get("status", "unknown")),
"source": row.get("source", row.get("src", "")),
"user": row.get("user", row.get("dest_user", "")),
"severity": row.get("severity", "medium"),
})
return alerts
def calculate_rule_metrics(alerts):
"""Calculate per-rule alert volume, FP rate, and disposition breakdown."""
rule_stats = defaultdict(lambda: {"total": 0, "true_positive": 0, "false_positive": 0,
"pending": 0, "sources": set(), "users": set()})
for alert in alerts:
rule = alert["rule_name"]
rule_stats[rule]["total"] += 1
disp = alert["disposition"].lower()
if disp in ("true_positive", "tp", "confirmed", "escalated"):
rule_stats[rule]["true_positive"] += 1
elif disp in ("false_positive", "fp", "benign", "closed_fp"):
rule_stats[rule]["false_positive"] += 1
else:
rule_stats[rule]["pending"] += 1
if alert["source"]:
rule_stats[rule]["sources"].add(alert["source"])
if alert["user"]:
rule_stats[rule]["users"].add(alert["user"])
metrics = []
for rule, stats in rule_stats.items():
reviewed = stats["true_positive"] + stats["false_positive"]
fp_rate = stats["false_positive"] / reviewed if reviewed > 0 else 0.0
precision = stats["true_positive"] / reviewed if reviewed > 0 else 0.0
metrics.append({
"rule_name": rule,
"total_alerts": stats["total"],
"true_positives": stats["true_positive"],
"false_positives": stats["false_positive"],
"pending": stats["pending"],
"fp_rate": round(fp_rate, 4),
"precision": round(precision, 4),
"unique_sources": len(stats["sources"]),
"unique_users": len(stats["users"]),
"top_sources": list(stats["sources"])[:10],
})
return sorted(metrics, key=lambda x: x["fp_rate"], reverse=True)
def identify_whitelist_candidates(alerts, fp_threshold=0.8):
"""Identify source/user pairs that consistently trigger FPs for a given rule."""
rule_source_stats = defaultdict(lambda: defaultdict(lambda: {"tp": 0, "fp": 0}))
for alert in alerts:
disp = alert["disposition"].lower()
key = alert["source"] or alert["user"]
if not key:
continue
if disp in ("false_positive", "fp", "benign", "closed_fp"):
rule_source_stats[alert["rule_name"]][key]["fp"] += 1
elif disp in ("true_positive", "tp", "confirmed", "escalated"):
rule_source_stats[alert["rule_name"]][key]["tp"] += 1
candidates = []
for rule, sources in rule_source_stats.items():
for source, counts in sources.items():
total = counts["tp"] + counts["fp"]
if total >= 3 and counts["fp"] / total >= fp_threshold:
candidates.append({
"rule_name": rule,
"entity": source,
"fp_count": counts["fp"],
"tp_count": counts["tp"],
"fp_ratio": round(counts["fp"] / total, 4),
"recommendation": "Add to whitelist" if counts["tp"] == 0 else "Review before whitelisting",
})
return sorted(candidates, key=lambda x: x["fp_count"], reverse=True)
def compute_threshold_recommendation(alerts, rule_name, field="total"):
"""Compute statistical threshold for a rule based on hourly alert distribution."""
hourly_counts = defaultdict(int)
for alert in alerts:
if alert["rule_name"] != rule_name:
continue
try:
dt = datetime.fromisoformat(alert["timestamp"].replace("Z", "+00:00"))
hourly_counts[dt.strftime("%Y-%m-%d %H")] += 1
except (ValueError, AttributeError):
continue
if not hourly_counts:
return None
values = list(hourly_counts.values())
mean = sum(values) / len(values)
variance = sum((x - mean) ** 2 for x in values) / len(values)
stdev = math.sqrt(variance)
return {
"rule_name": rule_name,
"hourly_mean": round(mean, 2),
"hourly_stdev": round(stdev, 2),
"suggested_threshold_2sd": round(mean + 2 * stdev, 0),
"suggested_threshold_3sd": round(mean + 3 * stdev, 0),
"sample_hours": len(hourly_counts),
}
def generate_tuning_report(metrics, whitelist, thresholds):
"""Generate comprehensive tuning report with recommendations."""
high_fp_rules = [m for m in metrics if m["fp_rate"] > 0.7]
medium_fp_rules = [m for m in metrics if 0.3 < m["fp_rate"] <= 0.7]
total_alerts = sum(m["total_alerts"] for m in metrics)
total_fp = sum(m["false_positives"] for m in metrics)
projected_reduction = sum(w["fp_count"] for w in whitelist)
return {
"analysis_time": datetime.utcnow().isoformat() + "Z",
"summary": {
"total_rules_analyzed": len(metrics),
"total_alerts": total_alerts,
"total_false_positives": total_fp,
"overall_fp_rate": round(total_fp / total_alerts, 4) if total_alerts else 0,
"high_fp_rules": len(high_fp_rules),
"whitelist_candidates": len(whitelist),
"projected_alert_reduction": projected_reduction,
},
"high_fp_rules": high_fp_rules,
"medium_fp_rules": medium_fp_rules,
"whitelist_recommendations": whitelist[:20],
"threshold_recommendations": thresholds,
"actions": [
{"priority": "high", "action": f"Disable or rewrite {len(high_fp_rules)} rules with FP rate > 70%"},
{"priority": "medium", "action": f"Add {len(whitelist)} whitelist entries to reduce {projected_reduction} FP alerts"},
{"priority": "low", "action": f"Review {len(medium_fp_rules)} rules with FP rate 30-70%"},
],
}
def main():
parser = argparse.ArgumentParser(description="SIEM Use Case Tuning Agent")
parser.add_argument("--alert-csv", required=True, help="CSV export of SIEM alerts with disposition data")
parser.add_argument("--fp-threshold", type=float, default=0.8, help="FP ratio threshold for whitelist candidates")
parser.add_argument("--top-rules", type=int, default=5, help="Number of top rules to compute thresholds for")
parser.add_argument("--output", default="tuning_report.json", help="Output report path")
args = parser.parse_args()
alerts = load_alert_data(args.alert_csv)
print(f"[+] Loaded {len(alerts)} alerts from {args.alert_csv}")
metrics = calculate_rule_metrics(alerts)
print(f"[+] Analyzed {len(metrics)} unique detection rules")
whitelist = identify_whitelist_candidates(alerts, args.fp_threshold)
print(f"[+] Found {len(whitelist)} whitelist candidates (FP ratio >= {args.fp_threshold})")
thresholds = []
for m in metrics[:args.top_rules]:
t = compute_threshold_recommendation(alerts, m["rule_name"])
if t:
thresholds.append(t)
report = generate_tuning_report(metrics, whitelist, thresholds)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[+] Tuning report saved to {args.output}")
print(f"[+] Overall FP rate: {report['summary']['overall_fp_rate']:.1%}")
print(f"[+] Projected alert reduction from whitelisting: {report['summary']['projected_alert_reduction']}")
if __name__ == "__main__":
main()