package main import ( "compress/gzip" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestExtractUbuntuVersion(t *testing.T) { tests := []struct { name string ecosystem string expected string }{ { name: "Standard Ubuntu LTS", ecosystem: "Ubuntu:24.04:LTS", expected: "24.04", }, { name: "Ubuntu Pro", ecosystem: "Ubuntu:Pro:22.04:LTS", expected: "22.04", }, { name: "Ubuntu 20.04", ecosystem: "Ubuntu:20.04:LTS", expected: "20.04", }, { name: "Ubuntu 18.04", ecosystem: "Ubuntu:18.04:LTS", expected: "18.04", }, { name: "No version pattern", ecosystem: "Ubuntu:LTS", expected: "", }, { name: "Empty string", ecosystem: "", expected: "", }, { name: "Not Ubuntu", ecosystem: "Debian:12:stable", expected: "", }, { name: "Version without LTS", ecosystem: "Ubuntu:24.04", expected: "24.04", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractUbuntuVersion(tt.ecosystem) require.Equal(t, tt.expected, result) }) } } func TestExtractCVEID(t *testing.T) { tests := []struct { name string osv *OSVData expected string }{ { name: "CVE in Upstream field", osv: &OSVData{ ID: "UBUNTU-CVE-2024-1234", Upstream: []string{"CVE-2024-1234", "https://example.com"}, }, expected: "CVE-2024-1234", }, { name: "CVE as ID", osv: &OSVData{ ID: "CVE-2024-5678", Upstream: []string{}, }, expected: "CVE-2024-5678", }, { name: "UBUNTU-CVE prefix", osv: &OSVData{ ID: "UBUNTU-CVE-2024-9999", Upstream: []string{}, }, expected: "CVE-2024-9999", }, { name: "No CVE found", osv: &OSVData{ ID: "SOME-OTHER-ID", Upstream: []string{"https://example.com"}, }, expected: "", }, { name: "Multiple upstreams with CVE first", osv: &OSVData{ ID: "UBUNTU-123", Upstream: []string{"CVE-2024-1111", "CVE-2024-2222"}, }, expected: "CVE-2024-1111", }, { name: "Empty upstream, no CVE in ID", osv: &OSVData{ ID: "USN-1234-1", Upstream: []string{}, }, expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractCVEID(tt.osv) require.Equal(t, tt.expected, result) }) } } func TestExtractVersionRange(t *testing.T) { tests := []struct { name string ranges []Range expectedIntroduced string expectedFixed string }{ { name: "Simple range with introduced and fixed", ranges: []Range{ { Type: "ECOSYSTEM", Events: []Event{ {Introduced: "1.0.0"}, {Fixed: "2.0.0"}, }, }, }, expectedIntroduced: "1.0.0", expectedFixed: "2.0.0", }, { name: "Only introduced version", ranges: []Range{ { Type: "ECOSYSTEM", Events: []Event{ {Introduced: "1.5.0"}, }, }, }, expectedIntroduced: "1.5.0", expectedFixed: "", }, { name: "Only fixed version", ranges: []Range{ { Type: "ECOSYSTEM", Events: []Event{ {Fixed: "3.0.0"}, }, }, }, expectedIntroduced: "", expectedFixed: "3.0.0", }, { name: "Multiple ranges, first ECOSYSTEM wins", ranges: []Range{ { Type: "ECOSYSTEM", Events: []Event{ {Introduced: "1.0.0"}, {Fixed: "2.0.0"}, }, }, { Type: "ECOSYSTEM", Events: []Event{ {Introduced: "3.0.0"}, {Fixed: "4.0.0"}, }, }, }, expectedIntroduced: "1.0.0", expectedFixed: "2.0.0", }, { name: "Non-ECOSYSTEM range ignored", ranges: []Range{ { Type: "GIT", Events: []Event{ {Introduced: "abc123"}, {Fixed: "def456"}, }, }, { Type: "ECOSYSTEM", Events: []Event{ {Introduced: "2.0.0"}, {Fixed: "2.5.0"}, }, }, }, expectedIntroduced: "2.0.0", expectedFixed: "2.5.0", }, { name: "Empty ranges", ranges: []Range{}, expectedIntroduced: "", expectedFixed: "", }, { name: "Multiple events in single range", ranges: []Range{ { Type: "ECOSYSTEM", Events: []Event{ {Introduced: "0"}, {Fixed: "1.2.3"}, {Introduced: "2.0.0"}, // This should be ignored (first introduced wins) }, }, }, expectedIntroduced: "0", expectedFixed: "1.2.3", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { introduced, fixed := extractVersionRange(tt.ranges) require.Equal(t, tt.expectedIntroduced, introduced) require.Equal(t, tt.expectedFixed, fixed) }) } } func TestCountTotalCVEs(t *testing.T) { tests := []struct { name string artifact *ArtifactData expected int }{ { name: "Multiple packages with unique CVEs", artifact: &ArtifactData{ Vulnerabilities: map[string][]ProcessedVuln{ "curl": { {CVE: "CVE-2024-1234"}, {CVE: "CVE-2024-5678"}, }, "openssl": { {CVE: "CVE-2024-9999"}, }, }, }, expected: 3, }, { name: "Duplicate CVEs across packages", artifact: &ArtifactData{ Vulnerabilities: map[string][]ProcessedVuln{ "emacs": { {CVE: "CVE-2024-39331"}, }, "emacs-common": { {CVE: "CVE-2024-39331"}, }, "emacs-el": { {CVE: "CVE-2024-39331"}, }, }, }, expected: 1, // Deduplicated }, { name: "Mix of unique and duplicate CVEs", artifact: &ArtifactData{ Vulnerabilities: map[string][]ProcessedVuln{ "package1": { {CVE: "CVE-2024-1111"}, {CVE: "CVE-2024-2222"}, }, "package2": { {CVE: "CVE-2024-1111"}, // Duplicate {CVE: "CVE-2024-3333"}, }, }, }, expected: 3, // CVE-2024-1111, CVE-2024-2222, CVE-2024-3333 }, { name: "Empty artifact", artifact: &ArtifactData{ Vulnerabilities: map[string][]ProcessedVuln{}, }, expected: 0, }, { name: "Package with no vulnerabilities", artifact: &ArtifactData{ Vulnerabilities: map[string][]ProcessedVuln{ "safe-package": {}, }, }, expected: 0, }, { name: "Single package, single CVE", artifact: &ArtifactData{ Vulnerabilities: map[string][]ProcessedVuln{ "apache2": { {CVE: "CVE-2024-7777"}, }, }, }, expected: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := countTotalCVEs(tt.artifact) require.Equal(t, tt.expected, result) }) } } func TestBuildVersionFilter(t *testing.T) { tests := []struct { name string versions string excludeVersions string expectedTargetVersions map[string]bool expectedExcludedVersions map[string]bool }{ { name: "Inclusive mode: single version", versions: "20.04", excludeVersions: "", expectedTargetVersions: map[string]bool{"20.04": true}, expectedExcludedVersions: nil, }, { name: "Inclusive mode: multiple versions", versions: "20.04,22.04,24.04", excludeVersions: "", expectedTargetVersions: map[string]bool{"20.04": true, "22.04": true, "24.04": true}, expectedExcludedVersions: nil, }, { name: "Inclusive mode: with spaces", versions: "20.04, 22.04, 24.04", excludeVersions: "", expectedTargetVersions: map[string]bool{"20.04": true, "22.04": true, "24.04": true}, expectedExcludedVersions: nil, }, { name: "Exclusive mode: single version", versions: "", excludeVersions: "14.04", expectedTargetVersions: nil, expectedExcludedVersions: map[string]bool{"14.04": true}, }, { name: "Exclusive mode: multiple versions", versions: "", excludeVersions: "14.04,16.04,24.10", expectedTargetVersions: nil, expectedExcludedVersions: map[string]bool{"14.04": true, "16.04": true, "24.10": true}, }, { name: "Exclusive mode: with spaces", versions: "", excludeVersions: "14.04, 16.04, 24.10", expectedTargetVersions: nil, expectedExcludedVersions: map[string]bool{"14.04": true, "16.04": true, "24.10": true}, }, { name: "Auto-detect mode: both empty", versions: "", excludeVersions: "", expectedTargetVersions: nil, expectedExcludedVersions: nil, }, { name: "Inclusive takes precedence: both provided", versions: "20.04,22.04", excludeVersions: "14.04,16.04", expectedTargetVersions: map[string]bool{"20.04": true, "22.04": true}, expectedExcludedVersions: nil, }, { name: "Inclusive mode: trailing comma ignored", versions: "20.04,22.04,", excludeVersions: "", expectedTargetVersions: map[string]bool{"20.04": true, "22.04": true}, expectedExcludedVersions: nil, }, { name: "Inclusive mode: leading comma ignored", versions: ",20.04,22.04", excludeVersions: "", expectedTargetVersions: map[string]bool{"20.04": true, "22.04": true}, expectedExcludedVersions: nil, }, { name: "Inclusive mode: multiple commas ignored", versions: "20.04,,22.04", excludeVersions: "", expectedTargetVersions: map[string]bool{"20.04": true, "22.04": true}, expectedExcludedVersions: nil, }, { name: "Exclusive mode: trailing comma ignored", versions: "", excludeVersions: "14.04,16.04,", expectedTargetVersions: nil, expectedExcludedVersions: map[string]bool{"14.04": true, "16.04": true}, }, { name: "Inclusive mode: only empty strings falls back to auto-detect", versions: ",,", excludeVersions: "", expectedTargetVersions: nil, expectedExcludedVersions: nil, }, { name: "Inclusive mode: only whitespace falls back to auto-detect", versions: " , , ", excludeVersions: "", expectedTargetVersions: nil, expectedExcludedVersions: nil, }, { name: "Exclusive mode: only empty strings falls back to auto-detect", versions: "", excludeVersions: ",,", expectedTargetVersions: nil, expectedExcludedVersions: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { targetVersions, excludedVersions := buildVersionFilter(tt.versions, tt.excludeVersions) require.Equal(t, tt.expectedTargetVersions, targetVersions) require.Equal(t, tt.expectedExcludedVersions, excludedVersions) }) } } func TestShouldIncludeInDelta(t *testing.T) { tests := []struct { name string inputDir string filePath string changedFiles map[string]struct{} expectedMatch bool }{ { name: "Unix path: file in changed set", inputDir: "/tmp/ubuntu-osv", filePath: "/tmp/ubuntu-osv/osv/cve/CVE-2024-1234.json", changedFiles: map[string]struct{}{ "osv/cve/CVE-2024-1234.json": {}, }, expectedMatch: true, }, { name: "Unix path: file not in changed set", inputDir: "/tmp/ubuntu-osv", filePath: "/tmp/ubuntu-osv/osv/cve/CVE-2024-9999.json", changedFiles: map[string]struct{}{ "osv/cve/CVE-2024-1234.json": {}, }, expectedMatch: false, }, { name: "Nested directory: file in changed set", inputDir: "/data/osv", filePath: "/data/osv/osv/cve/2024/CVE-2024-1111.json", changedFiles: map[string]struct{}{ "osv/cve/2024/CVE-2024-1111.json": {}, }, expectedMatch: true, }, { name: "File already has osv/cve prefix in relative path", inputDir: "/workspace", filePath: "/workspace/osv/cve/CVE-2024-2222.json", changedFiles: map[string]struct{}{ "osv/cve/CVE-2024-2222.json": {}, }, expectedMatch: true, }, { name: "Changed files with leading slash (should not match)", inputDir: "/tmp/ubuntu-osv", filePath: "/tmp/ubuntu-osv/osv/cve/CVE-2024-3333.json", changedFiles: map[string]struct{}{ "/osv/cve/CVE-2024-3333.json": {}, // Wrong: has leading slash }, expectedMatch: false, }, { name: "Changed files without osv/cve prefix (should not match)", inputDir: "/tmp/ubuntu-osv", filePath: "/tmp/ubuntu-osv/osv/cve/CVE-2024-4444.json", changedFiles: map[string]struct{}{ "CVE-2024-4444.json": {}, // Wrong: missing osv/cve/ prefix }, expectedMatch: false, }, { name: "Empty changed files set", inputDir: "/tmp/ubuntu-osv", filePath: "/tmp/ubuntu-osv/osv/cve/CVE-2024-5555.json", changedFiles: map[string]struct{}{}, expectedMatch: false, }, { name: "File outside input directory tree (relative path doesn't match)", inputDir: "/tmp/ubuntu-osv/subdir", filePath: "/tmp/other-dir/osv/cve/CVE-2024-6666.json", changedFiles: map[string]struct{}{ "osv/cve/CVE-2024-6666.json": {}, }, expectedMatch: false, // filepath.Rel will work but path won't match }, { name: "Multiple files in changed set, match one", inputDir: "/data/osv", filePath: "/data/osv/osv/cve/CVE-2024-7777.json", changedFiles: map[string]struct{}{ "osv/cve/CVE-2024-1111.json": {}, "osv/cve/CVE-2024-7777.json": {}, "osv/cve/CVE-2024-9999.json": {}, }, expectedMatch: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := shouldIncludeInDelta(tt.inputDir, tt.filePath, tt.changedFiles) require.Equal(t, tt.expectedMatch, result) }) } } func TestRun(t *testing.T) { // Create temporary directories for input and output inputDir := t.TempDir() outputDir := t.TempDir() // Create a simple test OSV file testOSVData := `{ "schema_version": "1.0", "id": "USN-1234-1", "published": "2024-01-01T00:00:00Z", "modified": "2024-01-02T00:00:00Z", "details": "Test vulnerability", "affected": [{ "package": { "ecosystem": "Ubuntu:22.04:LTS", "name": "test-package" }, "ranges": [{ "type": "ECOSYSTEM", "events": [ {"introduced": "0"}, {"fixed": "1.2.3"} ] }] }], "upstream": ["CVE-2024-1234"] }` // Write test file require.NoError(t, os.WriteFile(filepath.Join(inputDir, "CVE-2024-1234.json"), []byte(testOSVData), 0o644)) // Create config cfg := Config{ InputDir: inputDir, OutputDir: outputDir, Versions: "", ExcludeVersions: "", ChangedFilesToday: "", ChangedFilesYesterday: "", DateStr: "2024-01-03", YesterdayStr: "2024-01-02", GeneratedTimestamp: "2024-01-03T00:00:00Z", RunTime: time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC), } // Run the function err := run(cfg) require.NoError(t, err) // Verify output artifact was created expectedFile := filepath.Join(outputDir, "osv-ubuntu-2204-2024-01-03.json.gz") require.FileExists(t, expectedFile) // Verify artifact content (decompress and check) artifact, err := readArtifact(expectedFile) require.NoError(t, err) require.Equal(t, "1.0", artifact.SchemaVersion) require.Equal(t, "22.04", artifact.UbuntuVersion) require.Equal(t, 1, artifact.TotalCVEs) require.Equal(t, 1, artifact.TotalPackages) require.Contains(t, artifact.Vulnerabilities, "test-package") require.Len(t, artifact.Vulnerabilities["test-package"], 1) require.Equal(t, "CVE-2024-1234", artifact.Vulnerabilities["test-package"][0].CVE) } func TestRunWithDeltaGeneration(t *testing.T) { // Create temporary directories inputDir := t.TempDir() outputDir := t.TempDir() changedFilesDir := t.TempDir() // Create two test OSV files testOSVData1 := `{ "schema_version": "1.0", "id": "USN-1234-1", "published": "2024-01-01T00:00:00Z", "modified": "2024-01-02T00:00:00Z", "affected": [{ "package": { "ecosystem": "Ubuntu:22.04:LTS", "name": "changed-today-package" }, "ranges": [{ "type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "1.0"}] }] }], "upstream": ["CVE-2024-1111"] }` testOSVData2 := `{ "schema_version": "1.0", "id": "USN-5678-1", "published": "2024-01-01T00:00:00Z", "modified": "2024-01-02T00:00:00Z", "affected": [{ "package": { "ecosystem": "Ubuntu:22.04:LTS", "name": "changed-yesterday-package" }, "ranges": [{ "type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "2.0"}] }] }], "upstream": ["CVE-2024-2222"] }` // Write test files osvCveDir := filepath.Join(inputDir, "osv", "cve") require.NoError(t, os.MkdirAll(osvCveDir, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(osvCveDir, "CVE-2024-1111.json"), []byte(testOSVData1), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(osvCveDir, "CVE-2024-2222.json"), []byte(testOSVData2), 0o644)) // Create changed files lists changedTodayFile := filepath.Join(changedFilesDir, "changed_today.txt") changedYesterdayFile := filepath.Join(changedFilesDir, "changed_yesterday.txt") require.NoError(t, os.WriteFile(changedTodayFile, []byte("osv/cve/CVE-2024-1111.json\n"), 0o644)) require.NoError(t, os.WriteFile(changedYesterdayFile, []byte("osv/cve/CVE-2024-2222.json\n"), 0o644)) // Create config with delta generation cfg := Config{ InputDir: inputDir, OutputDir: outputDir, Versions: "", ExcludeVersions: "", ChangedFilesToday: changedTodayFile, ChangedFilesYesterday: changedYesterdayFile, DateStr: "2024-01-03", YesterdayStr: "2024-01-02", GeneratedTimestamp: "2024-01-03T00:00:00Z", RunTime: time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC), } // Run err := run(cfg) require.NoError(t, err) // Verify full artifact fullArtifact, err := readArtifact(filepath.Join(outputDir, "osv-ubuntu-2204-2024-01-03.json.gz")) require.NoError(t, err) require.Equal(t, 2, fullArtifact.TotalCVEs) require.Equal(t, 2, fullArtifact.TotalPackages) // Verify today's delta artifact todayDelta, err := readArtifact(filepath.Join(outputDir, "osv-ubuntu-2204-delta-2024-01-03.json.gz")) require.NoError(t, err) require.Equal(t, 1, todayDelta.TotalCVEs) require.Equal(t, 1, todayDelta.TotalPackages) require.NotNil(t, todayDelta.Vulnerabilities) require.Contains(t, todayDelta.Vulnerabilities, "changed-today-package") require.NotEmpty(t, todayDelta.Vulnerabilities["changed-today-package"]) require.Equal(t, "CVE-2024-1111", todayDelta.Vulnerabilities["changed-today-package"][0].CVE) // Verify yesterday's delta artifact yesterdayDelta, err := readArtifact(filepath.Join(outputDir, "osv-ubuntu-2204-delta-2024-01-02.json.gz")) require.NoError(t, err) require.Equal(t, 1, yesterdayDelta.TotalCVEs) require.Equal(t, 1, yesterdayDelta.TotalPackages) require.NotNil(t, yesterdayDelta.Vulnerabilities) require.Contains(t, yesterdayDelta.Vulnerabilities, "changed-yesterday-package") require.NotEmpty(t, yesterdayDelta.Vulnerabilities["changed-yesterday-package"]) require.Equal(t, "CVE-2024-2222", yesterdayDelta.Vulnerabilities["changed-yesterday-package"][0].CVE) } func TestRunWithVersionFiltering(t *testing.T) { tests := []struct { name string versions string excludeVersions string expectedVersionCount int expectedVersions []string }{ { name: "Inclusive filtering - single version", versions: "22.04", excludeVersions: "", expectedVersionCount: 1, expectedVersions: []string{"22.04"}, }, { name: "Inclusive filtering - multiple versions", versions: "20.04,22.04", excludeVersions: "", expectedVersionCount: 2, expectedVersions: []string{"20.04", "22.04"}, }, { name: "Exclusive filtering - exclude one version", versions: "", excludeVersions: "24.04", expectedVersionCount: 2, expectedVersions: []string{"20.04", "22.04"}, }, { name: "Auto-detect - no filtering", versions: "", excludeVersions: "", expectedVersionCount: 3, expectedVersions: []string{"20.04", "22.04", "24.04"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create temporary directories inputDir := t.TempDir() outputDir := t.TempDir() // Create test OSV files for different Ubuntu versions for _, ver := range []string{"20.04", "22.04", "24.04"} { data := fmt.Sprintf(`{ "schema_version": "1.0", "id": "USN-1234-1", "published": "2024-01-01T00:00:00Z", "modified": "2024-01-02T00:00:00Z", "affected": [{ "package": { "ecosystem": "Ubuntu:%s:LTS", "name": "test-package-%s" }, "ranges": [{ "type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "1.0"}] }] }], "upstream": ["CVE-2024-%s"] }`, ver, strings.ReplaceAll(ver, ".", ""), strings.ReplaceAll(ver, ".", "")) filename := fmt.Sprintf("CVE-2024-%s.json", strings.ReplaceAll(ver, ".", "")) require.NoError(t, os.WriteFile(filepath.Join(inputDir, filename), []byte(data), 0o644)) } // Create config cfg := Config{ InputDir: inputDir, OutputDir: outputDir, Versions: tt.versions, ExcludeVersions: tt.excludeVersions, ChangedFilesToday: "", ChangedFilesYesterday: "", DateStr: "2024-01-03", YesterdayStr: "2024-01-02", GeneratedTimestamp: "2024-01-03T00:00:00Z", RunTime: time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC), } // Run err := run(cfg) require.NoError(t, err) // Count artifacts created files, err := filepath.Glob(filepath.Join(outputDir, "osv-ubuntu-*.json.gz")) require.NoError(t, err) require.Equal(t, tt.expectedVersionCount, len(files)) // Verify expected versions were generated for _, expectedVer := range tt.expectedVersions { verStr := strings.ReplaceAll(expectedVer, ".", "") expectedFile := filepath.Join(outputDir, fmt.Sprintf("osv-ubuntu-%s-2024-01-03.json.gz", verStr)) require.FileExists(t, expectedFile) artifact, err := readArtifact(expectedFile) require.NoError(t, err) require.Equal(t, expectedVer, artifact.UbuntuVersion) } }) } } func TestExtractRHELVersion(t *testing.T) { tests := []struct { name string ecosystem string expected string }{ { name: "RHEL 9 appstream", ecosystem: "Red Hat:enterprise_linux:9::appstream", expected: "9", }, { name: "RHEL 8 baseos", ecosystem: "Red Hat:enterprise_linux:8::baseos", expected: "8", }, { name: "RHEL 8 crb", ecosystem: "Red Hat:enterprise_linux:8::crb", expected: "8", }, { name: "RHEL 10 with minor version", ecosystem: "Red Hat:enterprise_linux:10.0", expected: "10", }, { name: "RHEL 10.1 with minor version", ecosystem: "Red Hat:enterprise_linux:10.1", expected: "10", }, { name: "RHEL 7 software collections", ecosystem: "Red Hat:rhel_software_collections:3::el7", expected: "", }, { name: "RHEL EUS not supported", ecosystem: "Red Hat:rhel_e4s:8.8::appstream", expected: "", }, { name: "Empty string", ecosystem: "", expected: "", }, { name: "Ubuntu ecosystem", ecosystem: "Ubuntu:24.04:LTS", expected: "", }, { name: "RHEL 9 no repository suffix", ecosystem: "Red Hat:enterprise_linux:9", expected: "9", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractRHELVersion(tt.ecosystem) require.Equal(t, tt.expected, result) }) } } func TestExtractCVEIDs(t *testing.T) { tests := []struct { name string osv *OSVData expected []string }{ { name: "CVEs in upstream", osv: &OSVData{ ID: "RHSA-2025:9978", Upstream: []string{"CVE-2025-32462"}, }, expected: []string{"CVE-2025-32462"}, }, { name: "Multiple CVEs in upstream", osv: &OSVData{ ID: "RHSA-2026:0001", Upstream: []string{"CVE-2025-59375", "CVE-2025-6965", "CVE-2025-8176", "CVE-2025-9900"}, }, expected: []string{"CVE-2025-59375", "CVE-2025-6965", "CVE-2025-8176", "CVE-2025-9900"}, }, { name: "CVE in related field as fallback", osv: &OSVData{ ID: "RHSA-2025:1234", Related: []string{"CVE-2025-1111"}, }, expected: []string{"CVE-2025-1111"}, }, { name: "CVE as ID fallback", osv: &OSVData{ ID: "CVE-2025-9999", }, expected: []string{"CVE-2025-9999"}, }, { name: "No CVE found", osv: &OSVData{ ID: "RHBA-2025:5678", Upstream: []string{"https://example.com"}, }, expected: nil, }, { name: "Non-CVE upstream entries filtered", osv: &OSVData{ ID: "RHSA-2025:0001", Upstream: []string{"https://bugzilla.redhat.com/123", "CVE-2025-4444"}, }, expected: []string{"CVE-2025-4444"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractCVEIDs(tt.osv) require.Equal(t, tt.expected, result) }) } } func TestRunRHEL(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Create a RHEL OSV advisory with one CVE affecting sudo on RHEL 9 testData := `{ "schema_version": "1.7.5", "id": "RHSA-2025:9978", "published": "2025-07-01T10:06:01Z", "modified": "2026-03-18T11:30:33Z", "upstream": ["CVE-2025-32462"], "summary": "sudo security update", "affected": [ { "package": { "name": "sudo", "ecosystem": "Red Hat:enterprise_linux:9::appstream" }, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "0:1.9.5p2-10.el9_6.1"}]}] }, { "package": { "name": "sudo", "ecosystem": "Red Hat:enterprise_linux:9::baseos" }, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "0:1.9.5p2-10.el9_6.1"}]}] } ] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "RHSA-2025-9978.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "rhel", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-04-08", GeneratedTimestamp: "2026-04-08T00:00:00Z", RunTime: time.Date(2026, 4, 8, 0, 0, 0, 0, time.UTC), } err := runRHEL(cfg) require.NoError(t, err) // Verify artifact was created expectedFile := filepath.Join(outputDir, "osv-rhel-9-2026-04-08.json.gz") require.FileExists(t, expectedFile) artifact, err := readRHELArtifact(expectedFile) require.NoError(t, err) require.Equal(t, "1.0", artifact.SchemaVersion) require.Equal(t, "9", artifact.RHELVersion) require.Equal(t, 1, artifact.TotalCVEs) require.Contains(t, artifact.Vulnerabilities, "sudo") // Deduplication: sudo appears in both appstream and baseos, should be deduplicated require.Len(t, artifact.Vulnerabilities["sudo"], 1) require.Equal(t, "CVE-2025-32462", artifact.Vulnerabilities["sudo"][0].CVE) require.Equal(t, "0:1.9.5p2-10.el9_6.1", artifact.Vulnerabilities["sudo"][0].Fixed) } func TestRunRHELMultiCVEAdvisory(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Advisory with 2 CVEs and packages across RHEL 8 and 9 testData := `{ "schema_version": "1.7.5", "id": "RHSA-2026:0001", "published": "2026-01-05T10:11:47Z", "modified": "2026-04-03T10:05:48Z", "upstream": ["CVE-2025-1111", "CVE-2025-2222"], "affected": [ { "package": {"name": "curl", "ecosystem": "Red Hat:enterprise_linux:9::baseos"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "0:7.76.1-29.el9_4.2"}]}] }, { "package": {"name": "curl", "ecosystem": "Red Hat:enterprise_linux:8::baseos"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "0:7.61.1-34.el8_10"}]}] } ] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "RHSA-2026-0001.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "rhel", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-04-08", GeneratedTimestamp: "2026-04-08T00:00:00Z", RunTime: time.Date(2026, 4, 8, 0, 0, 0, 0, time.UTC), } err := runRHEL(cfg) require.NoError(t, err) // Should produce artifacts for both RHEL 8 and 9 rhel9, err := readRHELArtifact(filepath.Join(outputDir, "osv-rhel-9-2026-04-08.json.gz")) require.NoError(t, err) require.Equal(t, 2, rhel9.TotalCVEs) require.Len(t, rhel9.Vulnerabilities["curl"], 2) rhel8, err := readRHELArtifact(filepath.Join(outputDir, "osv-rhel-8-2026-04-08.json.gz")) require.NoError(t, err) require.Equal(t, 2, rhel8.TotalCVEs) require.Len(t, rhel8.Vulnerabilities["curl"], 2) } func TestRunRHELVersionFiltering(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() testData := `{ "schema_version": "1.7.5", "id": "RHSA-2025:1234", "published": "2025-01-01T00:00:00Z", "modified": "2025-01-02T00:00:00Z", "upstream": ["CVE-2025-5555"], "affected": [ {"package": {"name": "pkg", "ecosystem": "Red Hat:enterprise_linux:8::baseos"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "0:1.0-1.el8"}]}]}, {"package": {"name": "pkg", "ecosystem": "Red Hat:enterprise_linux:9::baseos"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "0:1.0-1.el9"}]}]} ] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "RHSA-2025-1234.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "rhel", InputDir: inputDir, OutputDir: outputDir, Versions: "9", DateStr: "2026-04-08", GeneratedTimestamp: "2026-04-08T00:00:00Z", RunTime: time.Date(2026, 4, 8, 0, 0, 0, 0, time.UTC), } err := runRHEL(cfg) require.NoError(t, err) // Only RHEL 9 should be generated require.FileExists(t, filepath.Join(outputDir, "osv-rhel-9-2026-04-08.json.gz")) _, err = os.Stat(filepath.Join(outputDir, "osv-rhel-8-2026-04-08.json.gz")) require.True(t, os.IsNotExist(err), "RHEL 8 artifact should not exist when filtering to version 9") } func readRHELArtifact(path string) (*RHELArtifactData, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() gzReader, err := gzip.NewReader(file) if err != nil { return nil, err } defer gzReader.Close() var artifact RHELArtifactData if err := json.NewDecoder(gzReader).Decode(&artifact); err != nil { return nil, err } return &artifact, nil } func readArtifact(path string) (*ArtifactData, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() gzReader, err := gzip.NewReader(file) if err != nil { return nil, err } defer gzReader.Close() var artifact ArtifactData if err := json.NewDecoder(gzReader).Decode(&artifact); err != nil { return nil, err } return &artifact, nil } func readAndroidArtifact(path string) (*AndroidArtifactData, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() gzReader, err := gzip.NewReader(file) if err != nil { return nil, err } defer gzReader.Close() var artifact AndroidArtifactData if err := json.NewDecoder(gzReader).Decode(&artifact); err != nil { return nil, err } return &artifact, nil } func TestParseAndroidRangeEvent(t *testing.T) { tests := []struct { name string event string expectedMajor string expectedValue string }{ { name: "standard version with SPL date", event: "16:2026-05-01", expectedMajor: "16", expectedValue: "2026-05-01", }, { name: "version with zero introduced", event: "16:0", expectedMajor: "16", expectedValue: "0", }, { name: "next suffix stripped", event: "15-next:2025-01-01", expectedMajor: "15", expectedValue: "2025-01-01", }, { name: "qpr suffix stripped", event: "16-qpr2:2026-06-01", expectedMajor: "16", expectedValue: "2026-06-01", }, { name: "qpr-next suffix stripped", event: "16-qpr2-next:2026-07-01", expectedMajor: "16", expectedValue: "2026-07-01", }, { name: "kernel-only with empty prefix", event: ":0", expectedMajor: "", expectedValue: "0", }, { name: "kernel-only with SPL date", event: ":2020-09-05", expectedMajor: "", expectedValue: "2020-09-05", }, { name: "SoCVersion prefix", event: "SoCVersion:2020-09-05", expectedMajor: "SoCVersion", expectedValue: "2020-09-05", }, { name: "old version 8.1", event: "8.1:2021-01-01", expectedMajor: "8.1", expectedValue: "2021-01-01", }, { name: "12L version", event: "12L:2022-12-01", expectedMajor: "12L", expectedValue: "2022-12-01", }, { name: "12L-next version", event: "12L-next:2022-06-01", expectedMajor: "12L", expectedValue: "2022-06-01", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { major, value := parseAndroidRangeEvent(tt.event) require.Equal(t, tt.expectedMajor, major) require.Equal(t, tt.expectedValue, value) }) } } func TestExtractAndroidCVEIDs(t *testing.T) { tests := []struct { name string osv *OSVData expected []string }{ { name: "CVE in aliases", osv: &OSVData{ ID: "ASB-A-111893654", Aliases: []string{"A-111893654", "CVE-2020-0404"}, }, expected: []string{"CVE-2020-0404"}, }, { name: "multiple CVEs in aliases", osv: &OSVData{ ID: "ASB-A-999999999", Aliases: []string{"CVE-2025-1111", "A-999999999", "CVE-2025-2222"}, }, expected: []string{"CVE-2025-1111", "CVE-2025-2222"}, }, { name: "no CVE in aliases", osv: &OSVData{ ID: "ASB-A-123456789", Aliases: []string{"A-123456789"}, }, expected: nil, }, { name: "empty aliases", osv: &OSVData{ ID: "ASB-A-000000000", }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractAndroidCVEIDs(tt.osv) require.Equal(t, tt.expected, result) }) } } func TestRunAndroid(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Android 16 framework vulnerability fixed in June 2026 SPL testData := `{ "schema_version": "1.7.5", "id": "ASB-A-340239088", "published": "2026-06-01T00:00:00Z", "modified": "2026-07-14T00:00:00Z", "aliases": ["A-340239088", "CVE-2026-12345"], "affected": [{ "package": { "name": "platform/frameworks/base", "ecosystem": "Android" }, "ranges": [{ "type": "ECOSYSTEM", "events": [ {"introduced": "16:0"}, {"fixed": "16:2026-06-01"} ] }], "versions": ["16"], "ecosystem_specific": { "severity": "High", "spl": "2026-06-01" } }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "ASB-A-340239088.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) expectedFile := filepath.Join(outputDir, "osv-android-16-2026-07-14.json.gz") require.FileExists(t, expectedFile) artifact, err := readAndroidArtifact(expectedFile) require.NoError(t, err) require.Equal(t, "1.0", artifact.SchemaVersion) require.Equal(t, "16", artifact.AndroidVersion) require.Equal(t, 1, artifact.TotalCVEs) require.Len(t, artifact.Vulnerabilities, 1) require.Equal(t, "CVE-2026-12345", artifact.Vulnerabilities[0].CVE) require.Equal(t, "2026-06-01", artifact.Vulnerabilities[0].FixedSPL) require.Equal(t, "High", artifact.Vulnerabilities[0].Severity) } func TestRunAndroidMultiVersion(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Vulnerability affecting both Android 15 and 16 testData := `{ "schema_version": "1.7.5", "id": "ASB-A-222222222", "published": "2026-05-01T00:00:00Z", "modified": "2026-07-14T00:00:00Z", "aliases": ["A-222222222", "CVE-2026-55555"], "affected": [{ "package": { "name": "platform/frameworks/base", "ecosystem": "Android" }, "ranges": [{ "type": "ECOSYSTEM", "events": [ {"introduced": "15:0"}, {"fixed": "15:2026-05-01"}, {"introduced": "16:0"}, {"fixed": "16:2026-05-01"} ] }], "versions": ["15", "16"], "ecosystem_specific": { "severity": "Critical", "spl": "2026-05-01" } }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "ASB-A-222222222.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) // Should produce artifacts for both Android 15 and 16 a15, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-15-2026-07-14.json.gz")) require.NoError(t, err) require.Equal(t, "15", a15.AndroidVersion) require.Equal(t, 1, a15.TotalCVEs) require.Equal(t, "CVE-2026-55555", a15.Vulnerabilities[0].CVE) require.Equal(t, "2026-05-01", a15.Vulnerabilities[0].FixedSPL) a16, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-16-2026-07-14.json.gz")) require.NoError(t, err) require.Equal(t, "16", a16.AndroidVersion) require.Equal(t, 1, a16.TotalCVEs) require.Equal(t, "CVE-2026-55555", a16.Vulnerabilities[0].CVE) } func TestRunAndroidDeduplication(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Same CVE appears in multiple affected entries (different packages, same version) testData := `{ "schema_version": "1.7.5", "id": "ASB-A-333333333", "published": "2026-01-01T00:00:00Z", "modified": "2026-01-02T00:00:00Z", "aliases": ["A-333333333", "CVE-2026-33333"], "affected": [ { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "16:0"}, {"fixed": "16:2026-01-01"}]}], "ecosystem_specific": {"severity": "High"} }, { "package": {"name": "platform/packages/apps/Settings", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "16:0"}, {"fixed": "16:2026-01-01"}]}], "ecosystem_specific": {"severity": "High"} } ] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "ASB-A-333333333.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) artifact, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-16-2026-07-14.json.gz")) require.NoError(t, err) // CVE should appear only once despite two affected entries require.Equal(t, 1, artifact.TotalCVEs) require.Len(t, artifact.Vulnerabilities, 1) require.Equal(t, "CVE-2026-33333", artifact.Vulnerabilities[0].CVE) } func TestRunAndroidSkipsNonVersionPrefixes(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Kernel-only vuln (empty prefix ":0") and SoCVersion should be skipped testData := `{ "schema_version": "1.7.5", "id": "ASB-A-444444444", "published": "2020-09-01T00:00:00Z", "modified": "2026-07-14T00:00:00Z", "aliases": ["A-444444444", "CVE-2020-0404"], "affected": [{ "package": {"name": ":linux_kernel:", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": ":0"}, {"fixed": ":2020-09-05"}]}], "versions": ["Kernel"], "ecosystem_specific": {"severity": "High"} }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "kernel-only.json"), []byte(testData), 0o644)) socData := `{ "schema_version": "1.7.5", "id": "ASB-A-555555555", "published": "2020-09-01T00:00:00Z", "modified": "2026-07-14T00:00:00Z", "aliases": ["A-555555555", "CVE-2020-0505"], "affected": [{ "package": {"name": ":unknown:", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "SoCVersion:0"}, {"fixed": "SoCVersion:2020-09-05"}]}], "versions": ["SoCVersion"], "ecosystem_specific": {"severity": "Critical"} }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "soc-only.json"), []byte(socData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) // No artifacts should be created — both entries are non-version prefixes files, err := filepath.Glob(filepath.Join(outputDir, "osv-android-*.json.gz")) require.NoError(t, err) require.Empty(t, files, "no artifacts should be generated for kernel-only or SoCVersion entries") } func TestRunAndroidVersionFiltering(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Create entries for Android 14, 15, and 16 for i, ver := range []string{"14", "15", "16"} { data := fmt.Sprintf(`{ "schema_version": "1.7.5", "id": "ASB-A-10000000%d", "published": "2026-01-01T00:00:00Z", "modified": "2026-01-02T00:00:00Z", "aliases": ["CVE-2026-000%d%d"], "affected": [{ "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "%s:0"}, {"fixed": "%s:2026-01-01"}]}], "ecosystem_specific": {"severity": "High"} }] }`, i, i, i, ver, ver) require.NoError(t, os.WriteFile(filepath.Join(inputDir, fmt.Sprintf("test-%s.json", ver)), []byte(data), 0o644)) } // Filter to only version 16 cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, Versions: "16", DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) require.FileExists(t, filepath.Join(outputDir, "osv-android-16-2026-07-14.json.gz")) _, err = os.Stat(filepath.Join(outputDir, "osv-android-15-2026-07-14.json.gz")) require.True(t, os.IsNotExist(err)) _, err = os.Stat(filepath.Join(outputDir, "osv-android-14-2026-07-14.json.gz")) require.True(t, os.IsNotExist(err)) } func TestRunAndroidNextSuffixNormalization(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // "15-next" should map to the "15" artifact testData := `{ "schema_version": "1.7.5", "id": "ASB-A-666666666", "published": "2026-01-01T00:00:00Z", "modified": "2026-01-02T00:00:00Z", "aliases": ["CVE-2026-66666"], "affected": [{ "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "15-next:0"}, {"fixed": "15-next:2026-01-01"}]}], "ecosystem_specific": {"severity": "Medium"} }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "test-next.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) // Should produce Android 15 artifact (not "15-next") artifact, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-15-2026-07-14.json.gz")) require.NoError(t, err) require.Equal(t, "15", artifact.AndroidVersion) require.Equal(t, 1, artifact.TotalCVEs) require.Equal(t, "CVE-2026-66666", artifact.Vulnerabilities[0].CVE) require.Equal(t, "2026-01-01", artifact.Vulnerabilities[0].FixedSPL) } func TestRunAndroidLatestFixedSPLWins(t *testing.T) { // When the same CVE appears multiple times for the same Android major version // with different fixed SPL dates, the artifact must keep the LATEST date. // This happens in practice when Google's initial fix is incomplete and a // follow-up patch ships in a later monthly bulletin under the same CVE ID. // The earlier fixed_spl is effectively retracted — a host at that SPL is // still vulnerable. tests := []struct { name string description string input string expectedSPL string }{ { name: "incomplete fix revised months later (same package)", description: "Google patches frameworks/base in June, discovers the fix is incomplete, ships a second fix in September under the same CVE. The June SPL is stale.", input: `{ "schema_version": "1.7.5", "id": "ASB-A-777777777", "published": "2024-06-01T00:00:00Z", "modified": "2024-09-01T00:00:00Z", "aliases": ["CVE-2024-32896"], "affected": [ { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "14:0"}, {"fixed": "14:2024-06-05"}]}], "ecosystem_specific": {"severity": "High"} }, { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "14:0"}, {"fixed": "14:2024-09-01"}]}], "ecosystem_specific": {"severity": "High"} } ] }`, expectedSPL: "2024-09-01", }, { name: "different packages fixed in different bulletins", description: "Same CVE affects two packages. One is fixed in June, the other in September. Host needs both patches, so September wins.", input: `{ "schema_version": "1.7.5", "id": "ASB-A-888888888", "published": "2024-06-01T00:00:00Z", "modified": "2024-09-01T00:00:00Z", "aliases": ["CVE-2024-99999"], "affected": [ { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "14:0"}, {"fixed": "14:2024-06-05"}]}], "ecosystem_specific": {"severity": "High"} }, { "package": {"name": "platform/system/sepolicy", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "14:0"}, {"fixed": "14:2024-09-01"}]}], "ecosystem_specific": {"severity": "High"} } ] }`, expectedSPL: "2024-09-01", }, { name: "later date seen first in file does not regress", description: "Entries appear with September first, June second. Result must still be September — ordering in the JSON must not matter.", input: `{ "schema_version": "1.7.5", "id": "ASB-A-999999999", "published": "2024-06-01T00:00:00Z", "modified": "2024-09-01T00:00:00Z", "aliases": ["CVE-2024-11111"], "affected": [ { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "14:0"}, {"fixed": "14:2024-09-01"}]}], "ecosystem_specific": {"severity": "High"} }, { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "14:0"}, {"fixed": "14:2024-06-05"}]}], "ecosystem_specific": {"severity": "High"} } ] }`, expectedSPL: "2024-09-01", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(inputDir, "test.json"), []byte(tt.input), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) artifact, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-14-2026-07-14.json.gz")) require.NoError(t, err) require.Len(t, artifact.Vulnerabilities, 1, "same CVE must be deduplicated to one entry") require.Equal(t, tt.expectedSPL, artifact.Vulnerabilities[0].FixedSPL, "must keep the latest fixed SPL — earlier dates are stale") }) } } func TestRunAndroidSeverityUpgrade(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Same CVE, same version, different severities across affected entries. // The highest severity (Critical > High) must win. testData := `{ "schema_version": "1.7.5", "id": "ASB-A-152496149", "published": "2020-08-01T00:00:00Z", "modified": "2020-09-01T00:00:00Z", "aliases": ["CVE-2020-0245"], "affected": [ { "package": {"name": "platform/frameworks/av", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "11:0"}, {"fixed": "11:2020-09-01"}]}], "ecosystem_specific": {"severity": "High"} }, { "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "11:0"}, {"fixed": "11:2020-09-01"}]}], "ecosystem_specific": {"severity": "Critical"} } ] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "test.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-15", GeneratedTimestamp: "2026-07-15T00:00:00Z", RunTime: time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) artifact, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-11-2026-07-15.json.gz")) require.NoError(t, err) require.Len(t, artifact.Vulnerabilities, 1) require.Equal(t, "Critical", artifact.Vulnerabilities[0].Severity, "highest severity must win when deduplicating") } func TestRunAndroidGitRangeIgnored(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Real Android entries carry GIT ranges alongside ECOSYSTEM ranges. // Only the ECOSYSTEM range should be used; the GIT range must be ignored. testData := `{ "schema_version": "1.7.5", "id": "ASB-A-111111111", "published": "2026-01-01T00:00:00Z", "modified": "2026-01-02T00:00:00Z", "aliases": ["CVE-2026-11111"], "affected": [{ "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [ { "type": "GIT", "events": [ {"introduced": "0"}, {"fixed": "abc123def456"} ] }, { "type": "ECOSYSTEM", "events": [ {"introduced": "16:0"}, {"fixed": "16:2026-01-01"} ] } ], "ecosystem_specific": {"severity": "High"} }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "test.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-15", GeneratedTimestamp: "2026-07-15T00:00:00Z", RunTime: time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) artifact, err := readAndroidArtifact(filepath.Join(outputDir, "osv-android-16-2026-07-15.json.gz")) require.NoError(t, err) require.Len(t, artifact.Vulnerabilities, 1) require.Equal(t, "CVE-2026-11111", artifact.Vulnerabilities[0].CVE) require.Equal(t, "2026-01-01", artifact.Vulnerabilities[0].FixedSPL) } func TestRunAndroidNonAndroidEcosystemIgnored(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // An entry with a non-Android ecosystem that has version-like fixed events // must not produce an Android artifact. testData := `{ "schema_version": "1.7.5", "id": "FAKE-ENTRY", "published": "2026-01-01T00:00:00Z", "modified": "2026-01-02T00:00:00Z", "aliases": ["CVE-2026-99999"], "affected": [{ "package": {"name": "some-package", "ecosystem": "Ubuntu:24.04:LTS"}, "ranges": [{ "type": "ECOSYSTEM", "events": [{"introduced": "16:0"}, {"fixed": "16:2026-01-01"}] }] }] }` require.NoError(t, os.WriteFile(filepath.Join(inputDir, "test.json"), []byte(testData), 0o644)) cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, DateStr: "2026-07-15", GeneratedTimestamp: "2026-07-15T00:00:00Z", RunTime: time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) files, err := filepath.Glob(filepath.Join(outputDir, "osv-android-*.json.gz")) require.NoError(t, err) require.Empty(t, files, "non-Android ecosystem must not produce any artifact") } func TestRunAndroidExcludeVersions(t *testing.T) { inputDir := t.TempDir() outputDir := t.TempDir() // Create entries for Android 14, 15, and 16 for i, ver := range []string{"14", "15", "16"} { data := fmt.Sprintf(`{ "schema_version": "1.7.5", "id": "ASB-A-20000000%d", "published": "2026-01-01T00:00:00Z", "modified": "2026-01-02T00:00:00Z", "aliases": ["CVE-2026-100%d%d"], "affected": [{ "package": {"name": "platform/frameworks/base", "ecosystem": "Android"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "%s:0"}, {"fixed": "%s:2026-01-01"}]}], "ecosystem_specific": {"severity": "High"} }] }`, i, i, i, ver, ver) require.NoError(t, os.WriteFile(filepath.Join(inputDir, fmt.Sprintf("test-%s.json", ver)), []byte(data), 0o644)) } cfg := Config{ Platform: "android", InputDir: inputDir, OutputDir: outputDir, ExcludeVersions: "14,15", DateStr: "2026-07-15", GeneratedTimestamp: "2026-07-15T00:00:00Z", RunTime: time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.NoError(t, err) // Only Android 16 should be generated require.FileExists(t, filepath.Join(outputDir, "osv-android-16-2026-07-15.json.gz")) _, err = os.Stat(filepath.Join(outputDir, "osv-android-15-2026-07-15.json.gz")) require.True(t, os.IsNotExist(err), "excluded version 15 should not produce an artifact") _, err = os.Stat(filepath.Join(outputDir, "osv-android-14-2026-07-15.json.gz")) require.True(t, os.IsNotExist(err), "excluded version 14 should not produce an artifact") } func TestRunAndroidEmptyInputErrors(t *testing.T) { // Android ingests the full OSV corpus, so an input directory with no usable // Android OSV files means the input is broken. Fail loudly rather than // silently producing a release with no artifacts. cfg := Config{ Platform: "android", InputDir: t.TempDir(), // empty OutputDir: t.TempDir(), DateStr: "2026-07-14", GeneratedTimestamp: "2026-07-14T00:00:00Z", RunTime: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), } err := runAndroid(cfg) require.Error(t, err) require.Contains(t, err.Error(), "no Android OSV files with CVEs found") } func TestRunAndroidDeltaFlagsRejected(t *testing.T) { cfg := Config{ Platform: "android", InputDir: t.TempDir(), OutputDir: t.TempDir(), ChangedFilesToday: "some-file.txt", } err := runAndroid(cfg) require.Error(t, err) require.Contains(t, err.Error(), "not supported with --platform android") }