_ _ ___ ____ _ _ ____
| | | |/ _ \| _ \| | | / ___|
| |_| | | | | |_) | | | \___ \
| _ | |_| | _ <| |_| |___) |
|_| |_|\___/|_| \_\\___/|____/
🔱 the all-seeing eye — IOC enrichment & PE static analysis
Drop it anywhere. Point it at anything. Get a verdict.
Download horus.exe · View Source · Quick Start · Usage Guide
Horus is a single-file Windows security triage tool — no Python, no installers, no dependencies. It links only against APIs that already ship with Windows (BCrypt for hashing, WinHTTP for HTTPS) and compiles down to one self-contained .exe.
Ships in three flavours from one engine — pick whichever fits your box:
| Binary | What it is |
|---|---|
horus-cli.exe |
the pure command line, zero third-party dependencies |
horus-gui.exe |
drag a file on it and get a glass-console report in your browser |
horus.exe |
both — CLI by default, --gui (or a double-click) opens the GUI |
Hand it anything and it figures out what to do:
| You give it | What Horus does |
|---|---|
| A file path | Hashes and inventories any file; valid PEs also get parsing, signature checks, capabilities, ATT&CK, UEBA, and a scored verdict |
| An MD5, SHA-1, or SHA-256 | Classifies it locally; --enrich queries VirusTotal |
| An IP address | Classifies it locally; --enrich queries VirusTotal + AbuseIPDB in parallel |
| A domain or URL | Classifies it locally; --enrich queries VirusTotal |
| An email address | Recognises the indicator and reports UNKNOWN unless a configured provider supports it |
| A samples directory | --case creates or refreshes a persistent, portable analysis workspace |
Direct file analysis needs no mode flag: horus.exe sample.exe runs immediately. Network access is always opt-in through --enrich.
- Case change feed — each refresh records files added, modified, restored, newly missing, or removed by
--prune, plus changed assessments. Inspect the current run incase.json,changes.csv, or the offline dashboard. - Machine-readable PE diff —
horus --diff a.exe b.exe --jsonreturns structured comparison data. Incomplete or non-PE comparisons setanalysis_statustounknownand exit 3. - Stricter input handling — additional malformed-PE bounds checks and rejection of conflicting
--gui/--htmlcombinations with scan, diff, or each other.
- Persistent Case Mode — analyze up to 5,000 files with incremental refresh,
--verify,--rebuild,--prune, portable relative paths, and atomic state. - Explainable similarity clusters — exact SHA-256 duplicates for every file; valid PEs can be related by TLSH, imphash, Rich hash, and section layout. Results are similarity clusters, not definitive malware-family attribution.
- Offline case dashboard — searchable HTML plus
case.json,samples.csv,clusters.csv, andiocs.csv; no server or CDN is required. - Safer verdict semantics — non-PE inventory and incomplete analysis now return
UNKNOWNwith a nullable score instead of a falseCLEAN. - Opt-in enrichment — only hashes and extracted indicators are sent when
--enrichis present. Samples are never uploaded. - Hardened I/O — Base64 report embedding, safe DOM rendering, strict JSON parsing, 16 MiB HTTP responses, atomic writes, and CSV formula protection.
- Windows 10/11 x64 release checks — centralized version metadata, SHA-256 checksums, and GitHub artifact attestations for all release assets.
- Glass console (GUI) — a self-contained, glassmorphism report that renders in the browser: a risk ring, a live force-directed threat graph (sample → capabilities → ATT&CK → embedded IOCs), and a MITRE ATT&CK heatmap that lights up the techniques it found. No CDN or browser runtime dependency; network enrichment remains opt-in.
horus-gui.exe,horus --gui, orhorus --html report.htmlto export one. - Batch / folder scan —
horus --scan <dir>walks a directory, scores every file, prints a worst-first table, and dumps--csv/--json-outfor a ticket. - PE diff —
horus --diff a.exe b.exe: imphash, rich-hash, section-entropy drift, and TLSH distance side by side. Compare related samples without uploading either file. - Config file + intel cache — drop keys and a
cache_ttl_hoursin~/.horus.json; repeat VT/AbuseIPDB lookups come back instantly and don't burn quota. - Clipboard input —
horus --clipgrabs whatever indicator you already copied.
- Similarity hashing — TLSH and ssdeep digests (byte-for-byte compatible with the reference tools), so you can compare related samples instead of only scoring them in isolation. Plus the authentihash (matches signed-malware campaigns) and the rich hash (MSVC build fingerprint).
- Real Authenticode verification — not just "a certificate is present." Runs the signature through
WinVerifyTrust(embedded and catalog), pulls the signer's common name out of the PKCS#7, and flags anything that carries a certificate but no longer verifies as a tamper/forgery signal. - MITRE ATT&CK mapping — every capability finding is tagged with its technique ID (
T1055,T1056.001,T1071…) in both the console report and the JSON. - More of the PE — Rich header decode, CodeView PDB path + GUID (attribution goldmine), TLS callbacks, exports, appended overlay (size + entropy), and .NET/CLR detection.
Illustrative output only. The file, indicators, provider result, and verdict below are fictional; no scan or provider lookup produced this example.
_ _ ___ ____ _ _ ____
| | | |/ _ \| _ \| | | / ___|
| |_| | | | | |_) | | | \___ \
| _ | |_| | _ <| |_| |___) |
|_| |_|\___/|_| \_\\___/|____/
🔱 the all-seeing eye — IOC enrichment & PE static analysis
$ horus ransomware_sample.exe --enrich
FILE ────────────────────────────────────────────────────────────
path ransomware_sample.exe
size 102,912 bytes
MD5 a3f5d1e2c7b849d60e14a2b8f93c5d17
SHA-1 4b2e8a91c3d056f7e820b4a19c6d3f7e2b085c40
SHA-256 470735e5b9545078198d440e67abbf86...dad3f57d
PE HEADER ───────────────────────────────────────────────────────
type executable
bitness 64-bit (PE32+)
machine x64 (AMD64)
subsystem Windows Console
compiled 2018-11-06 22:28:12 UTC (suspicious)
entry RVA 0x12a0
imphash 9a4f2c8d1e7b3056a82c914d6e0f5b17
SECTIONS ────────────────────────────────────────────────────────
.text r-x H=6.21 [##########........]
.rdata r-- H=4.87 [########..........]
.data rw- H=4.12 [#######...........]
UPX0 rwx H=7.94 [##################] RWX!
UPX1 rwx H=7.81 [#################.] RWX!
IMPORTS (4 DLLs, 11 functions) ─────────────────────────────────
KERNEL32.DLL (6)
ADVAPI32.DLL (3)
WS2_32.DLL (2)
CAPABILITIES ────────────────────────────────────────────────────
[Cryptography] encrypts data — benign, or ransomware/payload packing
CryptAcquireContextA CryptGenKey CryptEncrypt
[Network Activity] opens sockets or fetches remote content
WSAStartup connect
[Persistence] writes autostart keys or installs itself to survive reboot
RegSetValueExA RegCreateKeyExA
! Encryption combined with network I/O (exfil / ransomware pattern) +12
! High-entropy section 'UPX0' (H=7.94) — likely packed/encrypted +12
! Section 'UPX0' is writable AND executable (RWX) +18
! Section 'UPX1' is writable AND executable (RWX) +18
! Known packer section name 'UPX0' +16
EMBEDDED INDICATORS (from strings) ────────────────────────────
url http://192.0.2.42:8080/gate.php
ip 192.0.2.42
tokens cmd.exe schtasks powershell
THREAT INTELLIGENCE ─────────────────────────────────────────────
VirusTotal 61/72 — Ransom.FileCryptor.Generic (Malicious)
VERDICT ─────────────────────────────────────────────────────────
[ LIKELY MALICIOUS ] risk 98/100
static analysis: LIKELY MALICIOUS (86)
virustotal: 61/72 engines
Horus doesn't just list imports — it interprets them. The scoring engine maps ~110 Windows APIs to the capabilities they imply, then weights the combinations that matter:
| Capability | What it means | Score |
|---|---|---|
| Process Injection | VirtualAllocEx, WriteProcessMemory, CreateRemoteThread |
+22 |
| Input Capture (Keylogger) | SetWindowsHookEx, GetAsyncKeyState, RegisterRawInputDevices |
+20 |
| Persistence | RegSetValueEx, CreateService |
+14 |
| Privilege Escalation | AdjustTokenPrivileges, DuplicateTokenEx, ImpersonateLoggedOnUser |
+14 |
| Screen Capture | BitBlt, GetDC, CreateCompatibleBitmap |
+12 |
| Defense Evasion | DeleteFile, SetFileAttributes, EventWrite |
+12 |
| Networking | Winsock + WinHTTP + WinInet | +8 |
| Cryptography | CryptEncrypt, BCryptEncrypt, CryptGenKey |
+8 |
| RWX Section | Writable and executable section | +18 each |
| High-entropy section | Shannon entropy ≥ 7.2 bits (packed/encrypted) | +12 each |
| Known packer name | UPX, Themida, VMProtect, .enigma, .vmp… | +16 |
| Combination | Why it matters | Bonus |
|---|---|---|
| Injection + API resolution | Hiding injected code behind dynamic imports | +15 |
| Keylogger + networking | Classic keylogger-and-exfil architecture | +15 |
| Crypto + networking | Ransomware or C2 exfiltration pattern | +12 |
Common dual-use APIs (LoadLibrary, CreateProcess, GetProcAddress) are shown but don't add to the score alone — they show up in ordinary binaries all the time. Only rare primitives and dangerous combinations move the needle.
| Score | Verdict | Meaning |
|---|---|---|
| 0–9 | CLEAN | Nothing concerning found |
| 10–29 | LOW RISK | Suspicious imports but no strong signals |
| 30–59 | SUSPICIOUS | Multiple concerning capabilities or combos |
| 60–100 | LIKELY MALICIOUS | Strong evidence of malicious intent |
| — | UNKNOWN | No scoreable evidence, or analysis was skipped/incomplete |
Grab horus.exe from Releases and put it somewhere on your PATH — or just run it from any folder.
# Run from wherever you downloaded it
.\horus.exe suspicious.exeHorus works without keys — it still does full local PE analysis and hashing. Keys configure providers; --enrich explicitly enables live lookups for that run.
# Set for this session
$env:VT_API_KEY = "your_virustotal_key"
$env:ABUSEIPDB_API_KEY = "your_abuseipdb_key"
# Or make them permanent (user profile)
[System.Environment]::SetEnvironmentVariable("VT_API_KEY","your_key","User")
[System.Environment]::SetEnvironmentVariable("ABUSEIPDB_API_KEY","your_key","User")Free API keys:
- VirusTotal → virustotal.com → Sign in → API Key
- AbuseIPDB → abuseipdb.com → Account → API
horus <indicator-or-file> [options]
horus --case <samples-dir> [case options]
<indicator-or-file> anything: a file path, hash, IP, domain, URL, or email
options:
--case <dir> create or refresh a persistent analysis case
--case-out <dir> workspace (default: <dir>.horus-case)
--case-name <name> analyst-facing case name
--cluster-mode <m> conservative, balanced, or broad
--verify re-hash cached case files before reuse
--rebuild repeat all case analysis
--prune remove missing files from case history
--enrich query configured providers; files are never uploaded
--max-lookups <n> live lookup cap (default: 100)
--max-file-mb <n> per-file limit (default: 512 MiB)
--no-recurse analyze only the top level of a case directory
--online-trust allow online Authenticode revocation checks
--scan <dir> triage every file in a folder, worst-first
--diff <a> <b> compare two PEs (add --json for structured output)
--clip read the indicator from the clipboard
--gui open the glass-console GUI (unified build)
--html <file> write the glass-console report to an HTML file
--csv <file> with --scan, also write a CSV
--json-out <file> with --scan, also write a JSON array
--cache-ttl <hours> cache intel lookups for N hours (0 = off)
--vt-key <key> VirusTotal API key (overrides VT_API_KEY env)
--abuse-key <key> AbuseIPDB API key (overrides ABUSEIPDB_API_KEY env)
--strings dump extracted ASCII/UTF-16 strings from the file
--profile verbose UEBA signal breakdown
--json machine-readable JSON (including case and diff modes)
--no-color disable ANSI colour (auto-disabled when piped)
-h, --help show this help
--version print the version
A ~/.horus.json is read if present, so you don't paste keys every run:
{ "vt_key": "…", "abuse_key": "…", "cache_ttl_hours": 24 }:: Static analysis of a suspicious binary (PE parser + entropy + capabilities)
horus C:\Samples\malware.exe
:: Same, plus hash and embedded-IOC reputation
horus C:\Samples\malware.exe --enrich --vt-key %VT_API_KEY%
:: Analyze and dump its full string table
horus C:\Samples\dropper.dll --strings
:: Get machine-readable JSON for scripting or SIEM ingestion
horus C:\Samples\payload.bin --json
:: Pipe JSON output into jq to extract just the verdict
horus suspicious.exe --json | jq .verdict# Creates C:\Cases\incoming.horus-case and recursively analyzes up to 5,000 files
horus.exe --case C:\Cases\incoming
# Re-check every cached hash, enable provider lookups, and open the offline dashboard
horus.exe --case C:\Cases\incoming --verify --enrich --gui
# Rebuild all analysis with the strictest similarity preset
horus.exe --case C:\Cases\incoming --rebuild --cluster-mode conservativeEach workspace contains private state under .horus and portable exports named case.json, report.html, samples.csv, clusters.csv, iocs.csv, and changes.csv. Stored paths are relative to the samples directory.
On each refresh, the top-level changes array in case.json and changes.csv describe what changed in that run. summary.changes gives the event count. Events distinguish added, modified, restored, missing, removed (with --prune), and assessment_changed. An unchanged file may get assessment_changed when its analysis status, score, verdict, or intel summary changes. Each event includes a relative path and available before/after SHA-256, analysis status, and score. This feed describes the latest refresh, not every past run. Files marked missing stay in case history until pruned. Use --verify to detect content changes that preserve both file size and modification time.
$case = Get-Content C:\Cases\incoming.horus-case\case.json -Raw | ConvertFrom-Json
$case.summary.changes
$case.changes | Where-Object kind -eq 'assessment_changed'horus.exe --diff C:\Samples\a.exe C:\Samples\b.exe
horus.exe --diff C:\Samples\a.exe C:\Samples\b.exe --jsonThe JSON form includes each file's analysis status and a comparison object with hash matches, TLSH distance, and section entropy drift where available. When one or both files cannot be fully analyzed as PEs, the top-level analysis_status is unknown and the command exits 3. A file read or usage error exits 2; a complete comparison exits 1 if either file is scored suspicious or malicious, otherwise 0.
The reserved IP addresses and domains below illustrate command syntax; they are not active threat indicators. Live provider results are not guaranteed for them.
:: MD5, SHA-1, or SHA-256 hash — Horus auto-detects the type
horus 44d88612fea8a8f36de82e1278abb02f --enrich
horus 3395856ce81f2b7382dee72602f798b642f14d0 --enrich
horus 470735e5b9545078198d440e67abbf86a43c29ae579eba1cf632eaa8dad3f57d --enrich
:: IP reputation from VirusTotal + AbuseIPDB simultaneously
horus 192.0.2.42 --enrich
:: Domain reputation
horus suspicious.example --enrich
:: Full URL
horus https://sketchy.example/drop.bin --enrich
:: Email address
horus sender@example.com --enrich
:: Combine keys inline instead of via env vars
horus 192.0.2.42 --enrich --vt-key abc123 --abuse-key def456
:: JSON output for automation
horus 192.0.2.42 --enrich --jsonExit codes make Horus useful in pipelines:
| Exit code | Meaning |
|---|---|
0 |
Analysis completed and no score reached 30 |
1 |
Suspicious or malicious — score ≥ 30 |
2 |
Fatal error, usage error, or output failure |
3 |
UNKNOWN, skipped, or otherwise incomplete, with no score ≥ 30 |
# Block a file from being used if Horus flags it
horus .\download.exe --json | Out-Null
if ($LASTEXITCODE -eq 1) {
Write-Warning "File flagged — not running it."
exit 1
}
# Batch-triage a folder
Get-ChildItem C:\Quarantine\*.exe | ForEach-Object {
$result = horus $_.FullName --json | ConvertFrom-Json
"$($_.Name): $($result.verdict) (score $($result.final_score))"
}Illustrative structure and values; this is not the output of a real sample.
{
"target": "malware.exe",
"analysis_status": "complete",
"size": 102912,
"md5": "a3f5d1e2c7b849d60e14a2b8f93c5d17",
"sha1": "4b2e8a91c3d056f7e820b4a19c6d3f7e2b085c40",
"sha256": "470735e5b9545078198d440e67abbf86a43c29ae579eba1cf632eaa8dad3f57d",
"is_pe": true,
"bitness": 64,
"imphash": "9a4f2c8d1e7b3056a82c914d6e0f5b17",
"static_score": 86,
"vt_detections": 61,
"final_score": 98,
"verdict": "LIKELY MALICIOUS"
}For a non-PE file with no provider evidence, analysis_status is unknown, final_score is null, and verdict is UNKNOWN.
No external libraries. No runtime dependencies. Everything in one .exe.
horus/
│
├── src/
│ ├── main.cpp unified CLI/GUI entry point
│ ├── core.hpp shared direct-analysis pipeline and report models
│ ├── case.hpp persistent case refresh and enrichment workflow
│ ├── cluster.hpp duplicate grouping and explainable PE similarity
│ ├── case_io.hpp portable case state and HTML/CSV/JSON exports
│ ├── pe.hpp PE32 / PE32+ parser — every field bounds-checked
│ ├── signatures.hpp 110-API knowledge base + capability scoring engine
│ ├── attack.hpp capability → MITRE ATT&CK technique mapping
│ ├── ueba.hpp behavioral archetype profiling
│ ├── fuzzy.hpp TLSH + ssdeep similarity hashing (self-contained)
│ ├── authenticode.cpp/hpp signature verification (WinVerifyTrust + crypt32)
│ ├── ioc.hpp indicator type auto-detection (no regex, hand-rolled)
│ ├── json.hpp zero-dependency JSON parser (recursive descent)
│ ├── crypto.cpp/hpp MD5 / SHA-1 / SHA-256 / authentihash via BCrypt/CNG
│ ├── http.cpp/hpp HTTPS client via Windows WinHTTP
│ ├── intel.cpp/hpp VirusTotal v3 + AbuseIPDB v2 (extensible interface)
│ ├── output.hpp atomic files and CSV cell hardening
│ └── console.hpp ANSI colour / formatted terminal output
│
├── tests/
│ ├── test_ioc.cpp indicator classifier unit tests
│ ├── test_json.cpp JSON parser unit tests
│ ├── test_pe.cpp PE parser unit tests
│ ├── test_fuzzy.cpp TLSH / ssdeep unit tests
│ ├── test_score.cpp risk scoring engine unit tests
│ ├── test_case.cpp refresh, duplicate, clustering, and 5,000-record benchmark
│ └── direct_smoke.ps1 process-level direct mode and exit-code checks
│
├── CMakeLists.txt CMake build (MSVC or MinGW-w64)
└── LICENSE
horus <argument>
│
├─ --case? ──YES──► refresh persistent workspace + cluster + export
│
├─ file_exists(arg)? ──YES──► analyze_file()
│ │
│ ├─ read bytes
│ ├─ hash_buffer() ← BCrypt/CNG
│ ├─ pe::Analyzer::analyze()
│ │ ├─ parse headers (bounds-checked)
│ │ ├─ parse sections + entropy
│ │ ├─ parse import table
│ │ └─ build imphash string
│ ├─ sig::score() ← capability engine
│ ├─ extract_strings() → scan_strings()
│ └─ hash / IOC lookup (only with --enrich)
│
└─ NO ──► classify(arg)
│
└─► intel::enrich() ← only with --enrich; parallel fan-out
├─ VirusTotal::lookup() (if supported + key)
└─ AbuseIPDB::lookup() (if supported + key)
The IntelSource interface is three methods. One new file, one new line in build_sources():
// In intel.cpp — add this class
class URLhaus : public IntelSource {
public:
explicit URLhaus(std::string key) : key_(std::move(key)) {}
std::string name() const override { return "URLhaus"; }
bool supports(IocType t) const override {
return t == IocType::Url || t == IocType::Domain;
}
SourceResult lookup(const std::string& ioc, IocType) const override {
// one WinHTTP call → parse JSON → fill SourceResult
}
private:
std::string key_;
};
// In main.cpp — build_sources(), add one line:
if (!o.urlhaus_key.empty()) v.push_back(std::make_shared<URLhaus>(o.urlhaus_key));Candidate sources to add: URLhaus · OTX (AlienVault) · GreyNoise · Shodan · MalwareBazaar
Requirements: Windows 10/11 x64, Visual Studio 2022 with the Desktop C++ workload, and CMake 3.15 or newer.
git clone https://github.com/mobinert/HORUS.git
cd HORUS
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DBUILD_TESTING=ON
cmake --build build --config Release
:: binaries at build\Release\ : horus-cli.exe horus.exe horus-gui.exeThe unit tests are wired into CTest:
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failureCovers direct PE and non-PE analysis, paths with spaces, Case Mode refresh/rebuild/prune, all three clustering presets, stable split/merge IDs, the 5,000-record benchmark, report injection, strict JSON, bounded HTTP responses, atomic output, version metadata, and exit codes. Provider behavior uses an in-memory mock; the test suite never calls live services.
Get-FileHash .\horus.exe -Algorithm SHA256
gh attestation verify .\horus.exe -R mobinert/HORUSCompare the first command with SHA256SUMS.txt. Release binaries are currently
Authenticode-unsigned, so Windows may still show a reputation warning; the checksum and
GitHub attestation verify the file's integrity and build provenance.
The imphash follows the Mandiant recipe: lowercase dll.function pairs (with extension stripped for .dll, .ocx, .sys), joined by commas in import order, then MD5'd. This matches what tools like pefile and pe-sieve produce, so you can cross-reference Horus results with existing sample collections.
- Horus never executes, copies, modifies, or uploads a sample. It only writes requested reports, case state, and local cache files.
- The PE parser checks offsets and lengths before reading and rejects malformed structures it detects. Keep Horus updated and analyze untrusted samples in a suitable environment.
- Network access is off by default.
--enrichsends only hashes and extracted indicators to configured providers;--online-trustseparately enables online certificate revocation checks. - The static risk score is a triage aid, not a production verdict. Pair it with independent evidence and, for anything that matters, a controlled sandbox detonation.
- Obfuscated samples that load all their imports at runtime will score low on static analysis — that's expected. The entropy and packing heuristics partially compensate, but there's no substitute for dynamic analysis on heavily obfuscated code.
| Tool | What it does |
|---|---|
| machunt | Read-only macOS threat hunting & compromise assessment — 20 modules, HTML/JSON reports |
| ssh-fortress | SSH hardening, brute-force protection & SIEM integration for Linux servers |
MIT — see LICENSE. Free for personal, commercial, and research use forever.