Skip to content

feat(extract/fortinet/cvrf): supplement detection for historical advisories without product_statuses - #892

Open
shino wants to merge 9 commits into
nightlyfrom
feat/extract-fortinet-cvrf-supplement
Open

feat(extract/fortinet/cvrf): supplement detection for historical advisories without product_statuses#892
shino wants to merge 9 commits into
nightlyfrom
feat/extract-fortinet-cvrf-supplement

Conversation

@shino

@shino shino commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What

Adds an embedded supplement table to extract fortinet-cvrf so the historical CVRF advisories (2012–2022) whose product_statuses/product_tree are empty upstream still produce detection. 425 of the 465 statuses-empty advisories gain detection (680 product rows); with this, 1,068 of the 1,119 corpus files carry detection (previously 643).

Why

Fortinet only started populating product_statuses consistently in 2022, and CSAF only exists for recent advisories (effectively 2024+), so the 2012–2022 corpus extracted as content-only — a detection gap for exactly the advisories the retired handmade dataset used to cover.

How

  • pkg/extract/fortinet/cvrf/supplement_data.go — a frozen, generated Go-literal table (same pattern as microsoft/bulletin's compiled-in archive amendments) keyed by advisory ID: per product either exact versions, ranges, or neither (= whole product). Generated once from two Fortinet-authored sources and then hand-reviewed:
    • the curated version ranges of the legacy vuls-data-raw-fortinet (handmade) dataset — precise for 2012–2018, but its 2019+ entries are mostly constraint-less and sometimes list not-impacted products, so only version-constrained nodes are used;
    • the affected-version data of Fortinet's own CNA records in cvelistV5 (containers.cna.affected, assignerShortName: fortinet only) — covers 2016–2022, exactly where handmade degrades.
    • Per advisory the sources merge at product level (handmade wins per product, CNA fills missing products); disagreements and the 40 advisories left content-only (third-party CVEs, hardware-model-only listings, informational advisories) were reviewed individually.
  • supplement.go — builds criterions from the table in the same shapes the existing paths emit (exact versions → CPEMatches like the Known Affected path; ranges → per-product range type like the CSAF extractor), with the same whitelist + hard-error policy. cvrf.go falls back to it only when product_statuses is empty.
  • CPEs come exclusively from the existing internal/product whitelist (no CPEs are taken from cvelistV5), so the CPE vocabulary is identical to the CSAF/CVRF paths. 14 historical products are added to the table (CPE slugs from the legacy handmade curation) with per-product range types.
  • Normalizes vulnerability cve[] entries to bare CVE IDs — FG-IR-14-010 suffixes free text and embeds a stray HTML tag (CVE-<br />2014-2722 key issue), which previously became the vulnerability ID verbatim.
  • Adds FortiSIEMWindowsAgent (product tree branch of the new FG-IR-26-155) to the whitelist, which the current raw corpus requires to extract at all.

The gap is historical and closed going forward (2023+ statuses-empty files are third-party/informational only), so the table is a frozen asset, not a maintained feed.

Testing

  • GOEXPERIMENT=jsonv2 go test ./... passes.
  • New golden fixtures cover the three row shapes: range (FG-IR-012-001), CNA enumeration (FG-IR-21-085), whole-product (FG-IR-14-010); existing fixtures FG-IR-012-003 / FG-IR-13-008 now gain detection.
  • TestSupplementCriterions walks every embedded table entry and asserts it builds cleanly against the product table and numeric version scheme.
  • Full-corpus end-to-end: extract fortinet-cvrf over the current vuls-data-raw-fortinet-cvrf (1,119 files) completes without error.

The table generator (inputs fetch + generation + cross-source arbitration log) is published as a gist: https://gist.github.com/shino/5cefdef6d36283f50795e32ffcf1a51d

🤖 Generated with Claude Code

…sories without product_statuses

CVRF advisories from 2012 through 2022 (465 of 1,119 files) carry no
product_statuses/product_tree upstream, so extraction yielded content
without detection. Embed a frozen supplement table (425 advisories, 680
product rows) generated from two Fortinet-authored sources — the curated
version ranges of the legacy vuls-data-raw-fortinet (handmade) dataset
and the affected-version data of Fortinet's CNA records in cvelistV5 —
and fall back to it when product_statuses is empty. The remaining 40
advisories were reviewed and stay content-only (third-party CVEs,
hardware-model-only listings, informational advisories). The gap is
historical, so the table is a frozen asset, not a maintained feed.

Also normalize vulnerability cve[] entries to bare CVE IDs
(FG-IR-14-010 suffixes free text and embeds a stray HTML tag), and add
the product-table entries the supplement rows and the new
FortiSIEMWindowsAgent product tree branch of FG-IR-26-155 require.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the extract fortinet-cvrf pipeline to restore CPE detection for historical Fortinet CVRF advisories (2012–2022) where upstream product_statuses / product_tree are empty, by falling back to an embedded, frozen supplement table. It also normalizes messy cve[] entries into proper CVE IDs and expands the Fortinet product whitelist/range-type vocabulary to support additional historical products.

Changes:

  • Add embedded supplement.json + builder (supplement.go) and wire CVRF extraction to use it only when product_statuses.status.type is empty.
  • Normalize CVRF vulnerability.cve[] entries to bare CVE IDs via regex extraction (handling the known <br /> artifact).
  • Extend Fortinet product whitelist and Fortinet per-product range types; update/add golden + fixture coverage and add a whole-table validation test.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/extract/types/data/detection/condition/criteria/criterion/cpecriterion/range/range.go Adds new Fortinet per-product RangeType constants + JSON/string mappings + Compare dispatch entries.
pkg/extract/fortinet/internal/product/table.go Extends the Fortinet product-name → (CPE, RangeType) whitelist to include additional historical products.
pkg/extract/fortinet/cvrf/supplement.go Implements embedded supplement-table loading and conversion into detection criterions (exact versions / ranges / whole-product).
pkg/extract/fortinet/cvrf/supplement.json Frozen generated advisory→affected-product table used to supplement historical statuses-empty CVRF advisories.
pkg/extract/fortinet/cvrf/cvrf.go Falls back to supplement detection when product_statuses type is empty; normalizes CVE IDs using cvePattern.
pkg/extract/fortinet/cvrf/export_test.go Exposes supplement helpers to _test package for table-wide validation.
pkg/extract/fortinet/cvrf/cvrf_test.go Adds TestSupplementCriterions to ensure every supplement entry builds cleanly and unknown IDs return nil.
pkg/extract/fortinet/cvrf/testdata/golden/data/2012/FG-IR-012-001.json New/updated golden to cover supplemented range-shaped detection output.
pkg/extract/fortinet/cvrf/testdata/golden/data/2012/FG-IR-012-003.json Updated golden to reflect newly supplemented detection output.
pkg/extract/fortinet/cvrf/testdata/golden/data/2013/FG-IR-13-008.json Updated golden to reflect newly supplemented detection output (mixed exact/range/whole-product cases).
pkg/extract/fortinet/cvrf/testdata/golden/data/2014/FG-IR-14-010.json New golden validating whole-product detection + CVE ID normalization from messy cve[] strings.
pkg/extract/fortinet/cvrf/testdata/golden/data/2021/FG-IR-21-085.json New golden validating supplemented enumeration-shaped detection output.
pkg/extract/fortinet/cvrf/testdata/fixtures/2012/FG-IR-012-001.json Adds fixture exercising statuses-empty advisory structure.
pkg/extract/fortinet/cvrf/testdata/fixtures/2014/FG-IR-14-010.json Adds fixture containing the known malformed CVE strings (free text + <br />).
pkg/extract/fortinet/cvrf/testdata/fixtures/2021/FG-IR-21-085.json Adds fixture for a statuses-empty advisory that gains supplemented detection.

@shino
shino marked this pull request as ready for review July 22, 2026 06:28
…y exact string instead of lenient pattern

A pattern that recovers "the" CVE ID from arbitrary junk would silently
mis-handle a future malformed shape (e.g. keep only the first of two IDs
in one entry). Enumerate the three known FG-IR-14-010 entries as exact-
string fixups and hard-error on any other non-bare entry, matching the
whitelist policy used elsewhere in the extractor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 08:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Comment thread pkg/extract/fortinet/cvrf/cvrf.go Outdated
…teral instead of embedded JSON

Match the repo's established pattern for compiled-in frozen data
(microsoft/bulletin's archive amendments, microsoft/cvrf's per-CVE
overrides — pkg/extract has no other go:embed). A Go literal is also
checked at compile time, whereas a misspelled key in the embedded JSON
would be silently ignored by unmarshal and degrade that row to a
whole-product match. Extraction output is byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 08:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Comment thread pkg/extract/fortinet/cvrf/supplement_data.go Outdated
Comment thread pkg/extract/fortinet/cvrf/supplement.go
… in cve entry validation

Align with the nvd/feed/cve/v2 cveIDPattern; \d+ would accept 1-3 digit
sequences no real CVE ID has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 08:37
…instead of bare build 2312

FortiClient SSLVPN versions as "major.minor.build" over one monotonic
build sequence, but this advisory (and the handmade row) names only the
bare build: "build 2312 and lower", fixed in 2313. The bare bound
numerically swallows every full-form version (4 < 2312), including the
not-affected 4.0.2328 of FG-IR-16-048. The 4.0 prefix for build 2312 is
attested on both sides — 4.0.2012/4.0.2258 (FG-IR-13-008, 2013) and
4.0.2328/4.0.2332 (FG-IR-16-048, 2016; 4.4 starts at 4.4.2334) — so the
bound becomes le 4.0.2312. No other bare (dot-less) token remains in
the supplement table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

pkg/extract/fortinet/cvrf/supplement.go:109

  • supplementCriterions currently validates that bounds are parseable, but it does not validate that a constructed range is non-contradictory (e.g. GreaterEqual > LessEqual, or equal bounds with an exclusive end). This allows mistakes in the frozen supplement table to silently produce empty ranges and therefore silently drop detection.
		for _, sr := range row.Ranges {
			r := ccRangeTypes.Range{
				Type:         rt,
				GreaterEqual: sr.GreaterEqual,
				GreaterThan:  sr.GreaterThan,
				LessEqual:    sr.LessEqual,
				LessThan:     sr.LessThan,
			}
			for _, b := range []string{r.GreaterEqual, r.GreaterThan, r.LessEqual, r.LessThan} {
				if b == "" {
					continue
				}
				if _, err := numericVersion.NewVersion(b); err != nil {
					return nil, errors.Wrapf(err, "unexpected range bound %q for %q in supplement entry %q", b, row.Product, id)
				}
			}

Comment thread pkg/extract/fortinet/cvrf/supplement_data.go Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

pkg/extract/fortinet/cvrf/supplement.go:106

  • supplementCriterions validates that range bounds parse as numeric, but it doesn’t validate that the bounds form a sensible range (e.g., lower <= upper) or that mutually-exclusive fields (ge+gt, le+lt) aren’t both set. Since the frozen supplement table already contains inverted ranges, adding these checks would make bad table edits fail loudly and prevent silent false negatives.
			for _, b := range []string{r.GreaterEqual, r.GreaterThan, r.LessEqual, r.LessThan} {
				if b == "" {
					continue
				}
				if _, err := numericVersion.NewVersion(b); err != nil {

…ject them at build time

Two handmade-derived rows carried transposed bounds that made their
criterions unsatisfiable: FG-IR-22-056's FortiDeceptor 3.0/3.1 rows
(ge 3.0.2 le 3.0.0, ge 3.1.1 le 3.1.0 — the advisory note and the CNA
record both read 3.0.0-3.0.2 / 3.1.0-3.1.1, and the note lists 3.3.3 as
affected where handmade marked it fixed; the advisory now uses the CNA
rows, which also add FortiSandbox ranges handmade lacked) and
FG-IR-22-059's FortiWAN row (ge 4.4.0 le 4.3.1 — a corrupted "4.3 all
versions"; 4.3/4.4 are modeled as trains per the note).

supplementCriterions now rejects any range whose lower bound exceeds
its upper bound, so the whole-table test catches this class of table
error instead of shipping never-matching criterions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

pkg/extract/fortinet/cvrf/supplement.go:106

  • supplementCriterions validates that bounds parse as numeric versions, but it doesn’t validate that each range is logically satisfiable (lower bound <= upper bound). The current supplement table already contains inverted bounds, which will silently produce criterions that never match. Consider adding an explicit lower/upper ordering check here and failing extraction when a table entry is inconsistent.
				LessThan:     sr.LessThan,
			}
			for _, b := range []string{r.GreaterEqual, r.GreaterThan, r.LessEqual, r.LessThan} {
				if b == "" {
					continue

Copilot AI review requested due to automatic review settings July 22, 2026 08:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

…the advisory notes

A full version-level cross-check of every advisory covered by both the
legacy handmade dataset and the Fortinet CNA records (evaluating each
version token either source mentions against both sources' affected
predicates, then arbitrating every disagreement against the advisory's
Affected Products note) surfaced eight entries where the emitted
handmade rows contradicted the note:

- FG-IR-19-186: handmade carried 5.0.3-5.0.5, unrelated to this
  advisory (the CVE reads "6.2.1, 6.2.0, 6.0.8 and below") — CNA rows
- FG-IR-17-245: "5.2 and below" was cut off at ge 5.2.0
- FG-IR-19-034: "5.2.0 to 5.6.10" was cut off at ge 5.6.0
- FG-IR-20-082 / FG-IR-20-103: "6.0.10 and below" written as le 6.0.11,
  pulling the fixed release into the range
- FG-IR-20-158: 6.0 train stopped at 6.0.13 where the note says 6.0.x
  (CNA enumerates through 6.0.14)
- FG-IR-18-108: no versions in the note; CNA extends le 6.0.4 to 6.0.5
- FG-IR-18-232: "at least" note; CNA adds 5.6.0-5.6.11 for
  FortiAnalyzer/FortiManager

The other twenty-nine disagreements were arbitrated in handmade's
favor (CNA coarser trains/enumerations, boundary-token artifacts, or
CNA contradicting the note) and left as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Comment thread pkg/extract/fortinet/cvrf/cvrf_test.go
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 09:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Comment thread pkg/extract/fortinet/cvrf/cvrf.go Outdated
… comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants