CERT-FR Processor — Design Document
Overview
Fetches CERT-FR (French national CERT, ssi.gouv.fr) security advisories from the public alerte
and avis JSON feeds. Parses full advisory content — including French descriptions, affected
systems, and vendor references — and stores complete CVE records in CVEMetadata with
source="cert-fr". No API key required.
Processes two advisory types:
- alerte — active alerts (high-severity, time-sensitive)
- avis — informational notices
Data Sources
| Feed | URL | Type |
|---|---|---|
| Alerte index | https://cert.ssi.gouv.fr/alerte/json/ | JSON array (shallow) |
| Avis index | https://cert.ssi.gouv.fr/avis/json/ | JSON array (shallow) |
| Advisory detail | https://cert.ssi.gouv.fr{json_url} | JSON object (full) |
Processing Logic
- Fetch alerte + avis index feeds (shallow JSON arrays: reference + json_url + last_revision_date)
- Load per-advisory resume set:
cveId → sourceFileHashfromCVEMetadatawheresource='cert-fr' - For each advisory (up to
--workersin parallel, rate-limited to 5 req/s):- Fetch full advisory JSON from the per-item
json_url - Compute SHA256 of raw JSON bytes
- Skip if primary CVE’s stored hash matches (unchanged) — unless
--force - Parse advisory into
certfr.Advisorystruct - Skip if
cves[]is empty - Map to
[]*osv.CVESourceDataviacertfr.MapAdvisory - Store via
processor.StoreCVESourceDatain its own transaction (with retries)
- Fetch full advisory JSON from the per-item
- Exit non-zero if every advisory errored
Package Layout
internal/certfr/types.go
Data structures: IndexItem, Advisory, AdvisoryCVE, AdvisoryRisk, Revision,
VendorAdvisory, AffectedSystem, Product, Vendor.
internal/certfr/mapper.go
MapAdvisory(adv *Advisory, rawData []byte, advURL string) []*osv.CVESourceData- Dates from
revisions[](index 0 = published, last = updated) — format"2006-01-02T15:04:05.000000"UTC - State:
"CLOSED"ifclosed_atis non-empty, else"PUBLISHED" - Descriptions (lang=
"fr"):summarypreferred; falls back tocontent; then appends non-duplicate entries fromrisks[].description - References: advisory URL + all
vendor_advisories[].url(type=“advisory”) - Affected: one entry per unique
vendor + productfromaffected_systems[] - Multi-CVE bundle handling: a CERT-FR bulletin listing more than one CVE is bundling
co-listed CVEs, not asserting them as aliases of each other (commit
56bdb9b):- The first CVE is the primary (full record: title, refs, descriptions, affected).
- Each remaining CVE is upserted as its own minimal
CVEMetadatarow sharing the samesourceAdvisoryRef,title,state, dates, and references — but noDescriptions, noAffected, no inter-CVECVEAliasedges. - Cross-source linkage (cert-fr ⇄ cve.org / nist-nvd / certbund / …) for every CVE in
the bundle is still emitted automatically by
db.InsertAliasesvia the same-cveId cross-source backfill.
- Bundle-level scoring suppression (
SuppressDerivedMetrics): CERT-FRsummary/content/risks[].descriptionis bulletin-level prose. The sharedprocessor.StoreCVESourceDatapipeline normally derives a Vulnetix CVSS v4 vector fromDescriptions(cvss.DeriveV4FromDescription). For multi-CVE bulletins this produces a single vector that does not correspond to any specific CVE in the bundle yet would be attributed to the primary’s(cveId, source='cert-fr')row. To prevent this, the cert-fr mapper setsosv.CVESourceData.SuppressDerivedMetrics = trueon the primary record wheneverlen(adv.CVEs) > 1. Descriptions are still persisted toCVEDescription(lang='fr'); only the synthetic v4 metric is skipped. Single-CVE bulletins still receive the derived metric.
cmd/certfr-json-processor/main.go
Main binary with flags:
| Flag | Default | Description |
|---|---|---|
--force | false | Reprocess all advisories even if hash unchanged |
--limit | 0 | Max advisories to process (0 = unlimited) |
--workers | 5 | Concurrent advisory fetch workers |
scripts/task-manager.toml still declares an all flag for this task, which the
binary does not define — a dashboard run that passes --all exits 2 with
flag provided but not defined. The just go-certfr-json-backfill recipe also
defaults LIMIT=200, so a plain backfill invocation only touches the first 200
index entries.
cmd/certfr-json-processor/s3.go
buildS3Uploader — S3 PutObject closure using default AWS credential chain.
Storage Mapping
| Advisory field | DB table / column |
|---|---|
cves[0].name | CVEMetadata.cveId (primary CVE) |
"cert-fr" | CVEMetadata.source |
"5.0" | CVEMetadata.dataVersion |
"PUBLISHED" / "CLOSED" | CVEMetadata.state |
revisions[0].revision_date | CVEMetadata.datePublished (Unix seconds) |
revisions[-1].revision_date | CVEMetadata.dateUpdated (Unix seconds) |
title | CVEMetadata.title (French) |
| Advisory URL | CVEMetadata.sourceAdvisoryRef |
| Raw JSON bytes | CVEMetadata.rawDataJSON |
| SHA256(raw JSON) | CVEMetadata.sourceFileHash |
summary / content / risks[].description | CVEDescription (lang="fr") |
Advisory URL + vendor_advisories[].url | CVEMetadataReferences (type=“advisory”) |
affected_systems[].product.{name, vendor.name} | CVEAffected (vendor + product) — primary CVE only |
cves[1..].name | One additional CVEMetadata row per CVE — NOT written to CVEAlias |
| Raw JSON in S3 | Artifact (bomFormat="cert-fr", type="OTHER") + Link (PLAIN_JSON, pointing at the advisory URL) + CVEMetadata.fileLinkId — only when S3_BUCKET_NAME is set (main.go:315-356) |
Through the shared processor.StoreCVESourceData path, each affected entry also
feeds db.EnrichAffectedWithDependency, producing Dependency,
DependencyRegistry, PackageVersion and PackageVersionCVE rows.
Incremental / Resume Strategy
- Per-advisory resume:
LoadProcessedHashesloads(cveId → sourceFileHash)forsource='cert-fr'at startup. If the primary CVE’s stored hash matches the advisory JSON SHA256, the advisory is skipped. - Re-processing:
--forcebypasses the resume set (e.g. after description improvements). - Idempotent:
CVEMetadatausesON CONFLICT DO UPDATE;InsertDescriptionsdeletes then re-inserts, so lang is always accurate on re-process.InsertMetricslikewise deletes existing rows scoped to the(cveId, source, containerType, adpOrgId, metricType)tuples being written before inserting, so re-runs replace rather than accumulate duplicate metric rows. - Not idempotent:
db.InsertReferencesissues a bareON CONFLICT DO NOTHINGagainst a table whose only unique index is theuuidprimary key, so every re-store of an advisory appends duplicateCVEMetadataReferencesrows. The same applies toCVEProblemType.
Schedule
Runs daily at 01:00 UTC (cron(0 1 * * ? *) — terraform/go-schedules.tf:851).
ECS Fargate, 256 CPU / 512 MB RAM, expected_duration_minutes = 60.
The soft deadline is EXPECTED_DURATION_MINUTES - 10, but the fallback is a
hardcoded 55 minutes applied even when the variable is unset
(main.go:69-73), so a local backfill is also truncated at 55 minutes. Workers
that observe the deadline stop fetching and report the remaining advisories as
skipped rather than errored.
Verification Queries
-- CERT-FR CVE records
SELECT "cveId", "title", "state", "datePublished", "sourceAdvisoryRef"
FROM "CVEMetadata"
WHERE source = 'cert-fr'
ORDER BY "datePublished" DESC
LIMIT 20;
-- French descriptions
SELECT d."cveId", d.lang, LEFT(d.value, 120) AS description
FROM "CVEDescription" d
WHERE d.source = 'cert-fr'
LIMIT 20;
-- Vendor advisory references
SELECT "cveId", url, title
FROM "CVEMetadataReferences"
WHERE "referenceSource" = 'cert-fr'
LIMIT 20;
-- Affected products
SELECT "cveId", vendor, product
FROM "CVEAffected"
WHERE source = 'cert-fr'
LIMIT 20;
-- Lang distribution check
SELECT lang, COUNT(*) FROM "CVEDescription" WHERE source = 'cert-fr' GROUP BY lang;
S3 path deviation
The generated section below states the contract path. The archive is actually
written by a raw PutObject closure (s3.go:41-47) from inside the store
transaction, at certfr/advisories/{sha256}/{cveId}.json, and is paired with an
Artifact + Link row and CVEMetadata.fileLinkId (main.go:312-346) — not by
s3client.Uploader.Archive, and not under the cert-fr/files/ prefix. The
quarantine path is contract-conformant:
failed-feeds/certfr-json-processor/{YYYY-MM-DD}/parse-error/{reference}.json
(s3.go:28-29). Fetch failures and store-after-retry failures are counted but
their payloads are not quarantined.
S3 Persistence
- Archive path:
cert-fr/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/certfr-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
parse-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.