Snyk Fetch Processor — Design

Overview

Scrapes the Snyk vulnerability database (security.snyk.io) to ingest SNYK-prefixed vulnerability records. Listing pages are paginated to discover identifiers, then individual detail pages are fetched and parsed via the embedded __NUXT_DATA__ JSON array.

Records are stored under SNYK identifiers (source=snyk). CVE and GHSA identifiers found in each record are stored as aliases.

Feed

PropertyValue
URLhttps://security.snyk.io/disclosed-vulnerabilities/{page}
AuthNone — fully public
FormatHTML with embedded Nuxt.js __NUXT_DATA__ JSON
LanguageEnglish
Items10 per listing page, thousands total
NotePer-page enumeration; pages return SSR HTML with SNYK hrefs

Page Structure

Listing Pages

Each listing page at /disclosed-vulnerabilities/{n} contains:

  • Anchor tags with href="/vuln/SNYK-*" throughout the SSR HTML (full-page scan, no container regex needed)
  • Empty pages (beyond last) return 200 OK with 0 SNYK hrefs, terminating pagination

Detail Pages

Each vulnerability page at /vuln/{SNYK-ID} contains:

  • <script id="__NUXT_DATA__"> with a JSON array
  • Index 4 of the array is a field map: {"title": 6, "description": 7, ...}
  • Indexed values resolve to other array positions holding actual data
  • String values throughout the array contain CVE/GHSA aliases, CWE IDs, CVSS vectors, version ranges, and lesson URLs

Parsing

NUXT_DATA Field Map (index 4)

FieldUsage
idSNYK identifier
titleVulnerability title
descriptionFull description text
severitySeverity label (low/medium/high/critical)
packageNameAffected package name
packageManagerEcosystem (pip, npm, maven, etc.)
publicationTimeISO 8601 publication date
disclosureTimeISO 8601 disclosure date
CVSSv3CVSS v3.1 or v4.0 vector string
cvssScoreNumeric CVSS score
languageProgramming language
exploitMaturityExploit maturity level
maliciousBoolean malicious package indicator

Array Scan (prefix-matched strings)

Prefix/PatternStored as
CVE-YYYY-NNNNNCVEAlias
GHSA-xxxx-xxxx-xxxxCVEAlias
CWE-NNNCVEProblemType
CVSS:3.1/...CVEMetric (cvssV3_1)
CVSS:4.0/...CVEMetric (cvssV4_0)
https://learn.snyk.io/lesson/...CVEMetadataReferences
[version,version)CVEAffectedVersion (bracket range)
<version, <=version, >=versionCVEAffectedVersion (comparison operator)

Storage

No new tables or columns. All tables already exist.

TableRows inserted
CVEMetadataOne per SNYK ID; source="snyk"
CVEDescriptionOne per vuln; lang="en"
CVEMetadataReferencesSnyk vuln URL + lesson URLs
CVEMetricCVSS v3.1/v4.0 vectors with scores
CVEProblemTypeCWE IDs from array scan
CVEAffectedPackage with ecosystem, version ranges
CVEAliasCVE-* and GHSA-* cross-references

Malware Threat-Actor Enrichment

A Snyk record is treated as a malicious package when its __NUXT_DATA__ malicious boolean is set, or its title equals “Malicious Package”. Such records (isMaliciousPackage=true) qualify for threat-actor attribution under source='snyk'. After a record’s store transaction commits, a first-time-only post-commit pass invokes the shared attribution engine (internal/actorintel), which resolves the malware author from package/repo identity plus upstream registry, GitHub, and Docker Hub lookups. Results land in the generic, shared MalwareThreatActor edge (cveId, cveSource='snyk'ThreatActor) and MalwareAttribution status tables — not the OSM-only OsmThreat* tables. A (cveId, source) already carrying a MalwareAttribution row is never re-enriched.

AspectBehaviour
Detection rulemalicious boolean set, or title == “Malicious Package”
Attribution tablesMalwareThreatActor, MalwareAttribution, shared ThreatActor + ThreatActorKey
Attribution basisrepo owner / container namespace / registry maintainer / Go-module repo / commit author, per the engine’s identity heuristics
Hijack handlingcompromised-account advisories mark the maintainer as hijack-victim-excluded (the victim is NOT attributed)
Impersonation trapfor non-Go packages, a declared repository is often the dependency-confusion / typosquat target (e.g. a legit org’s repo) — captured as claimedRepo*, NEVER attributed as an actor
GitHub key harvestpublic SSH-auth, SSH-signing, and GPG keys → ThreatActorKey (OpenSSH SHA256 fingerprint / GPG key id); a reused fingerprint links operators across accounts

