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

PropertyValue
URLhttps://www.acn.gov.it/portale/feedrss/-/journal/rss/20119/723192
AuthNone — fully public
FormatRSS 2.0 with Dublin Core (dc:date) extension
LanguageItalian (it)
Items~50 advisories (rolling window)
EncodingUTF-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 &ldquo;/&rdquo; or directly as UTF-8 curly quotes), the following signals are extracted:

FieldSourceMethod
Advisory URL<link>Direct
Title<title>Direct
Published<dc:date> then <pubDate>ISO 8601 / RFC 2822
CVE IDsTitle + descriptionRegex CVE-\d{4}-\d{4,}
SeverityDescriptionItalian keyword near gravità (see below)
CVSS ScoreDerived from severitySentinel values: CRITICAL=9.5, HIGH=8.0, MEDIUM=5.0, LOW=2.0
ProductTitleHeuristic (see below)
PoC availableTitle + descriptionRegex: proof of concept / PoC
Active exploitationTitle + descriptionRegex: sfruttamento attivo / 0-day
CSIRT bulletin IDsDescriptionRegex [A-Z]{2}\d+/\d+/CSIRT-ITA
Content hashguid|title|pubDateSHA1

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 keywordSeverity labelRepresentative CVSS score
criticaCRITICAL9.5
altaHIGH8.0
mediaMEDIUM5.0
bassaLOW2.0

No CVSS vector string is present in the feed.

Product extraction from title

Three heuristic patterns tried in order:

  1. Colon prefix^(.+?):\s+\S — text before the first colon is the product (e.g. Langflow: rilevata 0-day…Langflow)
  2. After “in”/“per”vulnerabilità in X / aggiornamenti per XX
  3. Supply-chainversioni malevole su X / libreria Python XX

Italian qualifier prefixes (prodotti, firmware, software, libreria, etc.) are stripped from the result (e.g. prodotti FortinetFortinet).

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.

TableRows inserted
CVEMetadataOne per CVE ID; source="cert-it", cveId=CVE-YYYY-NNNN
CVEDescriptionOne per CVE; containerType="cna", lang="it"
CVEMetadataReferencesAdvisory page URL (type="advisory")
CVEMetricSeverity-derived sentinel score when severity is known (metricType="cvssV3_1", no vector) — see caveat below
CVEAffectedProduct extracted from title, vendor derived by keyword lookup
CVEAliasCross-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_1 baseScore with containerType="cna" (internal/certit/mapper.go:64-75). A consumer averaging or comparing baseScore across sources cannot distinguish them from measured scores. CVEMetadata.vectorString is 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

FlagDefaultDescription
--allfalseReprocess all advisories, not just new ones
--limit0Maximum advisories to process (0 = unlimited)
--forcefalseForce reprocessing even if content unchanged

Error Alerting

All lifecycle events are published to SNS → Lambda → Slack via the shared internal/notify package:

EventWhen publishedSlack behaviour
task.startedProcessor startNo Slack message (informational)
task.completedClean finish, no errorsNo Slack message
task.erroredFeed fetch/parse failure or any DB transaction failurePosts error alert to Slack with processor name, error text, and deep-links to CloudWatch and ECS console
task.overtimeExceeds EXPECTED_DURATION_MINUTESPosts 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 * * ? *)).

ResourceValue
EventBridge schedulego-cert-it-rss-processor
ECS task familygo-cert-it-rss-processor
CloudWatch log group/ecs/vdb-scheduler/go-cert-it-rss-processor
ECR image taggo-cert-it-rss-processor-latest
CPU / Memory256 / 512 MB
Expected duration30 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

FilePurpose
cmd/cert-it-rss-processor/main.goMain processor binary
internal/certit/types.goRSS feed and Advisory Go structs
internal/certit/parser.goFeed parsing, Italian text extraction
internal/certit/mapper.goAdvisory → 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).

flowchart LR SRC[Source feed] --> PROC[cert-it-rss-processor] PROC -->|success| ARCHIVE[("S3: cert-it/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/cert-it-rss-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.