mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
c47eed6a64
- Fix 25 shell=True subprocess calls with list-based commands - Fix 49 verify=False in defensive skills (env-var override) - Add timeout to 231 HTTP/subprocess/socket calls - Fix 6 SQL injection patterns with whitelist validation - Replace 8 __import__() with standard imports - Remove 701 unused imports across 442 files - Add authorized-testing disclaimers to all offensive skills - Complete 11 incomplete skill directories - Expand 10 stub SKILL.md files with full content - Fix 2 YAML parse errors in frontmatter - Fix 5 pre-existing syntax errors - Convert 22 hardcoded paths/ports to environment variables - Back up 21 redundant skill pairs to .bak - Fix 2 global declaration errors - 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE) - 0 compile errors across all 724 agent.py files
172 lines
6.5 KiB
Python
172 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for analyzing NTFS slack space and file system artifacts."""
|
|
|
|
import os
|
|
import json
|
|
import struct
|
|
import argparse
|
|
import subprocess
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_mft_with_analyzeMFT(mft_path, output_csv):
|
|
"""Parse MFT using analyzeMFT and return deleted/timestomped files."""
|
|
cmd = ["analyzeMFT.py", "-f", mft_path, "-o", output_csv, "-c"]
|
|
subprocess.run(cmd, check=True, timeout=120)
|
|
return output_csv
|
|
|
|
|
|
def extract_slack_space(image_path, offset, output_path):
|
|
"""Extract slack space from a disk image using blkls from The Sleuth Kit."""
|
|
cmd = ["blkls", "-s", "-o", str(offset), image_path]
|
|
with open(output_path, "wb") as out:
|
|
subprocess.run(cmd, stdout=out, check=True, timeout=120)
|
|
return output_path
|
|
|
|
|
|
def search_slack_keywords(slack_path, keywords=None):
|
|
"""Search extracted slack space for forensic keywords."""
|
|
if keywords is None:
|
|
keywords = ["password", "secret", "confidential", "credit card", "ssn"]
|
|
hits = []
|
|
with open(slack_path, "rb") as f:
|
|
data = f.read()
|
|
for kw in keywords:
|
|
kw_bytes = kw.encode("utf-8")
|
|
start = 0
|
|
while True:
|
|
idx = data.find(kw_bytes, start)
|
|
if idx == -1:
|
|
break
|
|
context = data[max(0, idx - 20):idx + len(kw_bytes) + 20]
|
|
hits.append({
|
|
"keyword": kw,
|
|
"offset": idx,
|
|
"context": context.decode("utf-8", errors="replace"),
|
|
})
|
|
start = idx + 1
|
|
return hits
|
|
|
|
|
|
def parse_usn_journal(usn_path):
|
|
"""Parse NTFS USN Change Journal ($UsnJrnl:$J) records."""
|
|
REASON_FLAGS = {
|
|
0x01: "DATA_OVERWRITE", 0x02: "DATA_EXTEND", 0x04: "DATA_TRUNCATION",
|
|
0x100: "FILE_CREATE", 0x200: "FILE_DELETE", 0x400: "EA_CHANGE",
|
|
0x800: "SECURITY_CHANGE", 0x1000: "RENAME_OLD_NAME",
|
|
0x2000: "RENAME_NEW_NAME", 0x80000000: "CLOSE",
|
|
}
|
|
records = []
|
|
with open(usn_path, "rb") as f:
|
|
data = f.read()
|
|
offset = 0
|
|
while offset < len(data) - 8:
|
|
rec_len = struct.unpack_from("<I", data, offset)[0]
|
|
if rec_len < 56 or rec_len > 65536 or offset + rec_len > len(data):
|
|
offset += 8
|
|
continue
|
|
major = struct.unpack_from("<H", data, offset + 4)[0]
|
|
if major != 2:
|
|
offset += max(rec_len, 8)
|
|
continue
|
|
mft_ref = struct.unpack_from("<Q", data, offset + 8)[0] & 0xFFFFFFFFFFFF
|
|
timestamp = struct.unpack_from("<Q", data, offset + 32)[0]
|
|
reason = struct.unpack_from("<I", data, offset + 40)[0]
|
|
fn_len = struct.unpack_from("<H", data, offset + 56)[0]
|
|
fn_off = struct.unpack_from("<H", data, offset + 58)[0]
|
|
name = data[offset + fn_off:offset + fn_off + fn_len].decode("utf-16-le", errors="ignore")
|
|
ts = datetime(1601, 1, 1) + timedelta(microseconds=timestamp // 10)
|
|
reasons = [desc for flag, desc in REASON_FLAGS.items() if reason & flag]
|
|
records.append({
|
|
"timestamp": ts.strftime("%Y-%m-%d %H:%M:%S"),
|
|
"filename": name,
|
|
"mft_entry": mft_ref,
|
|
"reasons": "|".join(reasons),
|
|
})
|
|
offset += rec_len
|
|
return records
|
|
|
|
|
|
def find_ads_in_image(image_path, offset):
|
|
"""List Alternate Data Streams using fls from The Sleuth Kit."""
|
|
cmd = ["fls", "-r", "-o", str(offset), image_path]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
ads_entries = [line for line in result.stdout.splitlines() if ":" in line]
|
|
return ads_entries
|
|
|
|
|
|
def detect_timestomping(mft_csv_path):
|
|
"""Detect timestomping by comparing $SI and $FN timestamps in MFT CSV output."""
|
|
import csv
|
|
suspicious = []
|
|
with open(mft_csv_path, "r", errors="ignore") as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
si_mod = row.get("SI_Modified", "")
|
|
fn_mod = row.get("FN_Modified", "")
|
|
if si_mod and fn_mod and si_mod != fn_mod:
|
|
suspicious.append({
|
|
"filename": row.get("Filename", ""),
|
|
"si_modified": si_mod,
|
|
"fn_modified": fn_mod,
|
|
})
|
|
return suspicious
|
|
|
|
|
|
def generate_report(results_data, case_id):
|
|
"""Generate a structured forensic report."""
|
|
report = {
|
|
"report_type": "File System Artifact Analysis",
|
|
"case_id": case_id,
|
|
"generated_at": datetime.utcnow().isoformat() + "Z",
|
|
"findings": results_data,
|
|
}
|
|
return json.dumps(report, indent=2, default=str)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="NTFS File System Artifact Analysis Agent")
|
|
parser.add_argument("--image", required=True, help="Path to forensic disk image")
|
|
parser.add_argument("--offset", type=int, default=2048, help="Partition offset in sectors")
|
|
parser.add_argument("--case-id", default="CASE-001", help="Case identifier")
|
|
parser.add_argument("--output-dir", default="./analysis", help="Output directory")
|
|
parser.add_argument("--action", choices=[
|
|
"extract_slack", "parse_usn", "find_ads", "search_slack",
|
|
"parse_mft", "detect_timestomping", "full_analysis"
|
|
], default="full_analysis")
|
|
parser.add_argument("--mft-path", help="Path to extracted $MFT file")
|
|
parser.add_argument("--usn-path", help="Path to extracted $UsnJrnl:$J file")
|
|
args = parser.parse_args()
|
|
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
findings = {}
|
|
|
|
if args.action in ("extract_slack", "full_analysis"):
|
|
slack_path = os.path.join(args.output_dir, "slack_space.raw")
|
|
extract_slack_space(args.image, args.offset, slack_path)
|
|
hits = search_slack_keywords(slack_path)
|
|
findings["slack_keywords"] = hits
|
|
print(f"[+] Slack space: {len(hits)} keyword hits found")
|
|
|
|
if args.action in ("parse_usn", "full_analysis") and args.usn_path:
|
|
records = parse_usn_journal(args.usn_path)
|
|
deletions = [r for r in records if "FILE_DELETE" in r["reasons"]]
|
|
findings["usn_journal"] = {
|
|
"total_records": len(records),
|
|
"deletions": len(deletions),
|
|
"recent_deletions": deletions[-20:],
|
|
}
|
|
print(f"[+] USN Journal: {len(records)} records, {len(deletions)} deletions")
|
|
|
|
if args.action in ("find_ads", "full_analysis"):
|
|
ads = find_ads_in_image(args.image, args.offset)
|
|
findings["alternate_data_streams"] = ads
|
|
print(f"[+] Alternate Data Streams: {len(ads)} found")
|
|
|
|
print(generate_report(findings, args.case_id))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|