CERT-IT RSS Processor — Design
Overview
Fetches security advisories from CSIRT-ITA (Italian Computer Security Incident
Response Team, under ACN — Agenzia per la Cybersicurezza Nazionale) and creates
CVEMetadata rows (source=cert-it) under the original CVE IDs.
Advisories are in Italian. The <description> field is unstructured plain text;
structured fields (CVE IDs, severity, PoC status, exploitation status, product
name, CSIRT bulletin codes) are extracted by regex. Items with no CVE reference
are skipped.
Feed
| Property | Value |
|---|---|
| URL | https://www.acn.gov.it/portale/feedrss/-/journal/rss/20119/723192 |
| Auth | None — fully public |
| Format | RSS 2.0 with Dublin Core (dc:date) extension |
| Language | Italian (it) |
| Items | ~50 advisories (rolling window) |
| Encoding | UTF-8; typographic curly quotes used for severity labels |
Parsing
The <description> field contains Italian plain text. After HTML-unescaping
(the feed may encode entities as “/” or directly as UTF-8
curly quotes), the following signals are extracted:
| Field | Source | Method |
|---|---|---|
| Advisory URL | <link> | Direct |
| Title | <title> | Direct |
| Published | <dc:date> then <pubDate> | ISO 8601 / RFC 2822 |
| CVE IDs | Title + description | Regex CVE-\d{4}-\d{4,} |
| Severity | Description | Italian keyword near gravità (see below) |
| CVSS Score | Derived from severity | Sentinel values: CRITICAL=9.5, HIGH=8.0, MEDIUM=5.0, LOW=2.0 |
| Product | Title | Heuristic (see below) |
| PoC available | Title + description | Regex: proof of concept / PoC |
| Active exploitation | Title + description | Regex: sfruttamento attivo / 0-day |
| CSIRT bulletin IDs | Description | Regex [A-Z]{2}\d+/\d+/CSIRT-ITA |
| Content hash | guid|title|pubDate | SHA1 |
Severity extraction
The feed expresses severity as an Italian keyword embedded in free text, e.g.:
…una vulnerabilità con gravità "alta" in Docker Model Runner…
…con gravità "critica" e per la quale risulta disponibile…
The regex (?i)gravit[^\s]*\s+.{0,15}(critica|alta|media|bassa) tolerates
any quote style (straight ", curly "…", or none) between gravità and the
keyword. Mapping:
| Italian keyword | Severity label | Representative CVSS score |
|---|---|---|
critica | CRITICAL | 9.5 |
alta | HIGH | 8.0 |
media | MEDIUM | 5.0 |
bassa | LOW | 2.0 |
No CVSS vector string is present in the feed.
Product extraction from title
Three heuristic patterns tried in order:
- Colon prefix —
^(.+?):\s+\S— text before the first colon is the product (e.g.Langflow: rilevata 0-day…→Langflow) - After “in”/“per” —
vulnerabilità in X/aggiornamenti per X→X - Supply-chain —
versioni malevole su X/libreria Python X→X
Italian qualifier prefixes (prodotti, firmware, software, libreria, etc.)
are stripped from the result (e.g. prodotti Fortinet → Fortinet).
ID Strategy
All items reference upstream CVE IDs — no VDB minting needed. Multi-CVE
advisories create one CVEMetadata row per CVE with all others listed as
aliases. Items without any CVE ID are skipped (skippedNoCVE counter).
Storage
No new tables or columns. All tables already exist.
| Table | Rows inserted |
|---|---|
CVEMetadata | One per CVE ID; source="cert-it", cveId=CVE-YYYY-NNNN |
CVEDescription | One per CVE; containerType="cna", lang="it" |
CVEMetadataReferences | Advisory page URL (type="advisory") |
CVEMetric | Severity-derived sentinel score when severity is known (metricType="cvssV3_1", no vector) — see caveat below |
CVEAffected | Product extracted from title, vendor derived by keyword lookup |
CVEAlias | Cross-references between CVEs within the same advisory — only for two-CVE advisories; db.InsertAliases drops CVE↔CVE edges when a CVE-prefixed primary carries ≥2 CVE aliases (internal/db/cvealias.go:65). Same-cveId cross-source edges are always written. |
The stored score is synthetic. 9.5 / 8.0 / 5.0 / 2.0 are stand-ins for the Italian severity keyword, not a computed CVSS base score, yet they are written as an ordinary
cvssV3_1baseScorewithcontainerType="cna"(internal/certit/mapper.go:64-75). A consumer averaging or comparingbaseScoreacross sources cannot distinguish them from measured scores.CVEMetadata.vectorStringis NULL for every row, correctly — the feed carries no vector.
rawDataJSON captures all parsed fields including poc_available,
active_exploit, and bulletin_ids for downstream use.
Incremental Strategy
On startup, load all sourceAdvisoryRef values from CVEMetadata where
source='cert-it' into a map[string]bool. Per advisory: if the URL is
already in the set and neither --all nor --force is set, skip. After
successful processing, add the URL to the in-memory set.
Flags
| Flag | Default | Description |
|---|---|---|
--all | false | Reprocess all advisories, not just new ones |
--limit | 0 | Maximum advisories to process (0 = unlimited) |
--force | false | Force reprocessing even if content unchanged |
Error Alerting
All lifecycle events are published to SNS → Lambda → Slack via the shared
internal/notify package:
| Event | When published | Slack behaviour |
|---|---|---|
task.started | Processor start | No Slack message (informational) |
task.completed | Clean finish, no errors | No Slack message |
task.errored | Feed fetch/parse failure or any DB transaction failure | Posts error alert to Slack with processor name, error text, and deep-links to CloudWatch and ECS console |
task.overtime | Exceeds EXPECTED_DURATION_MINUTES | Posts overtime warning to Slack |
Per-item DB errors are accumulated via notifier.RecordError() and flushed in
a single task.errored SNS event via notifier.HasErrors() at the end of the
processing loop. This means a partial run that stored some records but failed
on others still triggers a Slack alert.
Fatal errors (feed fetch failure, feed parse failure) call notifier.Errored()
immediately and os.Exit(1).
ECS Schedule
Runs once daily at 06:00 UTC (cron(0 6 * * ? *)).
| Resource | Value |
|---|---|
| EventBridge schedule | go-cert-it-rss-processor |
| ECS task family | go-cert-it-rss-processor |
| CloudWatch log group | /ecs/vdb-scheduler/go-cert-it-rss-processor |
| ECR image tag | go-cert-it-rss-processor-latest |
| CPU / Memory | 256 / 512 MB |
| Expected duration | 30 min |
Local Testing
# Backfill against prod (first 10 items)
just go-cert-it-rss-backfill prod false 10 false
# Full reprocess against prod
just go-cert-it-rss-backfill prod true 0 false
# Run in local ARM64 container (mirrors ECS)
just cert-it-rss-processor prod
Key Files
| File | Purpose |
|---|---|
cmd/cert-it-rss-processor/main.go | Main processor binary |
internal/certit/types.go | RSS feed and Advisory Go structs |
internal/certit/parser.go | Feed parsing, Italian text extraction |
internal/certit/mapper.go | Advisory → CVESourceData mapping |
S3 Persistence
- Archive path:
cert-it/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/cert-it-rss-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
parse-error,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.