2026-03-25 18:02:26 +00:00
package main
import (
"bufio"
"compress/gzip"
"encoding/json"
2026-04-21 19:39:22 +00:00
"errors"
2026-03-25 18:02:26 +00:00
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
)
type OSVData struct {
SchemaVersion string ` json:"schema_version" `
ID string ` json:"id" `
Published string ` json:"published" `
Modified string ` json:"modified" `
Details string ` json:"details" `
Affected [ ] Affected ` json:"affected" `
Upstream [ ] string ` json:"upstream,omitempty" `
Related [ ] string ` json:"related,omitempty" `
}
type Affected struct {
Package Package ` json:"package" `
Ranges [ ] Range ` json:"ranges" `
Versions [ ] string ` json:"versions,omitempty" `
EcosystemSpecific map [ string ] any ` json:"ecosystem_specific,omitempty" `
DatabaseSpecific map [ string ] any ` json:"database_specific,omitempty" `
}
type Package struct {
Ecosystem string ` json:"ecosystem" `
Name string ` json:"name" `
Purl string ` json:"purl,omitempty" `
}
type Range struct {
Type string ` json:"type" `
Events [ ] Event ` json:"events" `
}
type Event struct {
Introduced string ` json:"introduced,omitempty" `
Fixed string ` json:"fixed,omitempty" `
}
type ProcessedVuln struct {
CVE string ` json:"cve" `
Published string ` json:"published" `
Modified string ` json:"modified" `
Introduced string ` json:"introduced,omitempty" `
Fixed string ` json:"fixed,omitempty" `
Versions [ ] string ` json:"versions,omitempty" `
}
type Config struct {
2026-04-21 19:39:22 +00:00
Platform string
2026-03-25 18:02:26 +00:00
InputDir string
OutputDir string
Versions string
ExcludeVersions string
ChangedFilesToday string
ChangedFilesYesterday string
DateStr string
YesterdayStr string
GeneratedTimestamp string
RunTime time . Time
}
type ArtifactData struct {
SchemaVersion string ` json:"schema_version" `
UbuntuVersion string ` json:"ubuntu_version" `
Generated string ` json:"generated" `
TotalCVEs int ` json:"total_cves" `
TotalPackages int ` json:"total_packages" `
Vulnerabilities map [ string ] [ ] ProcessedVuln ` json:"vulnerabilities" `
}
2026-04-21 19:39:22 +00:00
type RHELArtifactData struct {
SchemaVersion string ` json:"schema_version" `
RHELVersion string ` json:"rhel_version" `
Generated string ` json:"generated" `
TotalCVEs int ` json:"total_cves" `
TotalPackages int ` json:"total_packages" `
Vulnerabilities map [ string ] [ ] ProcessedVuln ` json:"vulnerabilities" `
}
2026-03-25 18:02:26 +00:00
func main ( ) {
2026-04-21 19:39:22 +00:00
platform := flag . String ( "platform" , "ubuntu" , "Platform to process: ubuntu or rhel" )
inputDir := flag . String ( "input" , "" , "Input directory with OSV JSON files (default: /tmp/ubuntu-osv for ubuntu, /tmp/rhel-osv for rhel)" )
2026-03-25 18:02:26 +00:00
outputDir := flag . String ( "output" , "./artifacts" , "Output directory for artifacts" )
2026-04-21 19:39:22 +00:00
versions := flag . String ( "versions" , "" , "Comma-separated versions to process (inclusive)" )
excludeVersions := flag . String ( "exclude-versions" , "" , "Comma-separated versions to exclude (ignored if --versions is set)" )
changedFilesToday := flag . String ( "changed-files-today" , "" , "Path to file containing CVE files changed today (ubuntu only)" )
changedFilesYesterday := flag . String ( "changed-files-yesterday" , "" , "Path to file containing CVE files changed yesterday (ubuntu only)" )
2026-03-25 18:02:26 +00:00
flag . Parse ( )
2026-04-21 19:39:22 +00:00
if * inputDir == "" {
switch * platform {
case "rhel" :
* inputDir = "/tmp/rhel-osv"
default :
* inputDir = "/tmp/ubuntu-osv"
}
}
2026-03-25 18:02:26 +00:00
runTime := time . Now ( ) . UTC ( )
cfg := Config {
2026-04-21 19:39:22 +00:00
Platform : * platform ,
2026-03-25 18:02:26 +00:00
InputDir : * inputDir ,
OutputDir : * outputDir ,
Versions : * versions ,
ExcludeVersions : * excludeVersions ,
ChangedFilesToday : * changedFilesToday ,
ChangedFilesYesterday : * changedFilesYesterday ,
DateStr : runTime . Format ( "2006-01-02" ) ,
YesterdayStr : runTime . AddDate ( 0 , 0 , - 1 ) . Format ( "2006-01-02" ) ,
GeneratedTimestamp : runTime . Format ( time . RFC3339 ) ,
RunTime : runTime ,
}
2026-04-21 19:39:22 +00:00
switch cfg . Platform {
case "ubuntu" :
if err := run ( cfg ) ; err != nil {
log . Fatalf ( "Error: %v" , err )
}
case "rhel" :
if err := runRHEL ( cfg ) ; err != nil {
log . Fatalf ( "Error: %v" , err )
}
default :
log . Fatalf ( "Unknown platform: %s (supported: ubuntu, rhel)" , cfg . Platform )
2026-03-25 18:02:26 +00:00
}
}
func run ( cfg Config ) error {
if err := os . MkdirAll ( cfg . OutputDir , 0 o755 ) ; err != nil {
return fmt . Errorf ( "failed to create output directory: %w" , err )
}
// Build version filter
targetVersions , excludedVersions := buildVersionFilter ( cfg . Versions , cfg . ExcludeVersions )
switch {
case targetVersions != nil :
log . Printf ( "Processing OSV files from %s for versions: %s" , cfg . InputDir , cfg . Versions )
case excludedVersions != nil :
log . Printf ( "Processing OSV files from %s (auto-detecting, excluding: %s)" , cfg . InputDir , cfg . ExcludeVersions )
default :
log . Printf ( "Processing OSV files from %s (auto-detecting all versions)" , cfg . InputDir )
}
// Load changed CVE files for delta generation
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.
* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.
* **Chores**
* Added configuration flag and updated expected config fixtures.
* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 20:59:32 +00:00
var todayCVEFiles , yesterdayCVEFiles map [ string ] struct { }
2026-03-25 18:02:26 +00:00
generateTodayDeltas := cfg . ChangedFilesToday != ""
generateYesterdayDeltas := cfg . ChangedFilesYesterday != ""
if generateTodayDeltas {
log . Printf ( "Loading today's changed CVE files from %s" , cfg . ChangedFilesToday )
var err error
todayCVEFiles , err = loadChangedFiles ( cfg . ChangedFilesToday )
if err != nil {
return fmt . Errorf ( "failed to load today's changed files: %w" , err )
}
log . Printf ( "Found %d CVE files changed today" , len ( todayCVEFiles ) )
}
if generateYesterdayDeltas {
log . Printf ( "Loading yesterday's changed CVE files from %s" , cfg . ChangedFilesYesterday )
var err error
yesterdayCVEFiles , err = loadChangedFiles ( cfg . ChangedFilesYesterday )
if err != nil {
return fmt . Errorf ( "failed to load yesterday's changed files: %w" , err )
}
log . Printf ( "Found %d CVE files changed yesterday" , len ( yesterdayCVEFiles ) )
}
artifacts := make ( map [ string ] * ArtifactData )
todayArtifacts := make ( map [ string ] * ArtifactData )
yesterdayArtifacts := make ( map [ string ] * ArtifactData )
filesProcessed := 0
filesSkipped := 0
err := filepath . Walk ( cfg . InputDir , func ( path string , info os . FileInfo , err error ) error {
if err != nil {
return err
}
if info . IsDir ( ) || ! strings . HasSuffix ( path , ".json" ) {
return nil
}
osvData , err := parseOSVFile ( path )
if err != nil {
log . Printf ( "Failed to parse %s: %v" , path , err )
filesSkipped ++
return nil
}
inToday := false
inYesterday := false
if generateTodayDeltas {
inToday = shouldIncludeInDelta ( cfg . InputDir , path , todayCVEFiles )
}
if generateYesterdayDeltas {
inYesterday = shouldIncludeInDelta ( cfg . InputDir , path , yesterdayCVEFiles )
}
for _ , affected := range osvData . Affected {
ecosystem := affected . Package . Ecosystem
packageName := affected . Package . Name
ubuntuVer := extractUbuntuVersion ( ecosystem )
if ubuntuVer == "" {
continue
}
// Filter versions based on flags
if targetVersions != nil {
// Inclusive mode: only process if in target list
if ! targetVersions [ ubuntuVer ] {
continue
}
} else if excludedVersions != nil {
// Exclusive mode: skip if in excluded list
if excludedVersions [ ubuntuVer ] {
continue
}
}
// Otherwise auto-detect all versions (no filtering)
cveID := extractCVEID ( osvData )
if cveID == "" {
cveID = osvData . ID
}
introduced , fixed := extractVersionRange ( affected . Ranges )
vuln := ProcessedVuln {
CVE : cveID ,
Published : osvData . Published ,
Modified : osvData . Modified ,
Introduced : introduced ,
Fixed : fixed ,
Versions : affected . Versions ,
}
// Apply any transformations/filters to modify the package name or cve
packages , modifiedVuln := transformVuln ( packageName , cveID , & vuln )
if packages == nil {
continue
}
// Use modified vulnerability if provided, otherwise use original
vulnToUse := & vuln
if modifiedVuln != nil {
vulnToUse = modifiedVuln
}
for _ , pkg := range packages {
if _ , exists := artifacts [ ubuntuVer ] ; ! exists {
artifacts [ ubuntuVer ] = & ArtifactData {
SchemaVersion : "1.0" ,
UbuntuVersion : ubuntuVer ,
Vulnerabilities : make ( map [ string ] [ ] ProcessedVuln ) ,
}
}
artifacts [ ubuntuVer ] . Vulnerabilities [ pkg ] = append ( artifacts [ ubuntuVer ] . Vulnerabilities [ pkg ] , * vulnToUse )
}
// Add to today's delta artifact if this file was changed today
if inToday {
for _ , pkg := range packages {
if _ , exists := todayArtifacts [ ubuntuVer ] ; ! exists {
todayArtifacts [ ubuntuVer ] = & ArtifactData {
SchemaVersion : "1.0" ,
UbuntuVersion : ubuntuVer ,
Vulnerabilities : make ( map [ string ] [ ] ProcessedVuln ) ,
}
}
todayArtifacts [ ubuntuVer ] . Vulnerabilities [ pkg ] = append ( todayArtifacts [ ubuntuVer ] . Vulnerabilities [ pkg ] , * vulnToUse )
}
}
// Add to yesterday's delta artifact if this file was changed yesterday
if inYesterday {
for _ , pkg := range packages {
if _ , exists := yesterdayArtifacts [ ubuntuVer ] ; ! exists {
yesterdayArtifacts [ ubuntuVer ] = & ArtifactData {
SchemaVersion : "1.0" ,
UbuntuVersion : ubuntuVer ,
Vulnerabilities : make ( map [ string ] [ ] ProcessedVuln ) ,
}
}
yesterdayArtifacts [ ubuntuVer ] . Vulnerabilities [ pkg ] = append ( yesterdayArtifacts [ ubuntuVer ] . Vulnerabilities [ pkg ] , * vulnToUse )
}
}
}
filesProcessed ++
if filesProcessed % 1000 == 0 {
log . Printf ( "Processed %d files..." , filesProcessed )
}
return nil
} )
if err != nil {
return fmt . Errorf ( "error walking directory: %w" , err )
}
log . Printf ( "Processed %d files, skipped %d files" , filesProcessed , filesSkipped )
log . Printf ( "Discovered %d Ubuntu versions" , len ( artifacts ) )
// Write full artifacts
for ver , artifact := range artifacts {
artifact . Generated = cfg . GeneratedTimestamp
artifact . TotalCVEs = countTotalCVEs ( artifact )
artifact . TotalPackages = len ( artifact . Vulnerabilities )
outputFile := filepath . Join ( cfg . OutputDir , fmt . Sprintf ( "osv-ubuntu-%s-%s.json.gz" ,
strings . ReplaceAll ( ver , "." , "" ) ,
cfg . DateStr ) )
if err := writeArtifact ( outputFile , artifact ) ; err != nil {
return fmt . Errorf ( "failed to write artifact for Ubuntu %s: %w" , ver , err )
}
log . Printf ( "Ubuntu %s: %d packages, %d CVEs -> %s" ,
ver , artifact . TotalPackages , artifact . TotalCVEs , outputFile )
}
// Write delta artifacts (if any were generated)
if generateTodayDeltas && len ( todayArtifacts ) > 0 {
log . Printf ( "\nWriting today's delta artifacts (%s)..." , cfg . DateStr )
for ver , artifact := range todayArtifacts {
artifact . Generated = cfg . GeneratedTimestamp
artifact . TotalCVEs = countTotalCVEs ( artifact )
artifact . TotalPackages = len ( artifact . Vulnerabilities )
outputFile := filepath . Join ( cfg . OutputDir , fmt . Sprintf ( "osv-ubuntu-%s-delta-%s.json.gz" ,
strings . ReplaceAll ( ver , "." , "" ) , cfg . DateStr ) )
if err := writeArtifact ( outputFile , artifact ) ; err != nil {
return fmt . Errorf ( "failed to write today's delta for Ubuntu %s: %w" , ver , err )
}
log . Printf ( "Ubuntu %s (today): %d packages, %d CVEs -> %s" ,
ver , artifact . TotalPackages , artifact . TotalCVEs , outputFile )
}
}
if generateYesterdayDeltas && len ( yesterdayArtifacts ) > 0 {
log . Printf ( "\nWriting yesterday's delta artifacts (%s)..." , cfg . YesterdayStr )
for ver , artifact := range yesterdayArtifacts {
artifact . Generated = cfg . GeneratedTimestamp
artifact . TotalCVEs = countTotalCVEs ( artifact )
artifact . TotalPackages = len ( artifact . Vulnerabilities )
outputFile := filepath . Join ( cfg . OutputDir , fmt . Sprintf ( "osv-ubuntu-%s-delta-%s.json.gz" ,
strings . ReplaceAll ( ver , "." , "" ) , cfg . YesterdayStr ) )
if err := writeArtifact ( outputFile , artifact ) ; err != nil {
return fmt . Errorf ( "failed to write yesterday's delta for Ubuntu %s: %w" , ver , err )
}
log . Printf ( "Ubuntu %s (yesterday): %d packages, %d CVEs -> %s" ,
ver , artifact . TotalPackages , artifact . TotalCVEs , outputFile )
}
}
return nil
}
func buildVersionFilter ( versions , excludeVersions string ) ( targetVersions , excludedVersions map [ string ] bool ) {
if versions != "" {
// Inclusive mode: only process specified versions
targetVersions = make ( map [ string ] bool )
for ver := range strings . SplitSeq ( versions , "," ) {
trimmed := strings . TrimSpace ( ver )
if trimmed != "" {
targetVersions [ trimmed ] = true
}
}
// If no valid versions were parsed, fall back to auto-detect
if len ( targetVersions ) == 0 {
return nil , nil
}
return targetVersions , nil
}
if excludeVersions != "" {
// Exclusive mode: process all except specified versions
excludedVersions = make ( map [ string ] bool )
for ver := range strings . SplitSeq ( excludeVersions , "," ) {
trimmed := strings . TrimSpace ( ver )
if trimmed != "" {
excludedVersions [ trimmed ] = true
}
}
// If no valid versions were parsed, fall back to auto-detect
if len ( excludedVersions ) == 0 {
return nil , nil
}
return nil , excludedVersions
}
// Auto-detect all versions
return nil , nil
}
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.
* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.
* **Chores**
* Added configuration flag and updated expected config fixtures.
* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 20:59:32 +00:00
func shouldIncludeInDelta ( inputDir , filePath string , changedFiles map [ string ] struct { } ) bool {
2026-03-25 18:02:26 +00:00
relPath , err := filepath . Rel ( inputDir , filePath )
if err != nil {
return false
}
normalizedRelPath := strings . TrimPrefix ( filepath . ToSlash ( relPath ) , "osv/cve/" )
fullRelPath := "osv/cve/" + normalizedRelPath
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.
* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.
* **Chores**
* Added configuration flag and updated expected config fixtures.
* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 20:59:32 +00:00
_ , exists := changedFiles [ fullRelPath ]
return exists
2026-03-25 18:02:26 +00:00
}
func parseOSVFile ( path string ) ( * OSVData , error ) {
data , err := os . ReadFile ( path )
if err != nil {
return nil , err
}
var osv OSVData
if err := json . Unmarshal ( data , & osv ) ; err != nil {
return nil , err
}
return & osv , nil
}
func extractUbuntuVersion ( ecosystem string ) string {
// Example: "Ubuntu:24.04:LTS" -> "24.04"
// Example: "Ubuntu:Pro:22.04:LTS" -> "22.04"
for part := range strings . SplitSeq ( ecosystem , ":" ) {
// Look for version pattern like "24.04", "22.04", "20.04"
if len ( part ) == 5 && strings . Contains ( part , "." ) {
return part
}
}
return ""
}
func extractCVEID ( osv * OSVData ) string {
for _ , upstream := range osv . Upstream {
if strings . HasPrefix ( upstream , "CVE-" ) {
return upstream
}
}
if strings . HasPrefix ( osv . ID , "CVE-" ) {
return osv . ID
}
if strings . HasPrefix ( osv . ID , "UBUNTU-CVE-" ) {
return strings . TrimPrefix ( osv . ID , "UBUNTU-" )
}
return ""
}
func extractVersionRange ( ranges [ ] Range ) ( introduced string , fixed string ) {
for _ , r := range ranges {
if r . Type == "ECOSYSTEM" {
for _ , event := range r . Events {
if event . Introduced != "" && introduced == "" {
introduced = event . Introduced
}
if event . Fixed != "" && fixed == "" {
fixed = event . Fixed
}
}
}
}
return
}
func countTotalCVEs ( artifact * ArtifactData ) int {
seen := make ( map [ string ] bool )
for _ , vulns := range artifact . Vulnerabilities {
for _ , vuln := range vulns {
seen [ vuln . CVE ] = true
}
}
return len ( seen )
}
func writeArtifact ( path string , artifact * ArtifactData ) ( err error ) {
file , err := os . Create ( path )
if err != nil {
return err
}
defer func ( ) {
if cerr := file . Close ( ) ; err == nil && cerr != nil {
err = cerr
}
} ( )
gzWriter := gzip . NewWriter ( file )
defer func ( ) {
if cerr := gzWriter . Close ( ) ; err == nil && cerr != nil {
err = cerr
}
} ( )
encoder := json . NewEncoder ( gzWriter )
if err = encoder . Encode ( artifact ) ; err != nil {
return err
}
return nil
}
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.
* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.
* **Chores**
* Added configuration flag and updated expected config fixtures.
* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 20:59:32 +00:00
func loadChangedFiles ( changedFilesPath string ) ( map [ string ] struct { } , error ) {
2026-03-25 18:02:26 +00:00
file , err := os . Open ( changedFilesPath )
if err != nil {
return nil , fmt . Errorf ( "failed to open changed files list: %w" , err )
}
defer file . Close ( )
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.
* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.
* **Chores**
* Added configuration flag and updated expected config fixtures.
* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 20:59:32 +00:00
changedFiles := make ( map [ string ] struct { } )
2026-03-25 18:02:26 +00:00
scanner := bufio . NewScanner ( file )
for scanner . Scan ( ) {
line := strings . TrimSpace ( scanner . Text ( ) )
if line == "" {
continue
}
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.
* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.
* **Chores**
* Added configuration flag and updated expected config fixtures.
* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 20:59:32 +00:00
changedFiles [ line ] = struct { } { }
2026-03-25 18:02:26 +00:00
}
if err := scanner . Err ( ) ; err != nil {
return nil , fmt . Errorf ( "error reading changed files: %w" , err )
}
return changedFiles , nil
}
2026-04-21 19:39:22 +00:00
// extractRHELVersion extracts the major RHEL version from an ecosystem string.
// Only "enterprise_linux" ecosystems are supported; variants like rhel_e4s, rhel_eus,
// and rhel_software_collections are skipped.
//
// Repository suffixes (appstream, baseos, crb, nfv, realtime) and variant suffixes
// (server, workstation, client, computenode, fastdatapath, hypervisor) are stripped —
// all collapse to the same major version. For example, both
// "Red Hat:enterprise_linux:7::server" and "Red Hat:enterprise_linux:7::workstation"
// map to "7". Deduplication of CVE+package pairs across these variants happens in
// runRHEL.
//
// Examples:
//
// "Red Hat:enterprise_linux:9::appstream" -> "9"
// "Red Hat:enterprise_linux:8::baseos" -> "8"
// "Red Hat:enterprise_linux:7::server" -> "7"
// "Red Hat:enterprise_linux:7::workstation"-> "7"
// "Red Hat:enterprise_linux:10.0" -> "10"
// "Red Hat:enterprise_linux:10.1" -> "10"
// "Red Hat:rhel_e4s:8.8::appstream" -> "" (not enterprise_linux)
func extractRHELVersion ( ecosystem string ) string {
parts := strings . Split ( ecosystem , ":" )
if len ( parts ) < 3 || parts [ 0 ] != "Red Hat" {
return ""
}
if parts [ 1 ] != "enterprise_linux" {
return ""
}
// parts[2] is the version, possibly with minor: "9", "8", "10.0", "10.1"
ver := parts [ 2 ]
// Extract major version only
if dotIdx := strings . Index ( ver , "." ) ; dotIdx >= 0 {
ver = ver [ : dotIdx ]
}
return ver
}
// vulnKey is used for deduplication of CVE+package entries across ecosystems.
type vulnKey struct {
pkg string
cve string
}
func runRHEL ( cfg Config ) error {
// Delta generation is not supported for RHEL — the data source is a full GCS zip
// download with no git-based change tracking. Fail fast if callers pass delta flags.
if cfg . ChangedFilesToday != "" || cfg . ChangedFilesYesterday != "" {
return errors . New ( "--changed-files-today and --changed-files-yesterday are not supported with --platform rhel (no git-based change tracking for GCS data)" )
}
if err := os . MkdirAll ( cfg . OutputDir , 0 o755 ) ; err != nil {
return fmt . Errorf ( "failed to create output directory: %w" , err )
}
targetVersions , excludedVersions := buildVersionFilter ( cfg . Versions , cfg . ExcludeVersions )
log . Printf ( "Processing RHEL OSV files from %s" , cfg . InputDir )
artifacts := make ( map [ string ] * RHELArtifactData )
// Track seen CVE+package pairs per version for deduplication across ecosystems
seen := make ( map [ string ] map [ vulnKey ] struct { } )
filesProcessed := 0
filesSkipped := 0
err := filepath . Walk ( cfg . InputDir , func ( path string , info os . FileInfo , err error ) error {
if err != nil {
return err
}
if info . IsDir ( ) || ! strings . HasSuffix ( path , ".json" ) {
return nil
}
osvData , err := parseOSVFile ( path )
if err != nil {
log . Printf ( "Failed to parse %s: %v" , path , err )
filesSkipped ++
return nil
}
// Extract all CVE IDs from this advisory
cveIDs := extractCVEIDs ( osvData )
if len ( cveIDs ) == 0 {
filesSkipped ++
return nil
}
for _ , affected := range osvData . Affected {
ecosystem := affected . Package . Ecosystem
packageName := affected . Package . Name
rhelVer := extractRHELVersion ( ecosystem )
if rhelVer == "" {
continue
}
if targetVersions != nil {
if ! targetVersions [ rhelVer ] {
continue
}
} else if excludedVersions != nil {
if excludedVersions [ rhelVer ] {
continue
}
}
introduced , fixed := extractVersionRange ( affected . Ranges )
for _ , cveID := range cveIDs {
// Deduplicate: same CVE+package can appear in baseos, appstream, crb
if seen [ rhelVer ] == nil {
seen [ rhelVer ] = make ( map [ vulnKey ] struct { } )
}
key := vulnKey { pkg : packageName , cve : cveID }
if _ , exists := seen [ rhelVer ] [ key ] ; exists {
continue
}
seen [ rhelVer ] [ key ] = struct { } { }
vuln := ProcessedVuln {
CVE : cveID ,
Published : osvData . Published ,
Modified : osvData . Modified ,
Introduced : introduced ,
Fixed : fixed ,
Versions : affected . Versions ,
}
packages , modifiedVuln := transformVuln ( packageName , cveID , & vuln )
if packages == nil {
continue
}
vulnToUse := & vuln
if modifiedVuln != nil {
vulnToUse = modifiedVuln
}
for _ , pkg := range packages {
if _ , exists := artifacts [ rhelVer ] ; ! exists {
artifacts [ rhelVer ] = & RHELArtifactData {
SchemaVersion : "1.0" ,
RHELVersion : rhelVer ,
Vulnerabilities : make ( map [ string ] [ ] ProcessedVuln ) ,
}
}
artifacts [ rhelVer ] . Vulnerabilities [ pkg ] = append ( artifacts [ rhelVer ] . Vulnerabilities [ pkg ] , * vulnToUse )
}
}
}
filesProcessed ++
if filesProcessed % 1000 == 0 {
log . Printf ( "Processed %d files..." , filesProcessed )
}
return nil
} )
if err != nil {
return fmt . Errorf ( "error walking directory: %w" , err )
}
log . Printf ( "Processed %d files, skipped %d files" , filesProcessed , filesSkipped )
log . Printf ( "Discovered %d RHEL versions" , len ( artifacts ) )
for ver , artifact := range artifacts {
artifact . Generated = cfg . GeneratedTimestamp
artifact . TotalCVEs = countTotalRHELCVEs ( artifact )
artifact . TotalPackages = len ( artifact . Vulnerabilities )
outputFile := filepath . Join ( cfg . OutputDir , fmt . Sprintf ( "osv-rhel-%s-%s.json.gz" , ver , cfg . DateStr ) )
if err := writeRHELArtifact ( outputFile , artifact ) ; err != nil {
return fmt . Errorf ( "failed to write artifact for RHEL %s: %w" , ver , err )
}
log . Printf ( "RHEL %s: %d packages, %d CVEs -> %s" ,
ver , artifact . TotalPackages , artifact . TotalCVEs , outputFile )
}
return nil
}
// extractCVEIDs returns all CVE IDs from an OSV entry.
// RHEL advisories list CVEs in the "upstream" field (same as Ubuntu).
func extractCVEIDs ( osv * OSVData ) [ ] string {
var cves [ ] string
for _ , upstream := range osv . Upstream {
if strings . HasPrefix ( upstream , "CVE-" ) {
cves = append ( cves , upstream )
}
}
// Fallback: check Related field
if len ( cves ) == 0 {
for _ , related := range osv . Related {
if strings . HasPrefix ( related , "CVE-" ) {
cves = append ( cves , related )
}
}
}
// Fallback: check ID itself
if len ( cves ) == 0 {
if strings . HasPrefix ( osv . ID , "CVE-" ) {
cves = append ( cves , osv . ID )
}
}
return cves
}
func countTotalRHELCVEs ( artifact * RHELArtifactData ) int {
seen := make ( map [ string ] bool )
for _ , vulns := range artifact . Vulnerabilities {
for _ , vuln := range vulns {
seen [ vuln . CVE ] = true
}
}
return len ( seen )
}
func writeRHELArtifact ( path string , artifact * RHELArtifactData ) ( err error ) {
file , err := os . Create ( path )
if err != nil {
return err
}
defer func ( ) {
if cerr := file . Close ( ) ; err == nil && cerr != nil {
err = cerr
}
} ( )
gzWriter := gzip . NewWriter ( file )
defer func ( ) {
if cerr := gzWriter . Close ( ) ; err == nil && cerr != nil {
err = cerr
}
} ( )
encoder := json . NewEncoder ( gzWriter )
if err = encoder . Encode ( artifact ) ; err != nil {
return err
}
return nil
}