Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- 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
2026-03-19 13:26:49 +01:00

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()