Entity-resolved security intelligence console. Ingests raw telemetry from five different log formats, resolves the entities hiding inside them into one graph, detects with rules and behavioural analytics, reconstructs incidents as readable ATT&CK kill chains, and keeps every claim one click from the raw log line that produced it.
Pure Python 3.9+ standard library. No pip install, no services, no network calls.
python3 -m argus demo --serve # generate a sample intrusion, analyse it, open the consoleThen open http://127.0.0.1:8799.
A SIEM shows you alerts. Each alert knows almost nothing:
- a Suricata alert knows
10.20.14.42 → 185.243.115.84:443 - a Sysmon event knows
WS-0142 / svchost.exe / alice - an
sshdline knowsdeploy from 45.132.192.77 - a CloudTrail record knows
arn:aws:iam::…:user/alice / StopLogging
Nothing in that pile says these are the same intrusion. Assembling them is the job analysts spend their evenings on. ARGUS does the assembly.
On the shipped demo dataset: 8,672 raw records → 65 findings → 1 incident spanning 10 ATT&CK tactics, with a written reconstruction of what happened.
logs ──▶ ingest ──▶ resolve ──▶ detect ──▶ correlate ──▶ score ──▶ console
adapters entities rules + incidents risk graph /
+ graph analytics + kill propaga- timeline /
chains tion hunt / paths
| Adapter | Handles |
|---|---|
zeek |
conn / dns / http / ssl / notice, TSV and JSON-lines |
suricata |
EVE JSON — alert, flow, dns, http, tls |
sysmon |
Windows JSON — Sysmon 1/3/10/11/12-13/22 and Security 4624/4625/4672/4688/4698/7045/1102 |
authlog |
Linux auth.log / secure — sshd, sudo, PAM, useradd, groupadd |
cloudtrail |
AWS CloudTrail, both the {"Records":[…]} envelope and NDJSON |
Point it at a directory of mixed logs; each file is sniffed and routed. .gz is
handled transparently. Everything normalises into one wide ECS-shaped event
table, which is what makes cross-source correlation possible at all.
Three moves turn disconnected records into one graph:
- IP ↔ host binding. A Sysmon network event is emitted by the host that
owns the source IP, so that pairing is authoritative. Internal IPs then
collapse into their host entity — without this you get two disconnected
populations (network nodes and endpoint nodes) that never touch. Hosts with no
endpoint agent are covered from the asset inventory (
assets.json). - Principal normalisation.
CORP\Alice,alice@corp.localandalicebecome one user entity; the domain is kept as an attribute, not part of the key. - Resolution chain. DNS answers link domains to the external IPs they resolve to, so a flow to a bare address can be attributed back to the name.
Every entity/event association is written to an observations table. That table
is the provenance guarantee.
41 declarative rules across Windows, Linux, network and cloud, each mapped to
an ATT&CK technique. Rules are plain data (Python or JSON), with a Sigma-like
condition grammar: all/any/none combinators over leaf predicates
(eq, in, contains, regex, gt, startswith, …), reaching into the JSON
sidecar with extra.granted_access. Every rule aggregates — one burst of 46
brute-force packets is one finding that says "46 times", not 46 findings.
10 behavioural analytics for what no signature describes:
| Analytic | What it measures |
|---|---|
beaconing |
Inter-arrival regularity via MAD, not stdev — a single missed check-in destroys a stdev score but leaves MAD intact. Tolerates ±35% implant jitter. Collapses duplicate observations when two sensors see the same connection. |
dns_tunnel |
Subdomain cardinality × Shannon entropy per label × unusual qtype ratio |
rare_process |
Fleet prevalence, with system-directory binaries explicitly down-weighted (low prevalence there says more about the estate than the binary) |
credential_attacks |
Failure clustering by source, spray vs guessing, and — the part that matters — whether anything succeeded afterwards |
lateral_movement |
One account, several hosts, remote logon types, inside a window |
exfil_volume |
Outbound volume against the estate's own p99, not a fixed threshold, weighted by how rare the destination is |
staging_then_egress |
Archive written, then a large upload from the same host within hours — either half is weak, the sequence is the signal |
first_time_access |
Account/host pairs with no precedent in the baseline half of the data |
impossible_travel |
One credential in two countries too close in time (only runs when geo data is actually supplied) |
offhours_privileged |
Low-severity supporting context that correlation pulls into real incidents |
Findings that share identity-bearing entities inside a 48-hour window are clustered, then ordered along the ATT&CK kill chain into a written narrative.
Two design decisions carry this stage:
- Only substantive findings merge clusters. A weak observation cannot be the sole bridge welding two unrelated intrusions together; it attaches to the cluster it best matches instead.
- Breadth is scored, not just severity. Six alerts of one detection firing repeatedly is a noisy rule. Six findings spanning initial-access → execution → credential-access → lateral-movement → collection → exfiltration is a live intrusion, and it ranks above the former even when every individual severity is lower.
Compromise is contagious, so risk is seeded from findings and diffused across
the typed edge graph with per-edge-type conductance (authenticated_to ≈ 0.95,
resolved_to ≈ 0.15) and damping.
Attack paths are modelled the way an operator moves, not the way packets
flow: from a compromised host you harvest the credentials of accounts that
authenticated there, and with those accounts you reach every host they can
authenticate to. Alternating host → user → host hops is what surfaces the
non-obvious two-step route from a phished laptop to a domain controller.
Five views over the same graph:
- Link graph — force-directed canvas, colour by entity type, radius by risk, amber ring for crown jewels. Click to open, double-click to expand, drag, zoom.
- Timeline — stacked event volume by kind, with findings plotted on their own severity lane below. Click a marker to open the finding.
- Hunt — the query language, with
statsbar charts. - ATT&CK — technique coverage matrix; lit cells are what this dataset evidenced, dim cells are what ARGUS can detect but did not see.
- Attack paths — routes to crown jewels, ranked by where the attacker already is.
The detail pane always ends in raw log lines with their file:line reference.
Compiles to SQL, so it stays fast on millions of rows.
kind=process.create cmdline~"-enc "
kind=auth.login outcome=failure | stats count by src_ip
dst_port in [3389,22,445] src_ip:external
kind=network.flow bytes_out>50mb dst_ip:external | sort bytes_out desc | limit 50
process=powershell.exe AND NOT parent_process in [explorer.exe,cmd.exe]
domain~"\.(top|xyz|ru)$" | stats count by domain
"vssadmin"
| Syntax | Meaning |
|---|---|
field=value |
exact, case-insensitive; * and ? wildcards |
field~"text" |
substring, auto-upgraded to regex if it has metacharacters |
field>50mb |
numeric with k/mb/gb/tb suffixes |
field in [a,b] |
set membership (also not in) |
src_ip:external |
type predicate — external, internal, exists, empty |
AND OR NOT ( ) |
boolean logic; adjacency means AND |
"bare text" |
free-text across message, cmdline, url, host, raw |
| stats count by host |
aggregate |
| sort f desc | limit n | fields a,b |
order, cap, project |
python3 -m argus demo [--serve] # synthetic intrusion, full pipeline
python3 -m argus ingest /var/log/zeek/ # load real logs (auto-detects format)
python3 -m argus analyze # re-run resolution/detection/correlation
python3 -m argus serve --port 8799 # web console
python3 -m argus hunt 'kind=auth.login outcome=failure | stats count by src_ip'
python3 -m argus incidents -v # narratives on the terminal
python3 -m argus report --out brief.html # standalone HTML briefing
python3 -m argus statsGlobal flags: --db <path> (default argus.db, or $ARGUS_DB),
--assets <file>.
python3 -m argus --db corp.db --assets assets.json ingest /path/to/logs/
python3 -m argus --db corp.db serveDescribes your estate. Everything is optional; sensible defaults apply.
{
"crown_jewels": ["*dc*", "*sql*", "*vault*", "prod-*"],
"admin_users": ["*admin*", "root", "svc-*"],
"allow_domains": ["microsoft.com", "github.com"],
"hosts": { "lnx-app02": "10.20.2.12" },
"geoip": { "45.132.192.77": "RU", "203.0.113.0/24": "US" }
}hosts supplies IP↔host bindings for machines with no endpoint agent. geoip
is how impossible-travel gets geography — ARGUS ships no GeoIP database, and the
analytic simply does not run rather than guessing.
python3 -m argus analyze --rules my_rules.json[{
"id": "custom.suspicious_svc",
"title": "Service installed pointing at a UNC path",
"severity": 80, "confidence": 0.8, "technique": "T1543.003",
"kinds": ["service.install"],
"where": { "all": [ { "field": "file_path", "startswith": "\\\\" } ] }
}]python3 -m unittest discover -s tests -v47 tests. The analytics tests assert on behaviour rather than implementation — a jittered beacon is caught, a wildly-jittered legitimate update checker is not, a beacon survives being observed twice by two sensors, and an end-to-end test drives the whole pipeline over the generated intrusion and asserts that all ten kill-chain detections fire and that every finding traces back to raw events.
It is a working analysis engine with real detection math, honest provenance and a dense operator UI, that runs on real Zeek/Suricata/Sysmon/syslog/CloudTrail output today.
It is not a production SIEM. There is no streaming ingest, no authentication, no multi-tenancy, no retention policy, and it is single-node SQLite — comfortable into the low millions of events, not billions. Bind it to localhost; it has no access control of any kind.
Detection content is a curated ATT&CK subset covering what the shipped rules and analytics emit, not the full 600+ technique corpus. Findings are investigative leads, not verdicts — which is exactly why every one of them is traceable to the raw record.
MITRE ATT&CK® is a registered trademark of The MITRE Corporation.
argus/
model.py unified event schema, entities, edges, findings, incidents
store.py SQLite store (events, entities, observations, edges, …)
util.py IP/name normalisation, robust statistics, timestamp parsing
attck.py curated ATT&CK catalog + coverage matrix
ingest/ network.py (zeek, suricata) · host.py (sysmon, authlog) · cloud.py
resolve.py entity resolution + graph construction
enrich.py optional GeoIP / ASN enrichment from the assets file
detect/ rules.py (engine + 41 rules) · analytics.py (10 analytics)
correlate.py incident clustering, kill-chain narrative, ATT&CK coverage
graph.py risk propagation, attack paths, blast radius, subgraphs
hunt.py AQL parser and compiler
pipeline.py ingest → resolve → detect → correlate → score
server.py stdlib HTTP API + static console
report.py standalone HTML briefing
cli.py command line
scenarios/ synthetic multi-stage intrusion generator
web/ index.html · style.css · app.js · graph.js
tests/ test_argus.py
If this is useful to you, a ⭐ on the repo helps other people find it. Issues and pull requests are welcome.
Built by Priyankar "Sunny" Chakraborty · everbuiltstudio.com
