hashicorp-discuss-fetch-processor — Design
1. Overview
- Purpose: Ingest HashiCorp security advisories (HCSEC-YYYY-NN) from the HashiCorp Discuss forum security category.
- Source URL: https://discuss.hashicorp.com/c/security/52
- Owner: HashiCorp Security Team
- Licence: Publicly readable; no auth required
- Schedule cadence: Runs weekly on Fridays at 05:00 UTC (
cron(0 5 ? * FRI *)). - ECS resources: 256 cpu_units, 512 memory_mb, expected_duration_minutes = 15
- Reads: Discourse JSON API (
/c/security/52.json,/t/{slug}/{id}.json?include_raw=1) - Writes: CVEMetadata, CVEDescription, CVEReference, CVEAlias, BulkDataDumpTracker, S3 archive
2. Source contract (from Phase 0 recon)
- Cadence: ~1-2 new advisories per month; daily poll is conservative
- Volume: ~194 total topics across 7 pages (30 per page); ~1-2 new per month
- Identifier shape: HCSEC-YYYY-NN (source-native); CVE-YYYY-NNNN embedded in post text
- Backfill feasibility: Yes — all historical topics accessible via pagination
- Anti-bot / auth: None observed; public Discourse instance
- Pagination:
?page=Non category JSON endpoint;more_topics_urlsignals next page - Freshness: No Last-Modified/ETag; use per-topic content hash for change detection
3. Architecture diagram
4. Source → DB field mapping
| Source field | Target | Notes |
|---|---|---|
topic.title | CVEMetadata.title | Full title including HCSEC prefix |
topic.created_at | CVEMetadata.datePublished | ISO 8601 → Unix seconds |
post.raw | CVEDescription.value | Raw markdown text |
topic.slug + topic.id | CVEMetadata.sourceAdvisoryRef | Permalink |
Extracted HCSEC-YYYY-NN | CVEMetadata.cveId | Primary identifier |
Extracted CVE-YYYY-NNNN | CVEAlias.aliasCveId | Cross-cveId edges |
| Permalink | CVEAlias.aliasCveId | Self-reference edge |
Tag security-* | CVEMetadata.affectedProduct | Mapped to product name |
"HashiCorp" | CVEMetadata.affectedVendor | Static |
SHA-256 of post.raw | CVEMetadata.sourceFileHash | Change detection |
5. Identifier policy
cveId=HCSEC-YYYY-NN(source-native, kept as primary)Aliases= all extractedCVE-YYYY-NNNN+ topic permalink- Rationale: HashiCorp advisories frequently bundle multiple CVEs under one HCSEC (e.g., meta-bulletin HCSEC-2025-22 links to 8 sub-bulletins). Using HCSEC as primary avoids bundle-suppression complexity and keeps one row per advisory. CVEs are discoverable via the alias graph.
SuppressDerivedMetrics = truebecause descriptions cover the whole advisory, not a single CVE.
⚠ The topic permalink must stop being passed as an alias (audit 2026-08-06).
map.go:44prepends thehttps://discuss.hashicorp.com/t/{slug}/{id}URL toAliases. Sincedb.InsertAliasesgained non-CVE alias backfill (commit08f7f28, 2026-05-25), an alias with noCVEMetadatarow of its own gets a placeholder row minted under the writing source — so the next successful run would create ~193CVEMetadatarows whosecveIdis a URL. The permalink already lives insourceAdvisoryRefand in aCVEMetadataReferencesrow; it is not an identifier and does not belong in the alias graph. Production is clean only because nothing has been stored since 2026-05-07 (see §9).
6. CRIT / VEX
No CRIT staging. HashiCorp advisories are for on-prem/enterprise software products (Vault, Consul, Terraform, Nomad, Boundary, Packer, Vagrant, Waypoint, Sentinel), not cloud-provider resource types. No (Provider, Service, ResourceType) triples to map.
7. S3 / source-file archive layout
- Bucket:
$S3_BUCKET_NAME - Key prefix:
hashicorp-discuss/files/{sha256}/{hcsec-id}.json - Payload schema: Normalised JSON of
osv.CVESourceData - Quarantine reasons:
store-error— transaction commit failed after retriesfetch-error— topic detail HTTP request failed
8. Error handling & Slack
- Per-record vs batch: Per-record failures are logged and counted; batch continues
- Retry profile: 3 attempts, 500 ms × attempt backoff, transient network errors retried
- Slack events:
Started— at processor startupCompleted— with{"stored": N}statsErrored— with{"stored": N, "errors": M}and error detailNoWork— when tracker freshness check skips
9. Performance
- Concurrency: 4 fetch workers (configurable via
--workers) - Per-request timeout: 30 seconds
- Rate-limit pacing: 250 ms gap between dispatches
- Soft deadline:
EXPECTED_DURATION_MINUTES - 10minutes; the fan-out goroutine stops dispatching a further 5 minutes before that (main.go:161) - Resume strategy: per-topic SHA-256 of
post.rawcompared against the storedCVEMetadata.sourceFileHash; unchanged topics are skipped
⚠ Known defects (audit 2026-08-06) — the two bullets above do not hold in ECS:
- The processor stores nothing on a scheduled run.
softDeadlineisnow + (EXPECTED_DURATION_MINUTES − 10)(main.go:65) and the fan-out loop breaks atsoftDeadline − 5m(main.go:161). With the configuredexpected_duration_minutes = 15that budget is zero, so the dispatcher exits before emitting a single topic and the run reportsCompleted{stored:0}— which also bumps the tracker, so nothing looks wrong. Production evidence: the newestlastFetchedAtforsource='hashicorp-discuss'is 2026-05-07, the day the processor landed, with nothing written since. Local backfills are unaffected because they unsetEXPECTED_DURATION_MINUTES.- The resume set can never hit.
db.LoadProcessedHashesreturns a map keyed bycveId(internal/db/resume.go:58) but the lookup key isHashString(topic.Slug)(main.go:193-194), so once (1) is fixed every topic is re-fetched and re-stored on every run. The key must be theHCSEC-YYYY-NNid.Both are code bugs, not design changes — the intended behaviour is what the two bullets above describe.
10. Backfill
- Backfill warranted? Yes — all historical topics are accessible
- Justfile recipe:
go-hashicorp-discuss-fetch-backfill - Expected runtime: ~5 minutes for full ~194 topics
- Differs from scheduled binary: Unsets
EXPECTED_DURATION_MINUTESso backfill runs to completion
S3 Persistence
- Archive path:
hashicorp-discuss/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/hashicorp-discuss-fetch-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
store-error
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.