From e63b3a354ed77b6f380a439b6212247e670c0c35 Mon Sep 17 00:00:00 2001 From: Shunichi Shinohara Date: Tue, 30 Jun 2026 14:57:34 +0900 Subject: [PATCH 1/4] feat(detector): route Fortinet to vuls2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the Cisco / Palo Alto migrations: move Fortinet CPE detection from go-cve-dictionary to the vuls2 path. Fortinet now comes from the two new extractors — fortinet-cvrf (merged) and fortinet-csaf (MaineK00n/vuls-data-update #853) — both rooted on the FG-IR PSIRT advisory. - detector.go: drop Fortinet from the go-cve-dictionary admission gate and the FillCvesWithGoCVEDictionary CveContent fill, remove the DistroAdvisory synthesis, and nil detail.Fortinets before confidence selection (same as NVD / Cisco / Palo Alto). - vuls2/vendor.go: extend every Fortinet source switch (toCveContentType, toVuls0Confidence x2, advisoryReference, compareSourceID) to cover FortinetCVRF and FortinetCSAF, with CSAF preferred over CVRF in compareSourceID (tier 5 vs 4, like the Cisco/Palo Alto CSAF>CVRF/JSON order). cveContentSourceLink gains a models.Fortinet case returning the PSIRT advisory page keyed by the root ID (FG-IR-...), matching advisoryReference. Fortinet is advisory-rooted like Cisco. The CVRF stub is bare (sparse CveContent + DistroAdvisory); the CSAF stub is rich (CVSS/CWE/refs), so the generic path builds a full CveContent. Either way the advisory carries no vendor SIR (Fortinet severity is CVSS), so DistroAdvisory.Severity is empty. No enrich. Bumps vuls-data-update to MaineK00n/vuls-data-update#853 head (3263815) for the FortinetCSAF source id and the fortinet-* cpecriterion RangeTypes; keeps the pan-os RangeType from the Palo Alto work. Co-Authored-By: Claude Opus 4.8 --- detector/detector.go | 33 +++---- detector/vuls2/vendor.go | 16 +-- detector/vuls2/vuls2_test.go | 187 +++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 6 +- 5 files changed, 214 insertions(+), 31 deletions(-) diff --git a/detector/detector.go b/detector/detector.go index 6c655542bf..f7c1db0dc5 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -397,12 +397,12 @@ func DetectWordPressCves(r *models.ScanResult, wpCnf config.WpScanConf) error { return nil } -// FillCvesWithGoCVEDictionary fills CVE detail with VulnCheck, JVN, Fortinet +// FillCvesWithGoCVEDictionary fills CVE detail with VulnCheck, JVN // (NVD CveContent, EUVD, and MITRE are filled by the vuls2 enrich path instead, as are the US-CERT -// alerts; Cisco and Palo Alto are detected by vuls2, which always emits a CveContent and -// additionally a DistroAdvisory when the matched root carries advisories (Cisco roots always do; -// Palo Alto's PAN-SA roots do — their advisory fallback yields both a DistroAdvisory and an -// advisory-derived CveContent — while its plain CVE roots yield a CveContent only); only the +// alerts; Cisco, Palo Alto, and Fortinet are detected by vuls2, which always emits a CveContent and +// additionally a DistroAdvisory when the matched root carries advisories (Cisco and Fortinet roots +// always do; Palo Alto's PAN-SA roots do — their advisory fallback yields both a DistroAdvisory and +// an advisory-derived CveContent — while its plain CVE roots yield a CveContent only); only the // JP-CERT alerts, derived from JVN, still come from here) func FillCvesWithGoCVEDictionary(r *models.ScanResult, cnf config.GoCveDictConf, logOpts logging.LogOpts) (err error) { cveIDs := make([]string, 0, len(r.ScannedCves)) @@ -428,7 +428,6 @@ func FillCvesWithGoCVEDictionary(r *models.ScanResult, cnf config.GoCveDictConf, for _, d := range ds { vulnchecks := models.ConvertVulncheckToModel(d.CveID, d.Vulnchecks) jvns := models.ConvertJvnToModel(d.CveID, d.Jvns) - fortinets := models.ConvertFortinetToModel(d.CveID, d.Fortinets) alerts := fillCertAlerts(&d) for cveID, vinfo := range r.ScannedCves { @@ -444,7 +443,7 @@ func FillCvesWithGoCVEDictionary(r *models.ScanResult, cnf config.GoCveDictConf, for _, con := range vulnchecks { vinfo.CveContents[con.Type] = append(vinfo.CveContents[con.Type], con) } - for _, cons := range [][]models.CveContent{jvns, fortinets} { + for _, cons := range [][]models.CveContent{jvns} { for _, con := range cons { if !con.Empty() { if !slices.ContainsFunc(vinfo.CveContents[con.Type], func(e models.CveContent) bool { @@ -542,29 +541,18 @@ func detectCpeURIsCvesWithGoCVEDictionary(r *models.ScanResult, cpes []Cpe, cnf for _, detail := range details { // Skip detections carried by no dictionary-remaining DETECTION // source. The list mirrors go-cve-dictionary's GetByCpeURI - // admission gate minus the vuls2-migrated sources (NVD, Cisco, and - // Palo Alto), so NVD-only / Cisco-only / Palo Alto-only detections - // disappear here — vuls2 re-detects them from its own data. EUVD / + // admission gate minus the vuls2-migrated sources (NVD, Cisco, + // Palo Alto, and Fortinet), so detections carried only by those + // sources disappear here — vuls2 re-detects them from its own data. EUVD / // MITRE contents can ride along on a detail but are never a // detection basis (gocve neither matches nor admits on them, and // getMaxConfidence has no tier for them), so they do not keep // a detail alive. - if !detail.HasJvn() && !detail.HasFortinet() && !detail.HasVulncheck() { + if !detail.HasJvn() && !detail.HasVulncheck() { continue } advisories := []models.DistroAdvisory{} - if detail.HasFortinet() { - for _, fortinet := range detail.Fortinets { - advisories = append(advisories, models.DistroAdvisory{ - AdvisoryID: fortinet.AdvisoryID, - Issued: fortinet.PublishedDate, - Updated: fortinet.LastModifiedDate, - Description: fortinet.Summary, - }) - } - } - // JVN advisories are redundant for CVEs that NVD also covers. if !detail.HasNvd() && detail.HasJvn() { for _, jvn := range detail.Jvns { @@ -586,6 +574,7 @@ func detectCpeURIsCvesWithGoCVEDictionary(r *models.ScanResult, cpes []Cpe, cnf detail.Nvds = nil detail.Ciscos = nil detail.Paloaltos = nil + detail.Fortinets = nil maxConfidence := getMaxConfidence(detail) if val, ok := r.ScannedCves[detail.CveID]; ok { diff --git a/detector/vuls2/vendor.go b/detector/vuls2/vendor.go index 098b26505d..80b12fea80 100644 --- a/detector/vuls2/vendor.go +++ b/detector/vuls2/vendor.go @@ -510,7 +510,7 @@ func advisoryReference(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID, da mo Source: "JVN", RefID: da.AdvisoryID, }, nil - case sourceTypes.Fortinet: + case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.Reference{ Link: fmt.Sprintf("https://www.fortiguard.com/psirt/%s", da.AdvisoryID), Source: "FORTINET", @@ -642,6 +642,10 @@ func cveContentSourceLink(ccType models.CveContentType, v vulnerabilityTypes.Vul // Palo Alto publishes a page per CVE, so the source link is keyed by the // CVE ID (which is the vulnerability content ID, e.g. CVE-2022-0778). return fmt.Sprintf("https://security.paloaltonetworks.com/%s", v.Content.ID) + case models.Fortinet: + // Fortinet content lives in the PSIRT advisory, whose ID is the root ID + // (e.g. FG-IR-24-041), so the per-CVE source link points at that page. + return fmt.Sprintf("https://www.fortiguard.com/psirt/%s", rootID) case models.RedHat, models.RedHatAPI: return fmt.Sprintf("https://access.redhat.com/security/cve/%s", v.Content.ID) case models.Oracle: @@ -696,9 +700,9 @@ func compareSourceID(e ecosystemTypes.Ecosystem, a, b sourceTypes.SourceID) int case ecosystemTypes.EcosystemTypeCPE: preferenceFn := func(sourceID sourceTypes.SourceID) int { switch sourceID { - case sourceTypes.NVDAPICVE, sourceTypes.JVNFeedDetail, sourceTypes.Fortinet, sourceTypes.PaloAltoCSAF, sourceTypes.CiscoCSAF: + case sourceTypes.NVDAPICVE, sourceTypes.JVNFeedDetail, sourceTypes.Fortinet, sourceTypes.FortinetCSAF, sourceTypes.PaloAltoCSAF, sourceTypes.CiscoCSAF: return 5 - case sourceTypes.NVDFeedCVEv2, sourceTypes.JVNFeedRSS, sourceTypes.PaloAltoJSON, sourceTypes.CiscoCVRF: + case sourceTypes.NVDFeedCVEv2, sourceTypes.JVNFeedRSS, sourceTypes.FortinetCVRF, sourceTypes.PaloAltoJSON, sourceTypes.CiscoCVRF: return 4 case sourceTypes.NVDFeedCVEv1: return 3 @@ -879,7 +883,7 @@ func toCveContentType(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID) models return models.Nvd case sourceTypes.JVNFeedRSS, sourceTypes.JVNFeedDetail: return models.Jvn - case sourceTypes.Fortinet: + case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.Fortinet case sourceTypes.PaloAltoCSAF, sourceTypes.PaloAltoJSON, sourceTypes.PaloAltoList: return models.Paloalto @@ -1042,7 +1046,7 @@ func toVuls0Confidence(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID, sd so return models.NvdVendorProductMatch case sourceTypes.JVNFeedRSS, sourceTypes.JVNFeedDetail: return models.JvnVendorProductMatch - case sourceTypes.Fortinet: + case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.FortinetVendorProductMatch case sourceTypes.PaloAltoCSAF, sourceTypes.PaloAltoJSON, sourceTypes.PaloAltoList: return models.PaloaltoVendorProductMatch @@ -1067,7 +1071,7 @@ func toVuls0Confidence(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID, sd so // VendorProductMatch branch above. Mapped to JvnVendorProductMatch // for safety should that demotion ever change. return models.JvnVendorProductMatch - case sourceTypes.Fortinet: + case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.FortinetExactVersionMatch case sourceTypes.PaloAltoCSAF, sourceTypes.PaloAltoJSON, sourceTypes.PaloAltoList: return models.PaloaltoExactVersionMatch diff --git a/detector/vuls2/vuls2_test.go b/detector/vuls2/vuls2_test.go index 6c1a003dc8..b14e698a46 100644 --- a/detector/vuls2/vuls2_test.go +++ b/detector/vuls2/vuls2_test.go @@ -9590,6 +9590,193 @@ func Test_postConvert(t *testing.T) { }, }, }, + { + // Fortinet is advisory-shaped (root = the FG-IR PSIRT advisory). The + // CSAF extractor puts rich content (CVSS, CWE, refs) on the + // vulnerability stub, so the generic path builds a full Fortinet + // CveContent whose source link points at the PSIRT advisory page + // (keyed by the root ID). The advisory itself carries no vendor SIR + // (Fortinet severity is CVSS), so DistroAdvisory.Severity is empty. + name: "cpe fortinet (csaf) detection emits CveContent (psirt source link) + DistroAdvisory", + args: args{ + scanned: scanTypes.ScanResult{ + CPE: []string{ + "cpe:2.3:a:fortinet:fortipam:1.0.0:*:*:*:*:*:*:*", + }, + }, + fsToOriginalCPE: map[string][]string{ + "cpe:2.3:a:fortinet:fortipam:1.0.0:*:*:*:*:*:*:*": {"cpe:/a:fortinet:fortipam:1.0.0", "cpe:2.3:a:fortinet:fortipam:1.0.0:*:*:*:*:*:*:*"}, + }, + detected: detectTypes.DetectResult{ + Detected: []detectTypes.VulnerabilityData{ + { + ID: "FG-IR-24-041", + Advisories: []dbTypes.VulnerabilityDataAdvisory{ + { + ID: "FG-IR-24-041", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]advisoryTypes.Advisory{ + sourceTypes.FortinetCSAF: { + dataTypes.RootID("FG-IR-24-041"): []advisoryTypes.Advisory{ + { + Content: advisoryContentTypes.Content{ + ID: "FG-IR-24-041", + Title: "FGFM protocol allows unauthenticated reset of the connection", + Published: new(time.Date(2025, 10, 14, 0, 0, 0, 0, time.UTC)), + Modified: new(time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC)), + }, + Segments: []segmentTypes.Segment{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + }, + }, + }, + }, + }, + }, + }, + }, + Vulnerabilities: []dbTypes.VulnerabilityDataVulnerability{ + { + ID: "CVE-2024-26008", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]vulnerabilityTypes.Vulnerability{ + sourceTypes.FortinetCSAF: { + dataTypes.RootID("FG-IR-24-041"): []vulnerabilityTypes.Vulnerability{ + { + Content: vulnerabilityContentTypes.Content{ + ID: "CVE-2024-26008", + Title: "FGFM protocol allows unauthenticated reset of the connection", + Description: "Improper check or handling of exceptional conditions vulnerability in FortiOS, FortiProxy, FortiPAM & FortiSwitchManager", + Severity: []severityTypes.Severity{ + { + Type: severityTypes.SeverityTypeVendor, + Source: "fortiguard.fortinet.com", + Vendor: new("Denial of service"), + }, + { + Type: severityTypes.SeverityTypeCVSSv31, + Source: "fortiguard.fortinet.com", + CVSSv31: new(cvssV31Types.CVSSv31{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/E:P/RL:X/RC:C", + BaseScore: 5.3, + BaseSeverity: "MEDIUM", + TemporalScore: 5, + TemporalSeverity: "MEDIUM", + EnvironmentalScore: 5, + EnvironmentalSeverity: "MEDIUM", + }), + }, + }, + CWE: []cweTypes.CWE{ + { + Source: "fortiguard.fortinet.com", + CWE: []string{"CWE-754"}, + }, + }, + References: []referenceTypes.Reference{ + { + Source: "fortiguard.fortinet.com", + URL: "https://fortiguard.fortinet.com/psirt/FG-IR-24-041", + }, + }, + }, + Segments: []segmentTypes.Segment{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + }, + }, + }, + }, + }, + }, + }, + }, + Detections: []detectTypes.VulnerabilityDataDetection{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + Contents: map[sourceTypes.SourceID][]conditionTypes.FilteredCondition{ + sourceTypes.FortinetCSAF: { + { + Criteria: criteriaTypes.FilteredCriteria{ + Operator: criteriaTypes.CriteriaOperatorTypeOR, + Criterions: []criterionTypes.FilteredCriterion{ + { + Criterion: criterionTypes.Criterion{ + Type: criterionTypes.CriterionTypeCPE, + CPE: new(ccTypes.Criterion{ + Vulnerable: true, + FixStatus: new(vcFixStatusTypes.FixStatus{ + Class: vcFixStatusTypes.ClassUnknown, + }), + CPE: ccTypes.CPE("cpe:2.3:a:fortinet:fortipam:*:*:*:*:*:*:*:*"), + Range: new(ccRangeTypes.Range{ + Type: ccRangeTypes.RangeTypeFortinetFortiPAM, + GreaterEqual: "1.0", + LessThan: "1.1", + }), + }), + }, + Accepts: criterionTypes.AcceptQueries{ + CPE: criterionTypes.CPEAccepts{Exact: []int{0}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: models.VulnInfos{ + "CVE-2024-26008": { + CveID: "CVE-2024-26008", + Confidences: models.Confidences{models.FortinetExactVersionMatch}, + CpeURIs: []string{"cpe:/a:fortinet:fortipam:1.0.0", "cpe:2.3:a:fortinet:fortipam:1.0.0:*:*:*:*:*:*:*"}, + DistroAdvisories: models.DistroAdvisories{ + { + AdvisoryID: "FG-IR-24-041", + // Severity empty: Fortinet advisory severity is CVSS, not a vendor SIR. + Issued: time.Date(2025, 10, 14, 0, 0, 0, 0, time.UTC), + Updated: time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC), + }, + }, + CveContents: models.CveContents{ + models.Fortinet: []models.CveContent{ + { + Type: models.Fortinet, + CveID: "CVE-2024-26008", + Title: "FGFM protocol allows unauthenticated reset of the connection", + Summary: "Improper check or handling of exceptional conditions vulnerability in FortiOS, FortiProxy, FortiPAM & FortiSwitchManager", + Cvss3Score: 5.3, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/E:P/RL:X/RC:C", + Cvss3Severity: "MEDIUM", + SourceLink: "https://www.fortiguard.com/psirt/FG-IR-24-041", + References: models.References{ + { + Link: "https://fortiguard.fortinet.com/psirt/FG-IR-24-041", + Source: "MISC", + }, + { + Link: "https://www.fortiguard.com/psirt/FG-IR-24-041", + Source: "FORTINET", + RefID: "FG-IR-24-041", + }, + }, + CweIDs: []string{"CWE-754"}, + Published: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + LastModified: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + Optional: map[string]string{ + "vuls2-sources": "[{\"root_id\":\"FG-IR-24-041\",\"source_id\":\"fortinet-csaf\",\"segment\":{\"ecosystem\":\"cpe\"}}]", + }, + }, + }, + }, + }, + }, + }, { // A criterion accepted the query only at version-unconfirmed // quality (the upstream matcher could not confirm the scanned diff --git a/go.mod b/go.mod index 5b62e92867..1c34a26865 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 github.com/BurntSushi/toml v1.6.0 github.com/CycloneDX/cyclonedx-go v0.10.0 - github.com/MaineK00n/vuls-data-update v0.0.0-20260626055850-7e818eb2ba51 + github.com/MaineK00n/vuls-data-update v0.0.0-20260630053343-3263815ecdb6 github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260629073543-a01483fb1da0 github.com/Ullaakut/nmap/v2 v2.2.2 github.com/aquasecurity/trivy v0.70.0 @@ -303,6 +303,7 @@ require ( github.com/vektah/gqlparser/v2 v2.5.32 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/vulsio/go-fortinet-version v0.0.0-20260629092944-998bd70fcdc5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index 4868bf7390..066ab74849 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ github.com/MaineK00n/go-microsoft-version v0.0.0-20260325021654-1d9206bdeffc h1: github.com/MaineK00n/go-microsoft-version v0.0.0-20260325021654-1d9206bdeffc/go.mod h1:GNf+Vhnxk8/pW56jsxAeFCBP0VCgVQlLIJ812UnAj9c= github.com/MaineK00n/go-paloalto-version v0.0.0-20260324210858-f043171ba2bf h1:QDz92+GEPbIR4HTw2EGNgYqH+T9jxmO/OvFMfY715PE= github.com/MaineK00n/go-paloalto-version v0.0.0-20260324210858-f043171ba2bf/go.mod h1:ELOxzfAd4oAe4niMmoZlSiJwzf1DF+DjNdjsUcuqAR8= -github.com/MaineK00n/vuls-data-update v0.0.0-20260626055850-7e818eb2ba51 h1:ADt0VRzK5n+x+VGUpvr+k/9ATgoqovvrM3lJiuvFkP4= -github.com/MaineK00n/vuls-data-update v0.0.0-20260626055850-7e818eb2ba51/go.mod h1:RiMrqmCnmpsz3esG0r6iCbS5EIgDnm9yW1ZV3f1+RjQ= +github.com/MaineK00n/vuls-data-update v0.0.0-20260630053343-3263815ecdb6 h1:SuG73UZGjEE34mAtePCvSafN/AnvhT+Z9SicRQk8os8= +github.com/MaineK00n/vuls-data-update v0.0.0-20260630053343-3263815ecdb6/go.mod h1:5QVGDCspc2SuU6aHQ99W8boUmpspqLyGJ9uTPGARa9A= github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260629073543-a01483fb1da0 h1:E7A6LDY5P9iHqNpoJkaz0cZVLuBiJ2N/9IiAPGWFGyE= github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260629073543-a01483fb1da0/go.mod h1:hsRv4IvqFBs3YMjlEYfPbKv7Ft+pVaIJh8HvkREhGRo= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -880,6 +880,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vulsio/go-cve-dictionary v0.16.2 h1:1sWXzHBXV55PoOYKiWxqtR9OHq8vfNf3kg1XeNeT7Mw= github.com/vulsio/go-cve-dictionary v0.16.2/go.mod h1:cD+HMRAavPw395gGBIa+TOkhOPGRYXkuw94Y31wetRk= +github.com/vulsio/go-fortinet-version v0.0.0-20260629092944-998bd70fcdc5 h1:enHWbWE2opbbaRgNVXvuSkcPwEUM8wtQzRuQt3S2AkM= +github.com/vulsio/go-fortinet-version v0.0.0-20260629092944-998bd70fcdc5/go.mod h1:6ERUhlKxZIKCSBNdWforXA62XUpWhks58due00b9IUM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= From ac7ef71dd7ba2936a5f41e0b65f726370909b9ae Mon Sep 17 00:00:00 2001 From: Shunichi Shinohara Date: Tue, 30 Jun 2026 15:20:15 +0900 Subject: [PATCH 2/4] test(detector/vuls2): cover Fortinet cvrf + csaf/cvrf both-detect; drop untested bare Fortinet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a CVRF-only postConvert case: a bare vulnerability stub yields a sparse Fortinet CveContent (no CVSS) plus a DistroAdvisory, source link to the PSIRT advisory (root id), DistroAdvisory.Severity empty (advisory CVSS, not vendor). - Add a csaf+cvrf both-detect case: the same FG-IR advisory / CVE is detected by both sources and merges to the csaf-preferred result — csaf scalars (title / summary / CVSS) win, the DistroAdvisory takes csaf's (later Updated), and vuls2-sources lists both. Verifies the compareSourceID CSAF > CVRF ordering. - Drop the bare sourceTypes.Fortinet from the source switches: only FortinetCVRF / FortinetCSAF are produced into the vuls2 DB and exercised by tests; the legacy "fortinet" source id is no longer routed. Built the real extracted data (vuls-data-update extract fortinet-cvrf/csaf from the GHCR raw) to model these on FG-IR-25-032 / CVE-2025-54838. Co-Authored-By: Claude Opus 4.8 --- detector/vuls2/vendor.go | 10 +- detector/vuls2/vuls2_test.go | 381 +++++++++++++++++++++++++++++++++++ 2 files changed, 386 insertions(+), 5 deletions(-) diff --git a/detector/vuls2/vendor.go b/detector/vuls2/vendor.go index 80b12fea80..6b03837f07 100644 --- a/detector/vuls2/vendor.go +++ b/detector/vuls2/vendor.go @@ -510,7 +510,7 @@ func advisoryReference(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID, da mo Source: "JVN", RefID: da.AdvisoryID, }, nil - case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: + case sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.Reference{ Link: fmt.Sprintf("https://www.fortiguard.com/psirt/%s", da.AdvisoryID), Source: "FORTINET", @@ -700,7 +700,7 @@ func compareSourceID(e ecosystemTypes.Ecosystem, a, b sourceTypes.SourceID) int case ecosystemTypes.EcosystemTypeCPE: preferenceFn := func(sourceID sourceTypes.SourceID) int { switch sourceID { - case sourceTypes.NVDAPICVE, sourceTypes.JVNFeedDetail, sourceTypes.Fortinet, sourceTypes.FortinetCSAF, sourceTypes.PaloAltoCSAF, sourceTypes.CiscoCSAF: + case sourceTypes.NVDAPICVE, sourceTypes.JVNFeedDetail, sourceTypes.FortinetCSAF, sourceTypes.PaloAltoCSAF, sourceTypes.CiscoCSAF: return 5 case sourceTypes.NVDFeedCVEv2, sourceTypes.JVNFeedRSS, sourceTypes.FortinetCVRF, sourceTypes.PaloAltoJSON, sourceTypes.CiscoCVRF: return 4 @@ -883,7 +883,7 @@ func toCveContentType(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID) models return models.Nvd case sourceTypes.JVNFeedRSS, sourceTypes.JVNFeedDetail: return models.Jvn - case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: + case sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.Fortinet case sourceTypes.PaloAltoCSAF, sourceTypes.PaloAltoJSON, sourceTypes.PaloAltoList: return models.Paloalto @@ -1046,7 +1046,7 @@ func toVuls0Confidence(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID, sd so return models.NvdVendorProductMatch case sourceTypes.JVNFeedRSS, sourceTypes.JVNFeedDetail: return models.JvnVendorProductMatch - case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: + case sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.FortinetVendorProductMatch case sourceTypes.PaloAltoCSAF, sourceTypes.PaloAltoJSON, sourceTypes.PaloAltoList: return models.PaloaltoVendorProductMatch @@ -1071,7 +1071,7 @@ func toVuls0Confidence(e ecosystemTypes.Ecosystem, s sourceTypes.SourceID, sd so // VendorProductMatch branch above. Mapped to JvnVendorProductMatch // for safety should that demotion ever change. return models.JvnVendorProductMatch - case sourceTypes.Fortinet, sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: + case sourceTypes.FortinetCVRF, sourceTypes.FortinetCSAF: return models.FortinetExactVersionMatch case sourceTypes.PaloAltoCSAF, sourceTypes.PaloAltoJSON, sourceTypes.PaloAltoList: return models.PaloaltoExactVersionMatch diff --git a/detector/vuls2/vuls2_test.go b/detector/vuls2/vuls2_test.go index b14e698a46..d9f2ea1f4e 100644 --- a/detector/vuls2/vuls2_test.go +++ b/detector/vuls2/vuls2_test.go @@ -9777,6 +9777,387 @@ func Test_postConvert(t *testing.T) { }, }, }, + { + // Fortinet CVRF roots carry a BARE vulnerability stub (CVE-ID only), + // so the generic path builds a sparse CveContent (no CVSS/title/ + // summary) whose source link still points at the PSIRT advisory + // (root ID). The rich data lives in the advisory -> DistroAdvisory. + // The advisory severity is CVSS (not a vendor SIR) so + // DistroAdvisory.Severity stays empty. + name: "cpe fortinet (cvrf) detection emits sparse CveContent + DistroAdvisory", + args: args{ + scanned: scanTypes.ScanResult{ + CPE: []string{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*", + }, + }, + fsToOriginalCPE: map[string][]string{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*": {"cpe:/a:fortinet:fortiportal:7.4.0", "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*"}, + }, + detected: detectTypes.DetectResult{ + Detected: []detectTypes.VulnerabilityData{ + { + ID: "FG-IR-25-032", + Advisories: []dbTypes.VulnerabilityDataAdvisory{ + { + ID: "FG-IR-25-032", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]advisoryTypes.Advisory{ + sourceTypes.FortinetCVRF: { + dataTypes.RootID("FG-IR-25-032"): []advisoryTypes.Advisory{ + { + Content: advisoryContentTypes.Content{ + ID: "FG-IR-25-032", + Title: "Incorrect authorization in multi-vdom environment", + Description: "An Incorrect Authorization vulnerability [CWE-863] in FortiPortal may allow an authenticated attacker to reboot a shared FortiGate device via crafted HTTP requests.", + Severity: []severityTypes.Severity{ + { + Type: severityTypes.SeverityTypeCVSSv31, + Source: "fortiguard.fortinet.com", + CVSSv31: new(cvssV31Types.CVSSv31{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + BaseScore: 6.8, + BaseSeverity: "MEDIUM", + TemporalScore: 6.4, + TemporalSeverity: "MEDIUM", + EnvironmentalScore: 6.4, + EnvironmentalSeverity: "MEDIUM", + }), + }, + }, + Published: new(time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC)), + Modified: new(time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC)), + }, + Segments: []segmentTypes.Segment{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + }, + }, + }, + }, + }, + }, + }, + }, + Vulnerabilities: []dbTypes.VulnerabilityDataVulnerability{ + { + ID: "CVE-2025-54838", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]vulnerabilityTypes.Vulnerability{ + sourceTypes.FortinetCVRF: { + dataTypes.RootID("FG-IR-25-032"): []vulnerabilityTypes.Vulnerability{ + { + Content: vulnerabilityContentTypes.Content{ + ID: "CVE-2025-54838", + }, + Segments: []segmentTypes.Segment{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + }, + }, + }, + }, + }, + }, + }, + }, + Detections: []detectTypes.VulnerabilityDataDetection{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + Contents: map[sourceTypes.SourceID][]conditionTypes.FilteredCondition{ + sourceTypes.FortinetCVRF: { + { + Criteria: criteriaTypes.FilteredCriteria{ + Operator: criteriaTypes.CriteriaOperatorTypeOR, + Criterions: []criterionTypes.FilteredCriterion{ + { + Criterion: criterionTypes.Criterion{ + Type: criterionTypes.CriterionTypeCPE, + CPE: new(ccTypes.Criterion{ + Vulnerable: true, + FixStatus: new(vcFixStatusTypes.FixStatus{ + Class: vcFixStatusTypes.ClassUnknown, + }), + CPE: ccTypes.CPE("cpe:2.3:a:fortinet:fortiportal:*:*:*:*:*:*:*:*"), + CPEMatches: []ccTypes.CPE{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*", + "cpe:2.3:a:fortinet:fortiportal:7.4.5:*:*:*:*:*:*:*", + }, + }), + }, + Accepts: criterionTypes.AcceptQueries{ + CPE: criterionTypes.CPEAccepts{Exact: []int{0}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: models.VulnInfos{ + "CVE-2025-54838": { + CveID: "CVE-2025-54838", + Confidences: models.Confidences{models.FortinetExactVersionMatch}, + CpeURIs: []string{"cpe:/a:fortinet:fortiportal:7.4.0", "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*"}, + DistroAdvisories: models.DistroAdvisories{ + { + AdvisoryID: "FG-IR-25-032", + // Severity empty: Fortinet advisory severity is CVSS, not a vendor SIR. + Issued: time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC), + Updated: time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC), + Description: "An Incorrect Authorization vulnerability [CWE-863] in FortiPortal may allow an authenticated attacker to reboot a shared FortiGate device via crafted HTTP requests.", + }, + }, + CveContents: models.CveContents{ + models.Fortinet: []models.CveContent{ + { + Type: models.Fortinet, + CveID: "CVE-2025-54838", + SourceLink: "https://www.fortiguard.com/psirt/FG-IR-25-032", + References: models.References{ + { + Link: "https://www.fortiguard.com/psirt/FG-IR-25-032", + Source: "FORTINET", + RefID: "FG-IR-25-032", + }, + }, + Published: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + LastModified: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + Optional: map[string]string{ + "vuls2-sources": "[{\"root_id\":\"FG-IR-25-032\",\"source_id\":\"fortinet-cvrf\",\"segment\":{\"ecosystem\":\"cpe\"}}]", + }, + }, + }, + }, + }, + }, + }, + { + // The same FG-IR advisory / CVE is present in BOTH Fortinet sources + // (csaf rich stub, cvrf bare stub). They detect independently and + // merge: csaf is preferred (compareSourceID tier 5 > cvrf tier 4), so + // the merged Fortinet CveContent takes csaf's scalars (title/summary/ + // CVSS) and the DistroAdvisory takes csaf's (later Updated). The + // vuls2-sources provenance lists both sources. + name: "cpe fortinet csaf+cvrf both detect same CVE -> csaf preferred", + args: args{ + scanned: scanTypes.ScanResult{ + CPE: []string{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*", + }, + }, + fsToOriginalCPE: map[string][]string{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*": {"cpe:/a:fortinet:fortiportal:7.4.0", "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*"}, + }, + detected: detectTypes.DetectResult{ + Detected: []detectTypes.VulnerabilityData{ + { + ID: "FG-IR-25-032", + Advisories: []dbTypes.VulnerabilityDataAdvisory{ + { + ID: "FG-IR-25-032", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]advisoryTypes.Advisory{ + sourceTypes.FortinetCSAF: { + dataTypes.RootID("FG-IR-25-032"): []advisoryTypes.Advisory{ + { + Content: advisoryContentTypes.Content{ + ID: "FG-IR-25-032", + Title: "Incorrect authorization in multi-vdom environment", + Published: new(time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC)), + Modified: new(time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC)), + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + sourceTypes.FortinetCVRF: { + dataTypes.RootID("FG-IR-25-032"): []advisoryTypes.Advisory{ + { + Content: advisoryContentTypes.Content{ + ID: "FG-IR-25-032", + Title: "Incorrect authorization in multi-vdom environment", + Description: "An Incorrect Authorization vulnerability [CWE-863] in FortiPortal may allow an authenticated attacker to reboot a shared FortiGate device via crafted HTTP requests.", + Severity: []severityTypes.Severity{ + { + Type: severityTypes.SeverityTypeCVSSv31, + Source: "fortiguard.fortinet.com", + CVSSv31: new(cvssV31Types.CVSSv31{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + BaseScore: 6.8, + BaseSeverity: "MEDIUM", + TemporalScore: 6.4, + TemporalSeverity: "MEDIUM", + EnvironmentalScore: 6.4, + EnvironmentalSeverity: "MEDIUM", + }), + }, + }, + Published: new(time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC)), + Modified: new(time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC)), + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + }, + }, + }, + Vulnerabilities: []dbTypes.VulnerabilityDataVulnerability{ + { + ID: "CVE-2025-54838", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]vulnerabilityTypes.Vulnerability{ + sourceTypes.FortinetCSAF: { + dataTypes.RootID("FG-IR-25-032"): []vulnerabilityTypes.Vulnerability{ + { + Content: vulnerabilityContentTypes.Content{ + ID: "CVE-2025-54838", + Title: "Incorrect authorization in multi-vdom environment", + Description: "Incorrect authorization in rebooting FortiGate device feature", + Severity: []severityTypes.Severity{ + { + Type: severityTypes.SeverityTypeVendor, + Source: "fortiguard.fortinet.com", + Vendor: new("Denial of service"), + }, + { + Type: severityTypes.SeverityTypeCVSSv31, + Source: "fortiguard.fortinet.com", + CVSSv31: new(cvssV31Types.CVSSv31{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + BaseScore: 6.8, + BaseSeverity: "MEDIUM", + TemporalScore: 6.4, + TemporalSeverity: "MEDIUM", + EnvironmentalScore: 6.4, + EnvironmentalSeverity: "MEDIUM", + }), + }, + }, + CWE: []cweTypes.CWE{ + {Source: "fortiguard.fortinet.com", CWE: []string{"CWE-863"}}, + }, + References: []referenceTypes.Reference{ + {Source: "fortiguard.fortinet.com", URL: "https://fortiguard.fortinet.com/psirt/FG-IR-25-032"}, + }, + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + sourceTypes.FortinetCVRF: { + dataTypes.RootID("FG-IR-25-032"): []vulnerabilityTypes.Vulnerability{ + { + Content: vulnerabilityContentTypes.Content{ + ID: "CVE-2025-54838", + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + }, + }, + }, + Detections: []detectTypes.VulnerabilityDataDetection{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + Contents: map[sourceTypes.SourceID][]conditionTypes.FilteredCondition{ + sourceTypes.FortinetCSAF: { + { + Criteria: criteriaTypes.FilteredCriteria{ + Operator: criteriaTypes.CriteriaOperatorTypeOR, + Criterions: []criterionTypes.FilteredCriterion{ + { + Criterion: criterionTypes.Criterion{ + Type: criterionTypes.CriterionTypeCPE, + CPE: new(ccTypes.Criterion{ + Vulnerable: true, + CPE: ccTypes.CPE("cpe:2.3:a:fortinet:fortiportal:*:*:*:*:*:*:*:*"), + Range: new(ccRangeTypes.Range{ + Type: ccRangeTypes.RangeTypeFortinetFortiPortal, + GreaterEqual: "7.4.0", + LessEqual: "7.4.5", + }), + }), + }, + Accepts: criterionTypes.AcceptQueries{CPE: criterionTypes.CPEAccepts{Exact: []int{0}}}, + }, + }, + }, + }, + }, + sourceTypes.FortinetCVRF: { + { + Criteria: criteriaTypes.FilteredCriteria{ + Operator: criteriaTypes.CriteriaOperatorTypeOR, + Criterions: []criterionTypes.FilteredCriterion{ + { + Criterion: criterionTypes.Criterion{ + Type: criterionTypes.CriterionTypeCPE, + CPE: new(ccTypes.Criterion{ + Vulnerable: true, + FixStatus: new(vcFixStatusTypes.FixStatus{Class: vcFixStatusTypes.ClassUnknown}), + CPE: ccTypes.CPE("cpe:2.3:a:fortinet:fortiportal:*:*:*:*:*:*:*:*"), + CPEMatches: []ccTypes.CPE{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*", + }, + }), + }, + Accepts: criterionTypes.AcceptQueries{CPE: criterionTypes.CPEAccepts{Exact: []int{0}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: models.VulnInfos{ + "CVE-2025-54838": { + CveID: "CVE-2025-54838", + Confidences: models.Confidences{models.FortinetExactVersionMatch}, + CpeURIs: []string{"cpe:/a:fortinet:fortiportal:7.4.0", "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*"}, + DistroAdvisories: models.DistroAdvisories{ + { + AdvisoryID: "FG-IR-25-032", + Issued: time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC), + Updated: time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC), + }, + }, + CveContents: models.CveContents{ + models.Fortinet: []models.CveContent{ + { + Type: models.Fortinet, + CveID: "CVE-2025-54838", + Title: "Incorrect authorization in multi-vdom environment", + Summary: "Incorrect authorization in rebooting FortiGate device feature", + Cvss3Score: 6.8, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + Cvss3Severity: "MEDIUM", + SourceLink: "https://www.fortiguard.com/psirt/FG-IR-25-032", + References: models.References{ + {Link: "https://fortiguard.fortinet.com/psirt/FG-IR-25-032", Source: "MISC"}, + {Link: "https://www.fortiguard.com/psirt/FG-IR-25-032", Source: "FORTINET", RefID: "FG-IR-25-032"}, + }, + CweIDs: []string{"CWE-863"}, + Published: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + LastModified: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + Optional: map[string]string{ + "vuls2-sources": "[{\"root_id\":\"FG-IR-25-032\",\"source_id\":\"fortinet-cvrf\",\"segment\":{\"ecosystem\":\"cpe\"}},{\"root_id\":\"FG-IR-25-032\",\"source_id\":\"fortinet-csaf\",\"segment\":{\"ecosystem\":\"cpe\"}}]", + }, + }, + }, + }, + }, + }, + }, { // A criterion accepted the query only at version-unconfirmed // quality (the upstream matcher could not confirm the scanned From 7f3d19450e197606fddf8b0ddd58adad26b0c3ae Mon Sep 17 00:00:00 2001 From: Shunichi Shinohara Date: Tue, 30 Jun 2026 15:46:12 +0900 Subject: [PATCH 3/4] test(detector/vuls2): NVD-v2 + Fortinet-CSAF both-detect keeps both CveContents Confirm that a CVE detected by both NVD (feed v2) and Fortinet CSAF retains both CveContents in the merged result: models.Nvd and models.Fortinet are distinct CveContentTypes, so mergeVulnInfo keys them separately and neither overwrites the other (and both confidences are kept). Distinct from the csaf+cvrf case, where the two contents share models.Fortinet and merge into one. Co-Authored-By: Claude Opus 4.8 --- detector/vuls2/vuls2_test.go | 245 +++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/detector/vuls2/vuls2_test.go b/detector/vuls2/vuls2_test.go index d9f2ea1f4e..8dfccdc45a 100644 --- a/detector/vuls2/vuls2_test.go +++ b/detector/vuls2/vuls2_test.go @@ -10158,6 +10158,251 @@ func Test_postConvert(t *testing.T) { }, }, }, + { + // A CVE detected by BOTH NVD (feed v2) and Fortinet CSAF must keep + // BOTH CveContents in the result — they are different CveContentTypes + // (models.Nvd vs models.Fortinet), so the merge keys them separately + // and neither overwrites the other. + name: "cpe nvd-v2 + fortinet-csaf both detect same CVE -> both CveContents kept", + args: args{ + scanned: scanTypes.ScanResult{ + CPE: []string{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*", + }, + }, + fsToOriginalCPE: map[string][]string{ + "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*": {"cpe:/a:fortinet:fortiportal:7.4.0", "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*"}, + }, + detected: detectTypes.DetectResult{ + Detected: []detectTypes.VulnerabilityData{ + { + ID: "CVE-2025-54838", + Vulnerabilities: []dbTypes.VulnerabilityDataVulnerability{ + { + ID: "CVE-2025-54838", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]vulnerabilityTypes.Vulnerability{ + sourceTypes.NVDFeedCVEv2: { + dataTypes.RootID("CVE-2025-54838"): []vulnerabilityTypes.Vulnerability{ + { + Content: vulnerabilityContentTypes.Content{ + ID: "CVE-2025-54838", + Title: "Fortinet FortiPortal incorrect authorization", + Description: "NVD-side description for CVE-2025-54838", + Severity: []severityTypes.Severity{ + { + Type: severityTypes.SeverityTypeCVSSv31, + Source: "nvd@nist.gov", + CVSSv31: new(cvssV31Types.CVSSv31{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + BaseScore: 6.8, + BaseSeverity: "MEDIUM", + TemporalScore: 6.4, + TemporalSeverity: "MEDIUM", + EnvironmentalScore: 6.4, + EnvironmentalSeverity: "MEDIUM", + }), + }, + }, + References: []referenceTypes.Reference{ + {Source: "nvd@nist.gov", URL: "https://nvd.nist.gov/vuln/detail/CVE-2025-54838"}, + }, + Published: new(time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC)), + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + }, + }, + }, + Detections: []detectTypes.VulnerabilityDataDetection{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + Contents: map[sourceTypes.SourceID][]conditionTypes.FilteredCondition{ + sourceTypes.NVDFeedCVEv2: { + { + Criteria: criteriaTypes.FilteredCriteria{ + Operator: criteriaTypes.CriteriaOperatorTypeOR, + Criterions: []criterionTypes.FilteredCriterion{ + { + Criterion: criterionTypes.Criterion{ + Type: criterionTypes.CriterionTypeCPE, + CPE: new(ccTypes.Criterion{ + Vulnerable: true, + CPE: ccTypes.CPE("cpe:2.3:a:fortinet:fortiportal:*:*:*:*:*:*:*:*"), + Range: new(ccRangeTypes.Range{ + Type: ccRangeTypes.RangeTypeSEMVER, + GreaterEqual: "7.4.0", + LessEqual: "7.4.5", + }), + }), + }, + Accepts: criterionTypes.AcceptQueries{CPE: criterionTypes.CPEAccepts{Exact: []int{0}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ID: "FG-IR-25-032", + Advisories: []dbTypes.VulnerabilityDataAdvisory{ + { + ID: "FG-IR-25-032", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]advisoryTypes.Advisory{ + sourceTypes.FortinetCSAF: { + dataTypes.RootID("FG-IR-25-032"): []advisoryTypes.Advisory{ + { + Content: advisoryContentTypes.Content{ + ID: "FG-IR-25-032", + Title: "Incorrect authorization in multi-vdom environment", + Published: new(time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC)), + Modified: new(time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC)), + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + }, + }, + }, + Vulnerabilities: []dbTypes.VulnerabilityDataVulnerability{ + { + ID: "CVE-2025-54838", + Contents: map[sourceTypes.SourceID]map[dataTypes.RootID][]vulnerabilityTypes.Vulnerability{ + sourceTypes.FortinetCSAF: { + dataTypes.RootID("FG-IR-25-032"): []vulnerabilityTypes.Vulnerability{ + { + Content: vulnerabilityContentTypes.Content{ + ID: "CVE-2025-54838", + Title: "Incorrect authorization in multi-vdom environment", + Description: "Incorrect authorization in rebooting FortiGate device feature", + Severity: []severityTypes.Severity{ + { + Type: severityTypes.SeverityTypeCVSSv31, + Source: "fortiguard.fortinet.com", + CVSSv31: new(cvssV31Types.CVSSv31{ + Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + BaseScore: 6.8, + BaseSeverity: "MEDIUM", + TemporalScore: 6.4, + TemporalSeverity: "MEDIUM", + EnvironmentalScore: 6.4, + EnvironmentalSeverity: "MEDIUM", + }), + }, + }, + CWE: []cweTypes.CWE{ + {Source: "fortiguard.fortinet.com", CWE: []string{"CWE-863"}}, + }, + References: []referenceTypes.Reference{ + {Source: "fortiguard.fortinet.com", URL: "https://fortiguard.fortinet.com/psirt/FG-IR-25-032"}, + }, + }, + Segments: []segmentTypes.Segment{{Ecosystem: ecosystemTypes.EcosystemTypeCPE}}, + }, + }, + }, + }, + }, + }, + Detections: []detectTypes.VulnerabilityDataDetection{ + { + Ecosystem: ecosystemTypes.EcosystemTypeCPE, + Contents: map[sourceTypes.SourceID][]conditionTypes.FilteredCondition{ + sourceTypes.FortinetCSAF: { + { + Criteria: criteriaTypes.FilteredCriteria{ + Operator: criteriaTypes.CriteriaOperatorTypeOR, + Criterions: []criterionTypes.FilteredCriterion{ + { + Criterion: criterionTypes.Criterion{ + Type: criterionTypes.CriterionTypeCPE, + CPE: new(ccTypes.Criterion{ + Vulnerable: true, + CPE: ccTypes.CPE("cpe:2.3:a:fortinet:fortiportal:*:*:*:*:*:*:*:*"), + Range: new(ccRangeTypes.Range{ + Type: ccRangeTypes.RangeTypeFortinetFortiPortal, + GreaterEqual: "7.4.0", + LessEqual: "7.4.5", + }), + }), + }, + Accepts: criterionTypes.AcceptQueries{CPE: criterionTypes.CPEAccepts{Exact: []int{0}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: models.VulnInfos{ + "CVE-2025-54838": { + CveID: "CVE-2025-54838", + Confidences: models.Confidences{models.NvdExactVersionMatch, models.FortinetExactVersionMatch}, + CpeURIs: []string{"cpe:/a:fortinet:fortiportal:7.4.0", "cpe:2.3:a:fortinet:fortiportal:7.4.0:*:*:*:*:*:*:*"}, + DistroAdvisories: models.DistroAdvisories{ + { + AdvisoryID: "FG-IR-25-032", + Issued: time.Date(2025, 12, 9, 0, 0, 0, 0, time.UTC), + Updated: time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC), + }, + }, + CveContents: models.CveContents{ + models.Nvd: []models.CveContent{ + { + Type: models.Nvd, + CveID: "CVE-2025-54838", + Title: "Fortinet FortiPortal incorrect authorization", + Summary: "NVD-side description for CVE-2025-54838", + Cvss3Score: 6.8, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + Cvss3Severity: "MEDIUM", + SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2025-54838", + References: models.References{ + {Link: "https://nvd.nist.gov/vuln/detail/CVE-2025-54838", Source: "NVD", RefID: "CVE-2025-54838"}, + }, + Published: time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC), + LastModified: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + Optional: map[string]string{ + "vuls2-sources": "[{\"root_id\":\"CVE-2025-54838\",\"source_id\":\"nvd-feed-cve-v2\",\"segment\":{\"ecosystem\":\"cpe\"}}]", + }, + }, + }, + models.Fortinet: []models.CveContent{ + { + Type: models.Fortinet, + CveID: "CVE-2025-54838", + Title: "Incorrect authorization in multi-vdom environment", + Summary: "Incorrect authorization in rebooting FortiGate device feature", + Cvss3Score: 6.8, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H/E:P/RL:X/RC:C", + Cvss3Severity: "MEDIUM", + SourceLink: "https://www.fortiguard.com/psirt/FG-IR-25-032", + References: models.References{ + {Link: "https://fortiguard.fortinet.com/psirt/FG-IR-25-032", Source: "MISC"}, + {Link: "https://www.fortiguard.com/psirt/FG-IR-25-032", Source: "FORTINET", RefID: "FG-IR-25-032"}, + }, + CweIDs: []string{"CWE-863"}, + Published: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + LastModified: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + Optional: map[string]string{ + "vuls2-sources": "[{\"root_id\":\"FG-IR-25-032\",\"source_id\":\"fortinet-csaf\",\"segment\":{\"ecosystem\":\"cpe\"}}]", + }, + }, + }, + }, + }, + }, + }, { // A criterion accepted the query only at version-unconfirmed // quality (the upstream matcher could not confirm the scanned From edeb21b1e05a5362815cb517adc642459644d1ee Mon Sep 17 00:00:00 2001 From: Shunichi Shinohara Date: Tue, 30 Jun 2026 21:05:44 +0900 Subject: [PATCH 4/4] chore(deps): re-pin vuls2 / vuls-data-update to the merged fortinet-csaf versions MaineK00n/vuls2#396 ("bump vuls-data-update to nightly (fortinet csaf extractor)") is merged, so drop the PR-head pins and use the released nightly commits: - github.com/MaineK00n/vuls2 -> v0.0.1-alpha.0.20260630120316-69ab575c2ebe - github.com/MaineK00n/vuls-data-update -> v0.0.0-20260630113343-f3c7f13ad814 (pulls in github.com/vulsio/go-fortinet-version, the fortinet-* RangeType backend) This replaces the temporary vuls-data-update#853-head pin now that the Fortinet CSAF extractor and range types have landed. Co-Authored-By: Claude Opus 4.8 --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1c34a26865..9ac0c61377 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 github.com/BurntSushi/toml v1.6.0 github.com/CycloneDX/cyclonedx-go v0.10.0 - github.com/MaineK00n/vuls-data-update v0.0.0-20260630053343-3263815ecdb6 - github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260629073543-a01483fb1da0 + github.com/MaineK00n/vuls-data-update v0.0.0-20260630113343-f3c7f13ad814 + github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260630120316-69ab575c2ebe github.com/Ullaakut/nmap/v2 v2.2.2 github.com/aquasecurity/trivy v0.70.0 github.com/aquasecurity/trivy-db v0.0.0-20251222105351-a833f47f8f0d diff --git a/go.sum b/go.sum index 066ab74849..f19460d8e5 100644 --- a/go.sum +++ b/go.sum @@ -70,10 +70,10 @@ github.com/MaineK00n/go-microsoft-version v0.0.0-20260325021654-1d9206bdeffc h1: github.com/MaineK00n/go-microsoft-version v0.0.0-20260325021654-1d9206bdeffc/go.mod h1:GNf+Vhnxk8/pW56jsxAeFCBP0VCgVQlLIJ812UnAj9c= github.com/MaineK00n/go-paloalto-version v0.0.0-20260324210858-f043171ba2bf h1:QDz92+GEPbIR4HTw2EGNgYqH+T9jxmO/OvFMfY715PE= github.com/MaineK00n/go-paloalto-version v0.0.0-20260324210858-f043171ba2bf/go.mod h1:ELOxzfAd4oAe4niMmoZlSiJwzf1DF+DjNdjsUcuqAR8= -github.com/MaineK00n/vuls-data-update v0.0.0-20260630053343-3263815ecdb6 h1:SuG73UZGjEE34mAtePCvSafN/AnvhT+Z9SicRQk8os8= -github.com/MaineK00n/vuls-data-update v0.0.0-20260630053343-3263815ecdb6/go.mod h1:5QVGDCspc2SuU6aHQ99W8boUmpspqLyGJ9uTPGARa9A= -github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260629073543-a01483fb1da0 h1:E7A6LDY5P9iHqNpoJkaz0cZVLuBiJ2N/9IiAPGWFGyE= -github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260629073543-a01483fb1da0/go.mod h1:hsRv4IvqFBs3YMjlEYfPbKv7Ft+pVaIJh8HvkREhGRo= +github.com/MaineK00n/vuls-data-update v0.0.0-20260630113343-f3c7f13ad814 h1:Qhe8kzurr/wdoN232kFAi4PgnrNpj27Dha9zfh9QFoc= +github.com/MaineK00n/vuls-data-update v0.0.0-20260630113343-f3c7f13ad814/go.mod h1:5QVGDCspc2SuU6aHQ99W8boUmpspqLyGJ9uTPGARa9A= +github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260630120316-69ab575c2ebe h1:x5FeISU0u+jglVPffLh5oFHDpyOjuL4y3sPNz3g3L4E= +github.com/MaineK00n/vuls2 v0.0.1-alpha.0.20260630120316-69ab575c2ebe/go.mod h1:FAVFG6rikN0zLlug40gX9ExUyOK/s0YUcjOSAMOiWVk= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=