Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Argus

A minimal, educational EDR / endpoint monitor written in Nim.

Argus observes what happens on a host — process, file and network events — normalizes them into a single typed event model, and runs a declarative, rule-based detection engine that raises alerts mapped to MITRE ATT&CK.

⚠️ Educational project — not a production EDR

Argus exists to teach how endpoint detection works, not to protect a real fleet. It has no kernel driver, no tamper protection, no anti-evasion, no managed update channel, and no support commitment. A determined adversary can trivially avoid or disable it. Do not rely on it as a security control. Use it to read, to experiment, and to understand the pipeline that real EDRs implement at far greater depth.

Principles

  • Defensive and observational only. Argus reads telemetry. It never injects into, tampers with, hides from, or modifies other processes. There is no offensive capability in this codebase and none will be added.
  • Least privilege, and honest about it. Each collector declares exactly what it needs and what it can still see without it. argus doctor prints that report before anything is collected. Argus never asks you to weaken host security.
  • Normalize early. Raw platform structures stop at the collector boundary. Everything downstream — engine, rules, sinks — sees only the normalized Event type.
  • Never lose anything silently. When the bus fills it drops events and counts them; when correlation state hits its cap it evicts and counts that; when a collector cannot start it is named. A monitor that looks healthy while observing nothing is worse than one that admits a gap.
  • Handle data responsibly. Command lines and file paths routinely contain personal data. The default watch list is short and explicit rather than "everything, filtered later".

Pipeline

collectors ──▶ normalizer ──▶ event bus ──▶ rule engine ──▶ alert sinks
(platform)     (to Event)     (bounded,     (YAML rules +    (json / console
                              threaded)     correlation)      / pluggable)

Quick start

Requires Nim 2.0+.

nimble build      # produces ./argus
nimble test       # 721 unit tests
nimble lint       # type-checks every module

./argus doctor                                     # what can be collected here
./argus rules rules/                               # what the pack detects
./argus replay tests/data/sample_trace.jsonl rules/ # test rules deterministically
./argus run --duration 30                          # watch this host

Or run the guided walkthrough, which is safe and self-contained:

./scripts/demo.sh

Replaying a trace

The fastest way to see the whole thing work — and the way the rules are tested. No host activity is observed; the trace is a file in the repository.

$ ./argus replay tests/data/sample_trace.jsonl rules/

[HIGH] ARG-PROC-001  Interactive shell spawned by a web server
    when   2026-08-01T10:15:32.412Z  host   web-01
    attack T1059.004 · execution  https://attack.mitre.org/techniques/T1059/004/
    evidence
      · process_start /usr/bin/bash (pid 4711)
          parent /usr/sbin/nginx (pid 1240)
          user   www-data (uid 33)

[CRITICAL] ARG-CORR-001  Web server spawned a shell, which then connected out
    attack T1071.001 · command-and-control
    evidence
      · process_start /usr/bin/bash (pid 4711)
      · net_connect tcp 203.0.113.10:4444

The last one is the point of the whole project: neither half is damning alone, but the sequence, from the same process, inside a time window, is.

The same trace contains three benign apt writes to /etc. Nothing fires on them — a test asserts that, because noise is how a real deployment gets muted.

What it can see

Collector Source Privilege Limitation
Process /proc polling read /proc misses processes shorter-lived than the interval
File inotify (Linux) or directory polling read the watched paths inotify watches are not recursive; polling misses churn between scans
Network /proc/net/* + /proc/*/fd unprivileged for your own processes; root to attribute all pids polling misses short-lived connections

argus doctor reports this for your actual host. On a machine with no /proc — macOS, for instance — it says so plainly rather than collecting nothing quietly:

process — unavailable
    reason: no /proc on this system; the process collector needs a Linux-style procfs
No collectors can run here. Argus would observe nothing.

Rules

Rules are YAML. See rules/ for the 22-rule starter pack and docs/design.md for the full format.

- id: ARG-PROC-001
  name: Interactive shell spawned by a web server
  severity: high
  attack: { tactic: execution, technique: T1059.004 }
  when: { action: process_start }
  match:
    all:
      - { field: actor.exe,       op: basename_in, values: [sh, bash, dash, zsh] }
      - { field: actor.parentExe, op: basename_in, values: [nginx, httpd, php-fpm] }

Nineteen operators (equals, in, regex, glob, path_under, basename_in, cidr, gt/lt, exists, …), all/any/not composition, and correlation rules that join a sequence of steps on shared fields inside a time window.

Everything checkable is checked at load time — unknown field paths, unknown operators, regexes that do not compile, gt given two values, duplicate ids. A detection that silently never fires is worse than one that refuses to load:

rules/01-process.yaml: rule ARG-PROC-001: rules[0].match.field:
  unknown field path 'actor.exee'; see docs/design.md for the field table

Configuration

Every setting has a defensible default, so argus run works with no config at all. Copy argus.example.yaml to argus.yaml to override; argus config shows what is actually in effect and where it came from.

Using it as a library

import argus

let rules = loadRuleDir("rules")
let engine = newEngine(rules)
let sinks = newSinkGroup(newConsoleSink(), newJsonFileSink("alerts.jsonl"))

for alert in engine.processAll(loadTrace("trace.jsonl")):
  sinks.submit(alert)
sinks.close()

Status

All seven milestones are complete. See docs/design.md for the event schema, collector interface, rule format and design rationale.

# Milestone Status
1 Scaffold, Event model, alert sinks
2 Rule engine: YAML rules, field matching, ATT&CK
3 Collector interface, event bus, process collector
4 File collector (inotify + polling)
5 Network collector with pid correlation
6 Stateful correlation across a time window
7 CLI, config, starter rule pack, demo

Deliberately not built

  • eBPF collection. Richer and cheaper than polling, and a much larger project. The collector interface is designed so it could be added without the engine noticing.
  • netlink proc connector. Would catch processes that polling misses, at the cost of CAP_NET_ADMIN and a lot of struct marshalling.
  • A second platform. macOS EndpointSecurity and Windows ETW both fit behind the existing collector interface.

Never

Process injection, tampering, hiding, evasion, or any capability whose purpose is to act on another process rather than observe it. Argus reads. That is the whole of its authority.

Testing

721 unit tests, no live host activity in any of them.

  • Parsers (/proc/<pid>/stat, /proc/net/tcp, inotify masks) are pure functions tested against real-shaped input, including the cases that break naive implementations — a process legally named (evil) thing, IPv6 addresses stored as four little-endian words.
  • Collectors take their tree root as a parameter, so the exact production code path runs against a fixture directory. No mocks.
  • Rules are tested by replaying recorded JSONL traces, which is deterministic and provokes nothing on the machine running the suite.
nimble test

License

MIT — see LICENSE.

About

Argus. A minimal, educational EDR / endpoint monitor written in Nim. It collects process, file, and network telemetry, normalizes it, and runs a rule-based detection engine with MITRE ATT&CK mapping. Read-only and defensive by design.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages