86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Safe sample-handling helpers: hashing, provenance records, and IOC defanging.
|
|
|
|
Operates only on file bytes and text — it never executes a sample. Use it to compute a
|
|
sample's identity, emit a provenance record, and defang indicators before they leave the lab.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as _dt
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_DEFANG_SCHEME = re.compile(r"\bhttp(s?)://", re.I)
|
|
_DEFANG_DOT = re.compile(r"\.(?=[A-Za-z0-9])")
|
|
_IPV4 = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
|
|
|
|
def compute_hashes(filepath: str | Path) -> dict:
|
|
data = Path(filepath).read_bytes()
|
|
return {
|
|
"md5": hashlib.md5(data).hexdigest(), # noqa: S324 - identification only
|
|
"sha1": hashlib.sha1(data).hexdigest(), # noqa: S324
|
|
"sha256": hashlib.sha256(data).hexdigest(),
|
|
"size": len(data),
|
|
}
|
|
|
|
|
|
def make_record(filepath: str | Path, source: str, case: str) -> dict:
|
|
rec = {
|
|
"case": case,
|
|
"source": source,
|
|
"filename": Path(filepath).name,
|
|
"recorded_at": _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
}
|
|
rec.update(compute_hashes(filepath))
|
|
return rec
|
|
|
|
|
|
def defang(text: str) -> str:
|
|
"""Render URLs/IPs non-clickable for safe sharing."""
|
|
def _ip(m: re.Match) -> str:
|
|
return m.group(0).replace(".", "[.]")
|
|
text = _IPV4.sub(_ip, text)
|
|
text = _DEFANG_SCHEME.sub(lambda m: "hxxp" + ("s" if m.group(1) else "") + "://", text)
|
|
# bracket the dot in domains within URLs only (lines containing hxxp)
|
|
out_lines = []
|
|
for line in text.splitlines():
|
|
if "hxxp" in line:
|
|
line = _DEFANG_DOT.sub("[.]", line)
|
|
out_lines.append(line)
|
|
return "\n".join(out_lines)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Safe sample-handling helpers")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
p_hash = sub.add_parser("hash", help="compute file hashes")
|
|
p_hash.add_argument("file")
|
|
|
|
p_rec = sub.add_parser("record", help="emit a provenance JSON record")
|
|
p_rec.add_argument("file")
|
|
p_rec.add_argument("--source", required=True)
|
|
p_rec.add_argument("--case", required=True)
|
|
|
|
p_def = sub.add_parser("defang", help="defang URLs/IPs in a text file")
|
|
p_def.add_argument("file")
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.cmd == "hash":
|
|
print(json.dumps(compute_hashes(args.file), indent=2))
|
|
elif args.cmd == "record":
|
|
print(json.dumps(make_record(args.file, args.source, args.case), indent=2))
|
|
elif args.cmd == "defang":
|
|
print(defang(Path(args.file).read_text(encoding="utf-8", errors="replace")))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|