80 lines
2.7 KiB
Python
Executable File
80 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Scan files and directories with YARA rules and summarize matches.
|
|
|
|
Wraps ``yara-python`` to compile a rules path and report matched rule names, tags, and string
|
|
offsets. Reads files as data; it never executes a sample.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def compile_rules(rules_path: str):
|
|
import yara # type: ignore
|
|
p = Path(rules_path)
|
|
if p.is_dir():
|
|
filepaths = {f.stem: str(f) for f in p.rglob("*.yar")}
|
|
filepaths.update({f.stem: str(f) for f in p.rglob("*.yara")})
|
|
if not filepaths:
|
|
raise FileNotFoundError(f"no .yar/.yara files under {rules_path}")
|
|
return yara.compile(filepaths=filepaths)
|
|
return yara.compile(filepath=str(p))
|
|
|
|
|
|
def scan_file(rules, filepath: str) -> dict:
|
|
matches = rules.match(filepath, timeout=60)
|
|
return {
|
|
"file": filepath,
|
|
"matches": [{
|
|
"rule": m.rule,
|
|
"tags": list(m.tags),
|
|
"strings": [
|
|
{"id": s.identifier, "offset": inst.offset}
|
|
for s in m.strings for inst in s.instances
|
|
] if hasattr(m, "strings") else [],
|
|
} for m in matches],
|
|
}
|
|
|
|
|
|
def scan_path(rules_path: str, target: str, recursive: bool) -> list[dict]:
|
|
rules = compile_rules(rules_path)
|
|
target_p = Path(target)
|
|
files = ([f for f in target_p.rglob("*") if f.is_file()] if recursive and target_p.is_dir()
|
|
else [f for f in target_p.iterdir() if f.is_file()] if target_p.is_dir()
|
|
else [target_p])
|
|
return [scan_file(rules, str(f)) for f in files]
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Scan with YARA")
|
|
parser.add_argument("rules", help="rule file or directory")
|
|
parser.add_argument("target", help="file or directory to scan")
|
|
parser.add_argument("--recursive", action="store_true")
|
|
parser.add_argument("--summary", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
|
|
try:
|
|
results = scan_path(args.rules, args.target, args.recursive)
|
|
except ImportError:
|
|
print("yara-python not installed: pip install yara-python", file=sys.stderr)
|
|
return 2
|
|
|
|
if args.summary:
|
|
hit = [r for r in results if r["matches"]]
|
|
print(json.dumps({
|
|
"scanned": len(results),
|
|
"with_matches": len(hit),
|
|
"files": [{"file": r["file"],
|
|
"rules": [m["rule"] for m in r["matches"]]} for r in hit],
|
|
}, indent=2))
|
|
else:
|
|
print(json.dumps(results, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|