// documentation

Methodology

What Echo measures, where the data comes from, every rule that turns a packet into a tag, and what we deliberately exclude. The goal is for defenders to be able to evaluate our data with eyes open — not to take our word for anything.

Data source

Every event Echo surfaces comes from Sentinel, the observability stack running across the ironcat fleet (two Azure VMs). Sentinel ingests:

  • SSH auth logs from each VM via Vector tailing /var/log/auth.log.
  • Caddy access logs from each VM via Vector tailing the structured JSON logs Caddy writes.

A second-pass Go classifier sits in front of the database to catch attack patterns Vector's VRL doesn't easily express. Events land in Postgres tagged at the source — no batch processing, no offline reclassification.

Refresh cadences

LayerRefreshNotes
Event tags (VRL + Go)at ingestPer-event classification happens before the row hits Postgres
Per-IP rollup (actors table)60 secondsAggregate stats + behavior tags recomputed every minute
Cluster detection60 secondsUnion-find on shared SSH usernames + shared HTTP paths
IP geo enrichmentasync, ~30 IPs/minVia ip-api.com; cached in ip_geo table
Public API caches60s–15minIP lookup 60s, tag list / feeds 15min
Echo pages (ISR)60s–300sServer-rendered with revalidation

Behavior tags — derived from rolled-up activity

Behavior tags describe what an actor IS, computed from their cumulative behavior. Recomputed every 60 seconds; an actor whose behavior changes loses or gains tags accordingly.

ssh.bruteforce10+ SSH auth failures from one IP in the rolling 30d window
ssh.stuffing5+ distinct usernames AND 10+ failures from the same IP — credential-stuffing pattern
probe.404flood20+ HTTP 4xx responses AND 15+ distinct paths — directory busting
actor.cross_protocolSame IP observed on both SSH and HTTP — multi-vector reconnaissance
actor.persistentActive across 24h+ with 50+ total events
actor.long_termActive across 7d+ with 200+ total events — committed campaign infrastructure
actor.heavy1000+ total events from a single IP. The truly relentless.
actor.multi_vmHitting both ironcat.io AND ironcatlabs.com hosts
subnet.coordinatedOne of ≥3 attacking IPs sharing the same /24 — almost certainly one operator on a contiguous block
asn.coordinated≥5 peers on a non-cloud-provider ASN. Excludes Google/DO/Azure/AWS/Tencent/Volcano/UCLOUD where shared tenancy is meaningless.
cluster.shared_wordlistMember of a union-find component over SSH username overlap — same brute-force tooling
cluster.shared_pathsMember of a union-find component over HTTP probe-path overlap — same web-probe tooling
cluster.confirmed_operatorIn BOTH a wordlist AND a paths cluster — two independent signals of shared operator

Event tags — applied at ingest by VRL

Event tags describe what a single request did. Applied at ingest by Vector's VRL rules. An actor accumulates the union of every event tag any of their events carried.

ssh.dictionarySSH login attempt against a common attacker-wordlist username (root, admin, test, ubuntu, etc.)
ssh.enumSSH login attempt against a non-existent system user — enumeration probe
probe.configProbing for known config-file paths (.env, .git, config files)
probe.traversalURI contains path traversal sequences (../, %2e%2e)
probe.sqliSQL-injection patterns in URI
probe.xssCross-site-scripting patterns in URI
probe.log4jJNDI lookup patterns (Log4Shell)
probe.shellShell/RCE payloads in URI
probe.method_abuseUnusual HTTP methods (TRACE, OPTIONS abuse)
scanner.*Matched a known scanner UA signature: nuclei, sqlmap, nikto, nmap, masscan, gobuster, ffuf, dirbuster, wpscan, etc.
client.threat_intel_crawlerKnown legitimate research crawler (Palo Alto, RecordedFuture, Common Crawl, Censys, Shodan, BinaryEdge, LeakIX, Onyphe)
client.{curl,wget,headless,library,scraper,search-bot}Client-tooling classification
client.{go,python,rust,node,fast}_httpHTTP library sub-classification when ascertainable
protocol.{old,no-accept,no-language}HTTP-spec irregularities
path.{long-suspect,encoded-heavy}URI-shape oddness

What Echo deliberately excludes

  • Successful exploitation. If a probe to /.envreturned a 200, that's a vulnerability we haven't patched — not something we broadcast. The IP lookup endpoint filters by NOT (source = 'ssh' AND category = 'auth.success') and NOT (http 2xx AND tags LIKE 'probe.%').
  • Research crawlers. Palo Alto, RecordedFuture, Common Crawl, Censys, Shodan, BinaryEdge, LeakIX, Onyphe. Tagged at ingest as client.threat_intel_crawler and filtered from every public feed via the feed_exclusions table.
  • Our own log shipper. Sentinel-Agent's Vector instance POSTs back to us, which Caddy logs. Self-tagged as internal.sentinel_agent and filtered.
  • Echo's SSR fetches. Echo's Next.js renders by fetching from Sentinel. Filtered from the public-traffic analytics by IP.

Caveats

  • Geographic data is approximate. We use ip-api.com for free, best-effort IP geolocation. A VPN or proxied attacker will appear in whichever country the proxy lives in.
  • ASNs include cloud providers. A high count on Google or DigitalOcean usually means many unrelated attackers happened to rent there, not one operator. The asn.coordinated behavior tag specifically excludes the cloud giants for this reason.
  • UA strings are spoofable. client.*tags are heuristics; an attacker who spoofs Mozilla in their User-Agent header will look like "human" traffic until other signals catch them.
  • Window is rolling. All counts default to a 30-day rolling window. A probe campaign that ended 35 days ago has dropped off.

Free & open

All Echo data is published under CC0 — public domain. No attribution required. No rate-limiting. No auth. Just:

Found a methodology issue? The Sentinel source is open and the behavior rules are inspectable via the admin /tactics page on the public side.