Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/performing-threat-hunting-with-yara-rules/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-13 | ✗→✓ | ▲ Improved | — | — |
Scan files, directories, and memory dumps using YARA rules to identify malware families, suspicious patterns, and IOC matches.
Do not use for real-time endpoint protection (use EDR agents instead); YARA scanning is best suited for batch hunting, triage, and post-collection analysis where scan latency is acceptable.
apt install yara on Debian/Ubuntu, brew install yara on macOS)yara-python (pip install yara-python)yarGen for automated rule generation (git clone https://github.com/Neo23x0/yarGen)pefile for PE header analysis, malduck for memory carvingbash# Linux sudo apt update && sudo apt install -y yara # Python bindings pip install yara-python # Verify installation yara --version python3 -c "import yara; print(yara.YARA_VERSION)"
Create rules that match on strings, hex patterns, and file metadata:
yara// File: rules/emotet_loader.yar rule Emotet_Loader_2026 { meta: author = "Threat Intel Team" description = "Detects Emotet first-stage loader DLL" date = "2026-01-20" reference = "https://attack.mitre.org/software/S0367/" mitre_attack = "T1059.001, T1055.001" severity = "critical" strings: // Emotet export function name patterns $export1 = "DllRegisterServer" ascii $export2 = "RunDLL" ascii nocase // Obfuscated string decryption routine $decrypt_loop = { 8B 45 ?? 33 45 ?? 89 45 ?? 8B 4D ?? 03 4D ?? } // PowerShell download cradle in embedded script $ps_cradle = /powershell[^\n]{0,50}-e(nc|ncodedcommand)/i // Known C2 URI patterns $uri1 = "/wp-content/uploads/" ascii $uri2 = "/wp-admin/css/" ascii $uri3 = "/wp-includes/" ascii // PE characteristics $mz = "MZ" at 0 condition: $mz and filesize < 2MB and ( ($export1 and $decrypt_loop) or ($ps_cradle and any of ($uri*)) or (2 of ($uri*) and $decrypt_loop) ) }
Use YARA modules for PE header inspection and math-based entropy checks:
yaraimport "pe" import "math" rule Suspicious_Packed_Executable { meta: author = "Threat Hunting Team" description = "Detects PE files with high entropy sections indicating packing or encryption" severity = "medium" condition: pe.is_pe and pe.number_of_sections > 0 and for any section in pe.sections : ( math.entropy(section.offset, section.size) > 7.2 and section.size > 1024 ) and pe.imports("kernel32.dll", "VirtualAlloc") and pe.imports("kernel32.dll", "VirtualProtect") } rule Suspicious_UPX_Modified { meta: description = "Detects UPX-packed binaries with tampered section names" severity = "medium" strings: $upx_magic = { 55 50 58 21 } // UPX! condition: pe.is_pe and $upx_magic and not ( pe.sections[0].name == "UPX0" and pe.sections[1].name == "UPX1" ) }
pythonimport yara import os import json from datetime import datetime from pathlib import Path def compile_rules(rule_paths): """Compile YARA rules from one or more .yar files.""" rule_files = {} for i, path in enumerate(rule_paths): namespace = Path(path).stem rule_files[namespace] = path return yara.compile(filepaths=rule_files) def scan_directory(rules, target_dir, recursive=True): """Scan a directory for matches and return structured results.""" results = [] scan_count = 0 error_count = 0 for root, dirs, files in os.walk(target_dir): for filename in files: filepath = os.path.join(root, filename) scan_count += 1 try: matches = rules.match(filepath, timeout=60) if matches: for match in matches: result = { "file": filepath, "rule": match.rule, "namespace": match.namespace, "tags": match.tags, "meta": match.meta, "strings": [], "scan_time": datetime.utcnow().isoformat() } for offset, identifier, data in match.strings: result["strings"].append({ "offset": hex(offset), "identifier": identifier, "data": data.hex() if isinstance(data, bytes) else data }) results.append(result) print(f" MATCH: {match.rule} -> {filepath}") except yara.TimeoutError: error_count += 1 print(f" TIMEOUT scanning {filepath}") except yara.Error as e: error_count += 1 if not recursive: break print(f"\nScan complete: {scan_count} files scanned, " f"{len(results)} matches, {error_count} errors") return results # Compile and scan rules = compile_rules([ "rules/emotet_loader.yar", "rules/suspicious_packed.yar" ]) matches = scan_directory(rules, "/mnt/evidence/collected_samples/") # Export results with open("yara_scan_results.json", "w") as f: json.dump(matches, f, indent=2)
Hunt for in-memory indicators that only exist in running processes:
pythonimport yara def scan_memory_dump(rules, dump_path): """Scan a process memory dump for YARA matches.""" matches = rules.match(dump_path, timeout=120) for match in matches: print(f"Rule: {match.rule}") print(f" Severity: {match.meta.get('severity', 'unknown')}") for offset, identifier, data in match.strings: # Show context around the match print(f" String {identifier} at offset {hex(offset)}") if len(data) <= 64: print(f" Data: {data.hex()}") return matches # Rules targeting in-memory artifacts memory_rules = yara.compile(source=""" rule Cobalt_Strike_Beacon_Memory { meta: description = "Detects Cobalt Strike beacon in process memory" severity = "critical" strings: $config_start = { 2E 2F 2E 2F 2E 2C } $sleep_mask = { 48 8B 44 24 ?? 48 89 44 24 ?? 48 8B 44 24 } $named_pipe = "\\\\\\\\.\\\\pipe\\\\msagent_" ascii $watermark = { 00 00 00 00 00 00 ?? ?? 00 00 } condition: 2 of them } """) scan_memory_dump(memory_rules, "/mnt/evidence/lsass_dump.dmp")
Use yarGen to create rules from malware samples by extracting unique strings:
bash# Clone and set up yarGen git clone https://github.com/Neo23x0/yarGen.git cd yarGen pip install -r requirements.txt # Download the string databases (run once) python3 yarGen.py --update # Generate rules from a directory of malware samples python3 yarGen.py \ -m /mnt/evidence/malware_samples/ \ -o generated_rules.yar \ --excludegood \ -p "AutoGen" \ -a "Threat Hunting Team" \ --score 50 # Generate rules for a single sample with maximum detail python3 yarGen.py \ -m /mnt/evidence/malware_samples/suspicious.exe \ -o single_sample_rule.yar \ --opcodes \ --debug
Download and combine rules from public threat intelligence repositories:
bash# Clone Florian Roth's signature-base (large community rule set) git clone https://github.com/Neo23x0/signature-base.git # Clone YARA-Rules community repository git clone https://github.com/Yara-Rules/rules.git yara-community-rules # Clone ReversingLabs YARA rules git clone https://github.com/reversinglabs/reversinglabs-yara-rules.git
pythonimport yara from pathlib import Path def load_rule_directory(rule_dir, extensions=(".yar", ".yara")): """Load all YARA rules from a directory tree.""" rule_files = {} for ext in extensions: for rule_file in Path(rule_dir).rglob(f"*{ext}"): namespace = rule_file.stem # Avoid namespace collisions if namespace in rule_files: namespace = f"{rule_file.parent.name}_{namespace}" rule_files[namespace] = str(rule_file) print(f"Loading {len(rule_files)} rule files from {rule_dir}") try: compiled = yara.compile(filepaths=rule_files) return compiled except yara.SyntaxError as e: print(f"Syntax error in rules: {e}") # Fall back to loading rules one by one, skipping broken ones valid_rules = {} for ns, path in rule_files.items(): try: yara.compile(filepath=path) valid_rules[ns] = path except yara.SyntaxError: print(f" Skipping broken rule: {path}") return yara.compile(filepaths=valid_rules) # Load and scan with community rules community_rules = load_rule_directory("signature-base/yara/") matches = community_rules.match("/mnt/evidence/suspicious_file.exe", timeout=120) for m in matches: print(f"Matched: {m.rule} (namespace: {m.namespace})")
Automate scanning of new files as they arrive using filesystem monitoring:
pythonimport yara import time import json import hashlib from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class YaraHuntingHandler(FileSystemEventHandler): def __init__(self, rules, alert_file="yara_alerts.jsonl"): self.rules = rules self.alert_file = alert_file self.scanned_hashes = set() def on_created(self, event): if event.is_directory: return self._scan_file(event.src_path) def _scan_file(self, filepath): # Deduplicate by file hash try: file_hash = hashlib.sha256(Path(filepath).read_bytes()).hexdigest() except (PermissionError, FileNotFoundError): return if file_hash in self.scanned_hashes: return self.scanned_hashes.add(file_hash) matches = self.rules.match(filepath, timeout=60) if matches: alert = { "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "file": filepath, "sha256": file_hash, "matches": [ {"rule": m.rule, "severity": m.meta.get("severity", "unknown")} for m in matches ] } with open(self.alert_file, "a") as f: f.write(json.dumps(alert) + "\n") print(f"ALERT: {filepath} matched {len(matches)} rules") # Set up continuous monitoring rules = yara.compile(filepaths={"hunting": "rules/all_hunting_rules.yar"}) handler = YaraHuntingHandler(rules) observer = Observer() observer.schedule(handler, path="/mnt/quarantine/", recursive=True) observer.start() print("YARA hunting pipeline active. Monitoring /mnt/quarantine/ ...")
yara -w rules/*.yar /dev/null| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.