The existing backlog of malicious records is cleared by the one-time cmd/malware-actor-backfill (just go-malware-actor-backfill), which selects isMaliciousPackage=true AND source<>'osm' across all malware sources.

Incremental Strategy

On startup, load all cveId values from CVEMetadata where source='snyk' into a map[string]bool. Per listing page: for each SNYK ID, skip if already known and --all/--force is false. Because the skip set is checked before the detail fetch, re-walking an already-ingested listing page costs one listing request and no detail requests.

Page frontier resume. The highest listing page reached is persisted as BulkDataDumpTracker.totalCVEs under source snyk_fetch_progress, and the next run starts at frontier - 5 and paginates forward until a page yields no SNYK hrefs (main.go:149-154).

Known defect. New Snyk disclosures appear at the front of /disclosed-vulnerabilities (the code comment at main.go:145-147 acknowledges that “new vulns shift older pages forward”). Starting at frontier - 5 therefore skips pages 1 … frontier-6, which is where all new content lands. Production evidence: the frontier row was last updated 2026-08-01, but max(lastFetchedAt) across source='snyk' rows is 2026-03-30 and max(datePublished) is 2026-03-22 — no new record has been stored in four months of weekly runs. Until this is fixed, treat the Snyk corpus as frozen at 3,081 records.

Rate limit: 600-900ms random delay between detail page fetches.

Locking: Uses BulkDataDumpTracker with key snyk_fetch_lock plus a 5-minute heartbeat to prevent concurrent instances. enrichment.CheckLock treats a lock row touched within the last 8 minutes as held.

Flags

FlagDefaultDescription
--allfalseReprocess all vulnerabilities (bypasses the known-ID skip set and the page-frontier resume)
--limit0Maximum vulnerabilities to process (0 = unlimited)
--forcefalseSame effect as --all
--backfill-refsfalseDB-only mode: re-reads every existing snyk CVEDescription, extracts URLs, classifies them, and inserts CVEMetadataReferences (referenceSource="Snyk"). No HTTP fetches, no locking, no notifier. Exposed as just go-snyk-fetch-backfill-refs.

Soft deadline

softDeadlineDuration defaults to 110 minutes and is applied unconditionally (main.go:74-78); EXPECTED_DURATION_MINUTES only overrides the value, it does not disable the deadline. The just go-snyk-fetch-backfill recipe unsets that env var, which means a local full-archive backfill is still cut off after 110 minutes rather than running to completion — contrary to the “backfill must not have a deadline” rule in scripts/go-processors/AGENTS.md.

Malware attribution post-pass

malwareactor.PostPass(ctx, pool, "snyk", …) runs after the lock is released (main.go:283), i.e. outside the ingest loop and outside any transaction. See the section below for what it writes.

ECS Schedule

Runs weekly on Saturdays at 04:00 UTC (cron(0 4 ? * SAT *)). CPU: 256, Memory: 512 MB, Expected duration: 120 minutes.

Key Files

FilePurpose
cmd/snyk-fetch-processor/main.goMain processor with pagination, page fetching, locking, frontier resume, --backfill-refs mode
internal/snyk/types.goSnykVuln data structure
internal/snyk/parser.goHTML extraction, __NUXT_DATA__ parsing, ExtractDescriptionURLs, ClassifyURL
internal/snyk/mapper.goSnykVuln to osv.CVESourceData mapping

Not wired

  • S3 archive / quarantine — no s3client.Uploader is constructed, so no raw payload is persisted at any point. This is the only processor in this batch that keeps nothing, and it is recorded as non-compliant in the compliance matrix.
  • internal/aienrich — no enricher is constructed, so the CWE / ATT&CK / TreeSitter passes never run for snyk records. Snyk publishes its own CWE and CVSS, so the gap is narrower here than for the vendor-bulletin scrapers.

S3 Persistence

Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.

Expected paths when implemented:

  • Archive: snyk/files/{sha256}/{filename}
  • Quarantine: failed-feeds/snyk-fetch-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Likely reasons: fetch-error, parse-error, store-error