@@ -3,6 +3,7 @@ package vuls2
33import (
44 "cmp"
55 "fmt"
6+ "maps"
67 "slices"
78 "strings"
89 "time"
@@ -25,6 +26,7 @@ import (
2526 v2 "github.com/MaineK00n/vuls-data-update/pkg/extract/types/data/severity/cvss/v2"
2627 v31 "github.com/MaineK00n/vuls-data-update/pkg/extract/types/data/severity/cvss/v31"
2728 v40 "github.com/MaineK00n/vuls-data-update/pkg/extract/types/data/severity/cvss/v40"
29+ ssvcTypes "github.com/MaineK00n/vuls-data-update/pkg/extract/types/data/ssvc"
2830 vulnerabilityTypes "github.com/MaineK00n/vuls-data-update/pkg/extract/types/data/vulnerability"
2931 sourceTypes "github.com/MaineK00n/vuls-data-update/pkg/extract/types/source"
3032 dbTypes "github.com/MaineK00n/vuls2/pkg/db/session/types"
@@ -649,6 +651,8 @@ func cveContentSourceLink(ccType models.CveContentType, v vulnerabilityTypes.Vul
649651 return fmt .Sprintf ("https://security.alpinelinux.org/vuln/%s" , v .Content .ID )
650652 case models .Nvd :
651653 return fmt .Sprintf ("https://nvd.nist.gov/vuln/detail/%s" , v .Content .ID )
654+ case models .Mitre :
655+ return fmt .Sprintf ("https://www.cve.org/CVERecord?id=%s" , v .Content .ID )
652656 case models .Microsoft :
653657 return fmt .Sprintf ("https://msrc.microsoft.com/update-guide/vulnerability/%s" , v .Content .ID )
654658 default :
@@ -1168,6 +1172,8 @@ func enrichVulnerabilities(vi *models.VulnInfo, vulns []dbTypes.VulnerabilityDat
11681172 enrichRedHatCVE (vi , rootMap )
11691173 case sourceTypes .NVDFeedCVEv2 :
11701174 enrichNVD (vi , rootMap )
1175+ case sourceTypes .MitreCVEV5 :
1176+ enrichMitreCVE (vi , rootMap )
11711177 case sourceTypes .Metasploit :
11721178 enrichMetasploit (vi , rootMap )
11731179 case sourceTypes .ExploitExploitDB , sourceTypes .ExploitGitHub , sourceTypes .ExploitInTheWild , sourceTypes .ExploitTrickest , sourceTypes .NucleiRepository :
@@ -1435,6 +1441,151 @@ func enrichNVD(vi *models.VulnInfo, rootMap map[dataTypes.RootID][]vulnerability
14351441 }
14361442}
14371443
1444+ // enrichMitreCVE adds MITRE CVE v5 data as CveContent, one entry per CNA/ADP
1445+ // source so each source's CVSS, CWE, references, and SSVC decision point are
1446+ // reported separately (rendered as mitre(<source>) by CveContents.SSVC and the
1447+ // reporter). The source is the CNA/ADP provider shortName (e.g. "CISA-ADP").
1448+ func enrichMitreCVE (vi * models.VulnInfo , rootMap map [dataTypes.RootID ][]vulnerabilityTypes.Vulnerability ) {
1449+ if _ , ok := vi .CveContents [models .Mitre ]; ok {
1450+ return
1451+ }
1452+
1453+ // mitreBySource accumulates the per-source MITRE fields so each CNA/ADP
1454+ // source becomes its own CveContent.
1455+ type mitreBySource struct {
1456+ severities []severityTypes.Severity
1457+ references models.References
1458+ cweIDs []string
1459+ ssvc * models.SSVC
1460+ }
1461+
1462+ for _ , vulns := range rootMap {
1463+ for _ , v := range vulns {
1464+ // Group every per-source field by its CNA/ADP source in a single pass.
1465+ bySource := map [string ]* mitreBySource {}
1466+ get := func (source string ) * mitreBySource {
1467+ b , ok := bySource [source ]
1468+ if ! ok {
1469+ b = & mitreBySource {}
1470+ bySource [source ] = b
1471+ }
1472+ return b
1473+ }
1474+ for _ , s := range v .Content .Severity {
1475+ b := get (s .Source )
1476+ b .severities = append (b .severities , s )
1477+ }
1478+ for _ , r := range v .Content .References {
1479+ b := get (r .Source )
1480+ b .references = append (b .references , toReference (r .URL ))
1481+ }
1482+ for _ , c := range v .Content .CWE {
1483+ b := get (c .Source )
1484+ b .cweIDs = append (b .cweIDs , c .CWE ... )
1485+ }
1486+ for _ , s := range v .Content .SSVC {
1487+ if b := get (s .Source ); b .ssvc == nil {
1488+ b .ssvc = mitreSSVC (s )
1489+ }
1490+ }
1491+ // Keep the CVE-level title/summary even when no per-source data exists.
1492+ if len (bySource ) == 0 {
1493+ bySource ["" ] = & mitreBySource {}
1494+ }
1495+
1496+ // container_types maps each source label to its container role
1497+ // ("CNA"/"ADP"), set by the extractor from structural position.
1498+ containerTypes := mitreContainerTypes (v .Content .Optional )
1499+
1500+ sourceLink := cveContentSourceLink (models .Mitre , v )
1501+ published := func () time.Time {
1502+ if v .Content .Published != nil {
1503+ return * v .Content .Published
1504+ }
1505+ return time .Date (1000 , time .January , 1 , 0 , 0 , 0 , 0 , time .UTC )
1506+ }()
1507+ lastModified := func () time.Time {
1508+ if v .Content .Modified != nil {
1509+ return * v .Content .Modified
1510+ }
1511+ return time .Date (1000 , time .January , 1 , 0 , 0 , 0 , 0 , time .UTC )
1512+ }()
1513+
1514+ for _ , source := range slices .Sorted (maps .Keys (bySource )) {
1515+ b := bySource [source ]
1516+ cvss2 , cvss3 , cvss40 := enrichCvss (b .severities )
1517+
1518+ cc := models.CveContent {
1519+ Type : models .Mitre ,
1520+ CveID : string (v .Content .ID ),
1521+ Title : v .Content .Title ,
1522+ Summary : v .Content .Description ,
1523+ Cvss2Score : cvss2 .BaseScore ,
1524+ Cvss2Vector : cvss2 .Vector ,
1525+ Cvss2Severity : cvss2 .NVDBaseSeverity ,
1526+ Cvss3Score : cvss3 .BaseScore ,
1527+ Cvss3Vector : cvss3 .Vector ,
1528+ Cvss3Severity : cvss3 .BaseSeverity ,
1529+ Cvss40Score : cvss40 .Score ,
1530+ Cvss40Vector : cvss40 .Vector ,
1531+ Cvss40Severity : cvss40 .Severity ,
1532+ SourceLink : sourceLink ,
1533+ References : b .references ,
1534+ CweIDs : b .cweIDs ,
1535+ Published : published ,
1536+ LastModified : lastModified ,
1537+ SSVC : b .ssvc ,
1538+ }
1539+ if source != "" {
1540+ // Qualify the source with its CNA/ADP role when known
1541+ // (e.g. "ADP:CISA-ADP"), so reports distinguish the two.
1542+ label := source
1543+ if ct := containerTypes [source ]; ct != "" {
1544+ label = fmt .Sprintf ("%s:%s" , ct , source )
1545+ }
1546+ cc .Optional = map [string ]string {"source" : label }
1547+ }
1548+ vi .CveContents [models .Mitre ] = append (vi .CveContents [models .Mitre ], cc )
1549+ }
1550+ }
1551+ }
1552+ }
1553+
1554+ // mitreContainerTypes extracts the source-label -> container role ("CNA"/"ADP")
1555+ // mapping that the MITRE CVE v5 extractor stores under Optional["container_types"].
1556+ // Returns an empty (non-nil) map when absent or malformed.
1557+ func mitreContainerTypes (optional map [string ]any ) map [string ]string {
1558+ cts := map [string ]string {}
1559+ m , ok := optional ["container_types" ].(map [string ]any )
1560+ if ! ok {
1561+ return cts
1562+ }
1563+ for source , ct := range m {
1564+ if s , ok := ct .(string ); ok {
1565+ cts [source ] = s
1566+ }
1567+ }
1568+ return cts
1569+ }
1570+
1571+ // mitreSSVC maps a single SSVC decision point to models.SSVC. The option keys
1572+ // follow the SSVC computed schema ("Exploitation", "Automatable",
1573+ // "Technical Impact").
1574+ func mitreSSVC (s ssvcTypes.SSVC ) * models.SSVC {
1575+ ssvc := & models.SSVC {}
1576+ for _ , o := range s .Options {
1577+ switch o .Key {
1578+ case "Exploitation" :
1579+ ssvc .Exploitation = o .Value
1580+ case "Automatable" :
1581+ ssvc .Automatable = o .Value
1582+ case "Technical Impact" :
1583+ ssvc .TechnicalImpact = o .Value
1584+ }
1585+ }
1586+ return ssvc
1587+ }
1588+
14381589// enrichVulnerabilityKEV extracts KEV data from vulnerability content and maps it to models.KEV.
14391590// Handles CISA and VulnCheck KEV sources where KEV data is stored in vulnerability content.
14401591func enrichVulnerabilityKEV (sourceID sourceTypes.SourceID , rootMap map [dataTypes.RootID ][]vulnerabilityTypes.Vulnerability ) []models.KEV {
0 commit comments