community-snort-processor
Status: Live Source: community Snort rulesets on GitHub (16 raw files —
travisbgreen/hunting-rules,thereisnotime/Snort-Rules) Type:snort(Snort 2.x rule text, parsed by the sharedinternal/emergingthreatsv2 parser) Source slug:community-snort(Exploit,ExploitCVE, placeholderCVEMetadata);SnortRulerows are written under the shared slugsnortSchedule: Daily at 09:00 UTC (cron(0 9 * * ? *)), 256 CPU / 512 MB,expected_duration_minutes = 15.
Overview
Detection coverage is a decision input, not trivia: when a CVE has no patch available, “is there an IDS signature we can deploy today?” is the only remaining mitigation question. This processor supplies the community half of that answer — the emergingthreats-snort-processor covers the commercial-grade ET open ruleset, and community-suricata-snort-processor covers Suricata-format community feeds.
Each run downloads a fixed list of 16 raw rule files, concatenates them, parses
every rule with emergingthreats.ParseV2Rules, and keeps only rules that carry
at least one reference:cve, reference:cnvd or GHSA reference
(cmd/community-snort-processor/main.go:144-150). Disabled rules (#alert …)
are parsed and stored with disabled=true — a commented-out signature is still
evidence that a signature exists.
Rules are stored in batches of 100, each rule inside its own
SAVEPOINT sp_sid_{SID} so a single bad rule cannot poison the batch
(cmd/community-snort-processor/main.go:213-231).
Records produced
| Condition | Records |
|---|---|
| Every stored rule | Exploit (source="community-snort", exploitId=SID, title=msg, category=classtype, platform from rule metadata, cveIds JSON) |
Each reference:cve on the rule | ExploitCVE junction, plus db.EnsureMinimalCVEMetadata(cveId, "community-snort") so the FK resolves — a placeholder CVEMetadata row (state="PUBLISHED", datePublished=0, no title, no rawDataJSON) |
| Every stored rule | SnortRule (source="snort", full parsed rule: action/protocol/addresses/ports, msg, flow, classtype, disabled, rawText, confidence, signature severity, affected products, attack targets, tags, MITRE technique/tactic ids, body options) |
| Each CVE / CNVD / GHSA id on the rule | SnortRuleCVE junction (the cveId column holds non-CVE identifiers too — 34 CNVD ids in production) |
| Successful batch | Raw rule text archived to community-snort/files/{sha256}/{SID}.rules |
| Failed rule or failed batch | Rule text quarantined to failed-feeds/community-snort-processor/{date}/store-error/{SID}.rules |
Placeholder rows under source='community-snort'
The CVEMetadata rows this processor creates exist only to satisfy the
ExploitCVE foreign key. Treat source='community-snort' as an alias-shell
source: exclude it from any title/CVSS coverage denominator. Production holds
4,381 such rows, of which 1,305 are pure placeholders; the remaining ~3,076
carry titles and dates written by an earlier revision of this processor that
cloned full CVE records, and are no longer refreshed by the current code.
Known issues
Exploit.originalUrlpoints at the wrong ruleset.MapToExploitRowis shared with the Emerging Threats processor and hardcodeshttps://rules.emergingthreats.net/open/snort-2.9.0/emerging-all.rules#sid-{SID}(internal/emergingthreats/mapper.go:12,34). All 2,146 productioncommunity-snortexploit rows carry that URL instead of the GitHub raw URL the rule came from, so the provenance link is misleading. The fix is to pass the per-rule source URL through the mapper.- Resume set is shared with the Suricata processor. Both processors call
db.LoadProcessedSnortRuleIDs(ctx, pool.Read, "snort")(cmd/community-snort-processor/main.go:111) and both writeSnortRulerows undersource='snort', which is unique on(snortId, source)(internal/db/snortrule.go:72). Snort SIDs are only unique within a ruleset, so whichever processor runs first claims a SID and the other silently skips its own, different rule with that number. No overlap is observable in production today (0 shared SIDs between the twoExploitsources), but a skipped rule leaves no trace, so absence of overlap is not proof of absence of loss. Give each ruleset its ownSnortRule.source, or key the resume set on(source, SID). - The soft deadline applies to backfills. The batch loop stops at a
hardcoded 20-minute deadline when
EXPECTED_DURATION_MINUTESis unset (cmd/community-snort-processor/main.go:84-88,176-179), andjust go-community-snort-backfillunsets it — contrary to the “backfills must run to completion” rule inscripts/go-processors/AGENTS.md.
Failure modes
| Failure | Behaviour |
|---|---|
| One rule file 404s / times out | Logged, RecordError, run continues with the remaining files (3 attempts each, 3 s × attempt backoff) |
| All 16 fetches fail | task.errored to SNS and exit 1 |
| Zero CVE-bearing rules parsed | task.completed with stored=0, no error |
| Single rule fails to store | Rolled back to its savepoint, quarantined to S3, counted in errors; run continues |
| Batch transaction fails | Whole batch quarantined, RecordError, next batch attempted |
| Any accumulated error at end of run | task.errored (Slack alert) instead of task.completed |
Flags
| Flag | Default | Description |
|---|---|---|
--all | false | Reprocess every parsed rule, bypassing the SID resume set |
--limit | 0 | Maximum rules to store (0 = unlimited) |
--force | false | Same effect as --all for the resume set |
Local run: just go-community-snort-backfill prod (positional:
TARGET ALL LIMIT FORCE).
Key files
| File | Purpose |
|---|---|
cmd/community-snort-processor/main.go | Rule-file list, fetch/parse loop, batch + savepoint writes |
internal/emergingthreats/parser_v2.go | Snort 2.x rule parser (shared) |
internal/emergingthreats/mapper.go | MapToExploitRow, MapToSnortRuleRow (shared) |
internal/db/snortrule.go | UpsertSnortRule, InsertSnortRuleCVEs, LoadProcessedSnortRuleIDs |
internal/db/exploit.go | UpsertExploit, InsertExploitCVE |
Verification
-- rules stored by this processor (Exploit side)
SELECT count(*) FROM "Exploit" WHERE source = 'community-snort';
-- CVE junctions
SELECT count(*) FROM "SnortRuleCVE" src
JOIN "SnortRule" sr ON sr.uuid = src."snortRuleUuid"
WHERE sr.source = 'snort';
-- placeholder CVEMetadata rows (expected: no title, datePublished = 0)
SELECT count(*) FILTER (WHERE title IS NULL) AS placeholders, count(*) AS total
FROM "CVEMetadata" WHERE source = 'community-snort';
S3 Persistence
- Archive path:
community-snort/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/community-snort-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted: (none documented)
Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).
See the S3 Persistence Contract for the full reason taxonomy.