███████╗████████╗ ██████╗ ██████╗ ███╗ ███╗
██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗████╗ ████║
███████╗ ██║ ██║ ██║██████╔╝██╔████╔██║
╚════██║ ██║ ██║ ██║██╔══██╗██║╚██╔╝██║
███████║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║
╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
They scan. STORM understands.
[ network ]· [ web ]· [ tls ]· [ secrets ]· [ cve ]· [ audit ]· [ container ]· [ iac ]· [ git-history ] ⟶ ⚡ KILL-CHAINS
zero deps · pure stdlib · built from scratch
$ whoami
STORM — an InfoSec scanner forged in Go. No third-party blood. Every line is ours.Every scanner out there does one job. One reads ports. One greps secrets. One diffs CVEs. One audits configs. Each spits out a flat list and calls it a day.
Everyone's got their own fighter jet. STORM brought the B2-Spirit — the thing nobody else flies: it doesn't just collect findings across domains, it welds them into attack chains on a single asset and scores the combined blast radius.
An open SSH port? Noise. An open SSH port + a vulnerable OpenSSH build + a private key leaked in the repo? That's a takeover. STORM sees the chain. The others see three unrelated lines.
go build -o storm ./cmd/storm
# instant demo: scan the bundled intentionally-vulnerable target
./storm scan --path ./examples # or: make demo
# what can we hit?
./storm probes
# multi-domain sweep + correlation
./storm scan \
--host 10.0.0.5,10.0.0.6 \
--pkg openssh@7.1,log4j@2.14 \
--path ./app \
--min-severity medium
# machine output for the pipeline
./storm scan --host 10.0.0.5 --format json > loot.json▓▓ KILL-CHAINS (correlated attack paths) ▓▓
CRITICAL Confirmed Exploitable Service [10.0.0.5 · risk 90/100]
STORM read the version straight off the wire and matched a live CVE — the
vulnerable service is listening and reachable right now.
1. MEDIUM Open port 9200 on 10.0.0.5
2. HIGH CVE-2018-15473: openssh 7.4 exposed on 10.0.0.5
CRITICAL Unrotated Leaked Secret [app.env · risk 90/100]
A secret committed to git history AND still in the working tree — deleting the
file won't save you. Rotate the credential.
1. HIGH AWS Access Key committed to git history (app.env)
2. HIGH AWS Access Key found in app.env
Every probe speaks one language — a Finding with an Evidence map (host,
package, secret_type, path, …). Those are the join keys. The correlator
buckets findings by asset, runs scenario rules, and escalates severity when
independent domains corroborate the same target:
network ─┐
secrets ─┤
cve ─┼─► group by asset ─► scenario rules ─► KILL-CHAIN (risk 0..100)
audit ─┤
iac ─┘
A new attack scenario = one Rule appended to internal/correlate/rules.go.
The engine loop never moves. Scenarios shipping today:
| Kill-chain | Fires when |
|---|---|
| 🔓 Remote SSH Takeover | exposed SSH + (OpenSSH CVE or leaked key) |
| 🎯 Confirmed Exploitable Service | banner-read version + matching CVE |
| 💥 Exposed Vulnerable Service | risky service exposed + any CVE |
| ☠️ Poisoned Container Image | baked-in secret / curl|bash + runs as root |
| 🪣 Internet-Exposed Data Store | public IaC store + unencrypted |
| 🗝️ Exposed Hardcoded Credential (IaC) | internet-facing infra + hardcoded creds |
| 🕳️ Unrotated Leaked Secret | secret in git history + still in working tree |
| 🌐 Reachable Web Secret Leak | .git/.env over HTTP + CVE/risky service on host |
| 🔒 Interceptable TLS Endpoint | weak/broken TLS + exposed port on host |
| 🔑 Credential Exposure Path | secret + weak file permissions |
| Probe | Domain | Hunts for |
|---|---|---|
📡 network.portscan |
network | open TCP ports, service banner-grab → version |
🌐 web.http |
web | missing security headers, leaky cookies, exposed .git/.env |
🔒 tls.scan |
tls | expired/self-signed certs, obsolete TLS 1.0/1.1, weak ciphers |
🔐 secrets.scan |
secret | leaked keys/tokens/passwords in files |
🕰️ git.history |
secret | secrets buried in commit history (survive deletion) |
🐛 cve.packages |
cve | installed packages vs. vuln DB |
🛡️ audit.hardening |
audit | insecure perms, debug flags, PermitRootLogin |
🐳 container.dockerfile |
container | root user, :latest, baked secrets, curl|bash |
☁️ iac.terraform |
iac | public S3, 0.0.0.0/0, unencrypted, IAM *:* |
| Component | Pattern | Job |
|---|---|---|
🧩 core.Probe |
Strategy | every scanner is interchangeable |
🏭 core.Registry |
Singleton + Factory | probes self-register via init() |
🔧 config.Builder |
Builder | fluent scan assembly |
📣 core.EventBus |
Observer | progress/events, zero coupling |
⛓️ core.Pipeline |
Chain of Responsibility | enrich → filter findings |
🛡️ WithTimeout/WithRetry |
Decorator | cross-cutting armor on any probe |
🧬 core.Enricher |
post-processing | banner→CVE fusion between probe & correlate |
📤 report.Reporter |
Visitor / Strategy | console / JSON / SARIF / HTML |
🧱 baseline.Filter |
Chain of Responsibility | suppress accepted findings (CI gate) |
🎮 cli.Command |
Command | subcommands, no switch monolith |
correlate.Correlator |
★ B2 | welds findings into kill-chains |
storm/
├── cmd/storm/ # entry point (binary)
├── examples/ # intentionally-vulnerable demo target (make demo)
└── internal/
├── core/ # Finding, Probe, Registry, Engine, Pipeline, Decorator, EventBus, Enricher
├── config/ # scan-config Builder
├── probes/ # network · web · tls · secrets · git.history · cve · audit · container · iac
├── secretrules/ # shared secret-detection patterns (DRY)
├── vulndb/ # neutral vulnerability-data layer
├── enrich/ # post-scan fusion: banner→CVE
├── correlate/ # B2-Spirit: kill-chain engine
├── baseline/ # finding suppression (CI gate)
├── report/ # console · json · sarif · html · progress
├── cli/ # Command router
└── app/ # composition root (everything is wired here)
internal/ is deliberate: Go physically forbids foreign modules from importing our guts.
# 1. snapshot the current state as accepted (baseline)
./storm scan --path . --write-baseline .storm-baseline.json
# 2. in PRs: SARIF into code scanning + fail only on NEW high+ findings
./storm scan --path . --baseline .storm-baseline.json --format sarif > storm.sarif
./storm scan --path . --baseline .storm-baseline.json --fail-on high--format html→ self-contained dashboard (one file, kill-chain cards + risk bars), open straight off disk.--format sarif→ SARIF 2.1.0, uploaded to GitHub code scanning (findings inline on the PR).--baseline→ suppresses accepted findings by stable fingerprint.--fail-on high→ non-zero exit when a finding lands at/above the threshold. That's the gate.
The bundled .github/workflows/ci.yml self-scans the repo and ships the SARIF.
package myscan
import "storm/internal/core"
func init() { core.Register(&Probe{}) }
type Probe struct{}
func (p *Probe) Name() string { return "my.scan" }
func (p *Probe) Domain() core.Domain { return core.DomainNetwork }
func (p *Probe) Accepts(t core.Target) bool { return t.Host != "" }
func (p *Probe) Scan(ctx context.Context, t core.Target, out chan<- core.Finding) error {
out <- core.Finding{ /* ... */ }
return nil
}Drop a blank import in cmd/storm/main.go and the engine picks it up. No central switch to touch.
STORM is a weapon for authorized security work: your own systems, contracted pentests, CTFs, research, education. Point it only at targets you're cleared to hit.
forged in Go · zero deps · stdlib only · the rest is up to you