Files
Juan FernandezandKonstantin Sykulev 621ede7584 Software > OS: Show Android versions and vulnerabilities (#49399)
Resolves #35075 

- Added vulnerability (CVE) reporting for Android OS versions.
- Folded the Android security patch level into the host's OS version.

---------

Co-authored-by: Konstantin Sykulev <konst@sykulev.com>
2026-07-16 10:37:57 -04:00

774 lines
24 KiB
Go

package osv
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)
func TestRemoveOldOSVArtifacts(t *testing.T) {
// Create a temporary directory for testing
tmpDir := t.TempDir()
today := time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC)
// Create some test files
currentFile := filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-30.json.gz")
oldFile := filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-29.json.gz")
otherFile := filepath.Join(tmpDir, "some-other-file.json")
for _, file := range []string{currentFile, oldFile, otherFile} {
err := os.WriteFile(file, []byte("test"), 0o644)
require.NoError(t, err)
}
// Create a directory that matches the OSV pattern
osvDir := filepath.Join(tmpDir, "osv-ubuntu-test.json.gz")
err := os.Mkdir(osvDir, 0o755)
require.NoError(t, err)
// Run the cleanup with 2204 as successfully downloaded (should remove old file but keep current)
err = removeOldOSVArtifacts(today, tmpDir, []string{"2204"})
require.NoError(t, err)
// Check that old file was removed
_, err = os.Stat(oldFile)
require.True(t, os.IsNotExist(err))
// Check that current file still exists
_, err = os.Stat(currentFile)
require.NoError(t, err)
// Check that other file still exists (should not be touched)
_, err = os.Stat(otherFile)
require.NoError(t, err)
// Check that directory still exists (should be skipped, not removed)
stat, err := os.Stat(osvDir)
require.NoError(t, err)
require.True(t, stat.IsDir())
}
func TestRemoveOldOSVArtifactsPreservesFailedVersions(t *testing.T) {
tmpDir := t.TempDir()
today := time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC)
// Create artifacts for two versions, both old and new
files := []string{
"osv-ubuntu-2204-2026-03-30.json.gz", // Today's 22.04 (just downloaded)
"osv-ubuntu-2204-2026-03-29.json.gz", // Yesterday's 22.04 (should be removed)
"osv-ubuntu-2404-2026-03-29.json.gz", // Yesterday's 24.04 (should be KEPT - download failed)
}
for _, file := range files {
err := os.WriteFile(filepath.Join(tmpDir, file), []byte("test"), 0o644)
require.NoError(t, err)
}
// Only 2204 downloaded successfully, 2404 failed
err := removeOldOSVArtifacts(today, tmpDir, []string{"2204"})
require.NoError(t, err)
// Today's 2204 should still exist
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-30.json.gz"))
require.NoError(t, err)
// Old 2204 should be removed (new one downloaded)
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-29.json.gz"))
require.True(t, os.IsNotExist(err))
// Old 2404 should be PRESERVED (download failed, need last-known-good)
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2404-2026-03-29.json.gz"))
require.NoError(t, err, "old 2404 artifact should be preserved when download fails")
}
func TestRemoveOldOSVArtifactsWithSkippedVersions(t *testing.T) {
tmpDir := t.TempDir()
today := time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC)
// Artifact for 2204 already exists with correct checksum (will be skipped)
files := []string{
"osv-ubuntu-2204-2026-03-30.json.gz", // Today's 22.04 (already exists, will be skipped)
"osv-ubuntu-2204-2026-03-29.json.gz", // Yesterday's 22.04 (should be REMOVED even though skipped)
"osv-ubuntu-2204-2026-03-28.json.gz", // Day before yesterday's 22.04 (should be REMOVED)
}
for _, file := range files {
err := os.WriteFile(filepath.Join(tmpDir, file), []byte("test"), 0o644)
require.NoError(t, err)
}
err := removeOldOSVArtifacts(today, tmpDir, []string{"2204"})
require.NoError(t, err)
// Today's 2204 should still exist
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-30.json.gz"))
require.NoError(t, err, "current artifact should be preserved")
// Old 2204 files should be REMOVED (this is the bug fix!)
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-29.json.gz"))
require.True(t, os.IsNotExist(err), "old artifact from yesterday should be removed even when version was skipped")
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-03-28.json.gz"))
require.True(t, os.IsNotExist(err), "old artifact from day before should be removed even when version was skipped")
}
func TestRemoveOldOSVArtifactsDateBoundaryRace(t *testing.T) {
tmpDir := t.TempDir()
// now is April 10 but the release only created April 9 artifacts.
today := time.Date(2026, 4, 10, 0, 5, 0, 0, time.UTC)
files := []string{
"osv-ubuntu-2404-2026-04-09.json.gz", // Yesterday's artifact (only one available)
}
for _, file := range files {
err := os.WriteFile(filepath.Join(tmpDir, file), []byte("test"), 0o644)
require.NoError(t, err)
}
// 2404 is in NotInRelease, not Skipped
// so removeOldOSVArtifacts should not touch it
err := removeOldOSVArtifacts(today, tmpDir, []string{})
require.NoError(t, err)
// Yesterday's artifact must still exist since the version wasn't in the successful set
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2404-2026-04-09.json.gz"))
require.NoError(t, err, "old artifact must be preserved when version is not in release")
}
func TestGetNeededUbuntuVersions(t *testing.T) {
tests := []struct {
name string
osVers *fleet.OSVersions
expected []string
}{
{
name: "multiple ubuntu versions",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "ubuntu", Version: "22.04.8 LTS"},
{Platform: "ubuntu", Version: "20.04.1 LTS"},
},
},
expected: []string{"2204", "2004"},
},
{
name: "duplicate ubuntu versions",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "ubuntu", Version: "22.04.8 LTS"},
{Platform: "ubuntu", Version: "22.04.3 LTS"},
{Platform: "ubuntu", Version: "22.04.1 LTS"},
},
},
expected: []string{"2204"},
},
{
name: "non-ubuntu platforms ignored",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "ubuntu", Version: "22.04.8 LTS"},
{Platform: "rhel", Version: "8.5"},
{Platform: "windows", Version: "10.0.19041"},
},
},
expected: []string{"2204"},
},
{
name: "no ubuntu platforms",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "rhel", Version: "8.5"},
{Platform: "windows", Version: "10.0.19041"},
},
},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getNeededUbuntuVersions(tt.osVers)
require.ElementsMatch(t, tt.expected, result)
})
}
}
func TestGetNeededRHELVersions(t *testing.T) {
tests := []struct {
name string
osVers *fleet.OSVersions
expected []string
}{
{
name: "multiple RHEL versions",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "rhel", Name: "Red Hat Enterprise Linux 8.10.0", Version: "8.10.0"},
{Platform: "rhel", Name: "Red Hat Enterprise Linux 9.4.0", Version: "9.4.0"},
},
},
expected: []string{"8", "9"},
},
{
name: "duplicate major versions deduplicated",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "rhel", Name: "Red Hat Enterprise Linux 9.2.0", Version: "9.2.0"},
{Platform: "rhel", Name: "Red Hat Enterprise Linux 9.4.0", Version: "9.4.0"},
},
},
expected: []string{"9"},
},
{
name: "Fedora skipped",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "rhel", Name: "Red Hat Enterprise Linux 9.4.0", Version: "9.4.0"},
{Platform: "rhel", Name: "Fedora Linux 36.0.0", Version: "36.0.0"},
},
},
expected: []string{"9"},
},
{
name: "non-RHEL platforms ignored",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "rhel", Name: "Red Hat Enterprise Linux 9.4.0", Version: "9.4.0"},
{Platform: "ubuntu", Name: "Ubuntu 22.04.8 LTS", Version: "22.04.8 LTS"},
{Platform: "windows", Name: "Windows 10", Version: "10.0.19041"},
},
},
expected: []string{"9"},
},
{
name: "no RHEL platforms",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "ubuntu", Name: "Ubuntu 22.04.8 LTS", Version: "22.04.8 LTS"},
},
},
expected: []string{},
},
{
name: "only Fedora",
osVers: &fleet.OSVersions{
OSVersions: []fleet.OSVersion{
{Platform: "rhel", Name: "Fedora Linux 36.0.0", Version: "36.0.0"},
},
},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getNeededRHELVersions(tt.osVers)
require.ElementsMatch(t, tt.expected, result)
})
}
}
func TestRemoveOldRHELOSVArtifacts(t *testing.T) {
tmpDir := t.TempDir()
today := time.Date(2026, 4, 8, 0, 0, 0, 0, time.UTC)
files := []string{
"osv-rhel-9-2026-04-08.json.gz", // today — keep
"osv-rhel-9-2026-04-07.json.gz", // yesterday — remove
"osv-rhel-8-2026-04-07.json.gz", // yesterday, different version, not in successful — keep
"osv-ubuntu-2204-2026-04-07.json.gz", // ubuntu — not touched
"some-other-file.json", // unrelated — not touched
}
for _, file := range files {
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, file), []byte("test"), 0o644))
}
err := removeOldRHELOSVArtifacts(today, tmpDir, []string{"9"})
require.NoError(t, err)
// Today's RHEL 9 — kept
_, err = os.Stat(filepath.Join(tmpDir, "osv-rhel-9-2026-04-08.json.gz"))
require.NoError(t, err)
// Yesterday's RHEL 9 — removed (successfully downloaded today)
_, err = os.Stat(filepath.Join(tmpDir, "osv-rhel-9-2026-04-07.json.gz"))
require.True(t, os.IsNotExist(err))
// Yesterday's RHEL 8 — kept (not in successful list, last-known-good)
_, err = os.Stat(filepath.Join(tmpDir, "osv-rhel-8-2026-04-07.json.gz"))
require.NoError(t, err)
// Ubuntu artifact — not touched
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-04-07.json.gz"))
require.NoError(t, err)
// Other file — not touched
_, err = os.Stat(filepath.Join(tmpDir, "some-other-file.json"))
require.NoError(t, err)
}
func TestRHELOSVFilename(t *testing.T) {
date := time.Date(2026, 4, 8, 0, 0, 0, 0, time.UTC)
tests := []struct {
version string
expected string
}{
{"9", "osv-rhel-9-2026-04-08.json.gz"},
{"8", "osv-rhel-8-2026-04-08.json.gz"},
{"10", "osv-rhel-10-2026-04-08.json.gz"},
}
for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
require.Equal(t, tt.expected, rhelOSVFilename(tt.version, date))
})
}
}
func TestOSVFilename(t *testing.T) {
date := time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC)
tests := []struct {
ubuntuVersion string
expected string
}{
{"2204", "osv-ubuntu-2204-2026-03-30.json.gz"},
{"2004", "osv-ubuntu-2004-2026-03-30.json.gz"},
{"1804", "osv-ubuntu-1804-2026-03-30.json.gz"},
}
for _, tt := range tests {
t.Run(tt.ubuntuVersion, func(t *testing.T) {
result := osvFilename(tt.ubuntuVersion, date)
require.Equal(t, tt.expected, result)
})
}
}
func TestComputeFileSHA256(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
// Write test content
testContent := []byte("test content")
err := os.WriteFile(testFile, testContent, 0o644)
require.NoError(t, err)
// Compute SHA256
digest, err := computeFileSHA256(testFile)
require.NoError(t, err)
// Expected digest for "test content"
// echo -n "test content" | sha256sum
// 6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72
expected := "sha256:6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72"
require.Equal(t, expected, digest)
_, err = computeFileSHA256(filepath.Join(tmpDir, "nonexistent.txt"))
require.Error(t, err)
}
func TestSyncOSVFaultTolerance(t *testing.T) {
tmpDir := t.TempDir()
date := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
// Create a mock release with only some artifacts available
release := &ReleaseInfo{
TagName: "cve-202604010000",
Assets: map[string]*AssetInfo{
"osv-ubuntu-2204-2026-04-01.json.gz": {
Name: "osv-ubuntu-2204-2026-04-01.json.gz",
ID: 12345,
Digest: "sha256:abc123",
},
},
}
versions := []string{"2204", "2504"}
// Mock download function that always fails
mockDownload := func(ctx context.Context, assetID int64, dstPath string) error {
return errors.New("mock download failure")
}
result, err := syncOSVWithDownloader(context.Background(), tmpDir, versions, date, release, mockDownload, osvFilename)
require.Error(t, err)
require.NotNil(t, result)
require.Contains(t, result.NotInRelease, "2504", "2504 artifact not in release")
require.Contains(t, result.Failed, "2204", "2204 download failed, should be in Failed")
}
func TestSyncOSVChecksumMatch(t *testing.T) {
tmpDir := t.TempDir()
date := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
// Create a test file with known content
testContent := []byte("test content")
filename := "osv-ubuntu-2204-2026-04-01.json.gz"
testFile := filepath.Join(tmpDir, filename)
err := os.WriteFile(testFile, testContent, 0o644)
require.NoError(t, err)
// Compute the digest
digest, err := computeFileSHA256(testFile)
require.NoError(t, err)
// Create a mock release with matching digest
release := &ReleaseInfo{
TagName: "cve-202604010000",
Assets: map[string]*AssetInfo{
filename: {
Name: filename,
ID: 12345,
Digest: digest,
},
},
}
result, err := SyncOSV(context.Background(), tmpDir, []string{"2204"}, date, release)
require.NoError(t, err)
require.NotNil(t, result)
require.Contains(t, result.Skipped, "2204")
require.Empty(t, result.Downloaded)
require.Empty(t, result.Failed)
}
func TestSyncOSVPartialFailureNotReturnedAsError(t *testing.T) {
// Documents the behavior RefreshAll guards against: SyncOSV reports per-
// version failures via SyncResult.Failed but does NOT return an error when
// some downloads succeed.
tmpDir := t.TempDir()
date := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
good := "osv-ubuntu-2204-2026-04-01.json.gz"
bad := "osv-ubuntu-2404-2026-04-01.json.gz"
release := &ReleaseInfo{
TagName: "cve-202604010000",
Assets: map[string]*AssetInfo{
good: {Name: good, ID: 1},
bad: {Name: bad, ID: 2},
},
}
mockDownload := func(ctx context.Context, assetID int64, dstPath string) error {
if assetID == 2 {
return errors.New("simulated download failure")
}
return os.WriteFile(dstPath, []byte("ok"), 0o644)
}
result, err := syncOSVWithDownloader(context.Background(), tmpDir, []string{"2204", "2404"}, date, release, mockDownload, osvFilename)
require.NoError(t, err, "SyncOSV does not return error on partial failure")
require.Contains(t, result.Downloaded, "2204")
require.Contains(t, result.Failed, "2404")
}
// TestIsOSVReleaseAsset guards getLatestRelease's asset filter. Android assets
// were silently dropped because the filter only matched Ubuntu and RHEL
// prefixes, so RefreshAndroid could never find an asset to download.
func TestIsOSVReleaseAsset(t *testing.T) {
tests := []struct {
name string
want bool
}{
{"osv-ubuntu-2204-2026-07-14.json.gz", true},
{"osv-rhel-9-2026-07-14.json.gz", true},
{"osv-android-16-2026-07-14.json.gz", true},
{"osv-android-8.1-2026-07-14.json.gz", true},
// delta artifacts are excluded.
{"osv-ubuntu-2204-delta-2026-07-14.json.gz", false},
{"osv-android-16-delta-2026-07-14.json.gz", false},
// unrelated assets are excluded.
{"osv-2026-07-14.json.gz", false},
{"some-other-file.json.gz", false},
{"", false},
}
for _, tt := range tests {
require.Equalf(t, tt.want, isOSVReleaseAsset(tt.name), "isOSVReleaseAsset(%q)", tt.name)
}
}
func TestVersionsFromRelease(t *testing.T) {
release := &ReleaseInfo{
TagName: "cve-202604270000",
Assets: map[string]*AssetInfo{
"osv-ubuntu-2204-2026-04-27.json.gz": {Name: "osv-ubuntu-2204-2026-04-27.json.gz"},
"osv-ubuntu-2404-2026-04-27.json.gz": {Name: "osv-ubuntu-2404-2026-04-27.json.gz"},
"osv-rhel-8-2026-04-27.json.gz": {Name: "osv-rhel-8-2026-04-27.json.gz"},
"osv-rhel-9-2026-04-27.json.gz": {Name: "osv-rhel-9-2026-04-27.json.gz"},
"osv-android-15-2026-04-27.json.gz": {Name: "osv-android-15-2026-04-27.json.gz"},
"osv-android-16-2026-04-27.json.gz": {Name: "osv-android-16-2026-04-27.json.gz"},
},
}
ubuntu, rhel, android := versionsFromRelease(release)
require.ElementsMatch(t, []string{"2204", "2404"}, ubuntu)
require.ElementsMatch(t, []string{"8", "9"}, rhel)
require.ElementsMatch(t, []string{"15", "16"}, android)
}
func TestVersionsFromReleaseEmpty(t *testing.T) {
release := &ReleaseInfo{TagName: "cve-202604270000", Assets: map[string]*AssetInfo{}}
ubuntu, rhel, android := versionsFromRelease(release)
require.Empty(t, ubuntu)
require.Empty(t, rhel)
require.Empty(t, android)
}
func TestVersionFromAssetName(t *testing.T) {
tests := []struct {
name string
prefix string
expected string
}{
{"osv-ubuntu-2204-2026-04-27.json.gz", OSVFilePrefix, "2204"},
{"osv-rhel-9-2026-04-27.json.gz", OSVRHELFilePrefix, "9"},
{"osv-rhel-10-2026-04-27.json.gz", OSVRHELFilePrefix, "10"},
{"osv-ubuntu-.json.gz", OSVFilePrefix, ""},
{"osv-ubuntu", OSVFilePrefix, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, versionFromAssetName(tt.name, tt.prefix))
})
}
}
func TestReleaseDateFromAssets(t *testing.T) {
release := &ReleaseInfo{
Assets: map[string]*AssetInfo{
"osv-ubuntu-2204-2026-04-27.json.gz": {Name: "osv-ubuntu-2204-2026-04-27.json.gz"},
},
}
d, ok := releaseDateFromAssets(release)
require.True(t, ok)
require.Equal(t, time.Date(2026, 4, 27, 0, 0, 0, 0, time.UTC), d)
emptyRelease := &ReleaseInfo{Assets: map[string]*AssetInfo{}}
_, ok = releaseDateFromAssets(emptyRelease)
require.False(t, ok)
}
func TestRemoveOldAndroidOSVArtifacts(t *testing.T) {
tmpDir := t.TempDir()
today := time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC)
files := []string{
"osv-android-16-2026-07-15.json.gz", // today — keep
"osv-android-16-2026-07-14.json.gz", // yesterday — remove
"osv-android-15-2026-07-14.json.gz", // yesterday, different version, not in successful — keep
"osv-rhel-9-2026-07-14.json.gz", // rhel — not touched
"osv-ubuntu-2204-2026-07-14.json.gz", // ubuntu — not touched
"some-other-file.json", // unrelated — not touched
}
for _, file := range files {
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, file), []byte("test"), 0o644))
}
err := removeOldAndroidOSVArtifacts(today, tmpDir, []string{"16"})
require.NoError(t, err)
// Today's Android 16 — kept
_, err = os.Stat(filepath.Join(tmpDir, "osv-android-16-2026-07-15.json.gz"))
require.NoError(t, err)
// Yesterday's Android 16 — removed (successfully downloaded today)
_, err = os.Stat(filepath.Join(tmpDir, "osv-android-16-2026-07-14.json.gz"))
require.True(t, os.IsNotExist(err))
// Yesterday's Android 15 — kept (not in successful list)
_, err = os.Stat(filepath.Join(tmpDir, "osv-android-15-2026-07-14.json.gz"))
require.NoError(t, err)
// RHEL artifact — not touched
_, err = os.Stat(filepath.Join(tmpDir, "osv-rhel-9-2026-07-14.json.gz"))
require.NoError(t, err)
// Ubuntu artifact — not touched
_, err = os.Stat(filepath.Join(tmpDir, "osv-ubuntu-2204-2026-07-14.json.gz"))
require.NoError(t, err)
// Other file — not touched
_, err = os.Stat(filepath.Join(tmpDir, "some-other-file.json"))
require.NoError(t, err)
}
// TestRefreshAndroidUsesReleaseDate guards the regression where RefreshAndroid
// built artifact filenames from the cron execution time instead of the release
// date. When the two differ (the cron runs on any day after the release was
// cut), filenames derived from "now" don't match the release assets, so nothing
// downloads — and the cleanup would delete a release-dated artifact that was
// downloaded. This exercises the sync + cleanup path RefreshAndroid delegates to.
func TestRefreshAndroidUsesReleaseDate(t *testing.T) {
releaseDate := time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC)
now := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC)
assetName := androidOSVFilename("16", releaseDate)
release := &ReleaseInfo{
TagName: "cve-202607140000",
Assets: map[string]*AssetInfo{
assetName: {Name: assetName, ID: 1},
},
}
mockDownload := func(ctx context.Context, assetID int64, dstPath string) error {
return os.WriteFile(dstPath, []byte("ok"), 0o644)
}
// Using the release date finds the asset and downloads it.
dir := t.TempDir()
result, err := syncOSVWithDownloader(context.Background(), dir, []string{"16"}, releaseDate, release, mockDownload, androidOSVFilename)
require.NoError(t, err)
require.Contains(t, result.Downloaded, "16")
require.Empty(t, result.NotInRelease)
// Using "now" (different from the release date) misses the asset entirely.
nowDir := t.TempDir()
nowResult, err := syncOSVWithDownloader(context.Background(), nowDir, []string{"16"}, now, release, mockDownload, androidOSVFilename)
require.NoError(t, err)
require.Contains(t, nowResult.NotInRelease, "16")
require.Empty(t, nowResult.Downloaded)
// Cleanup with the release date preserves the just-downloaded artifact...
require.NoError(t, removeOldAndroidOSVArtifacts(releaseDate, dir, []string{"16"}))
_, err = os.Stat(filepath.Join(dir, assetName))
require.NoError(t, err, "release-dated artifact must be preserved")
// ...whereas cleaning up with "now" would delete it, since its date suffix
// doesn't match and version 16 is in the successful set.
require.NoError(t, removeOldAndroidOSVArtifacts(now, dir, []string{"16"}))
_, err = os.Stat(filepath.Join(dir, assetName))
require.True(t, os.IsNotExist(err), "cleanup keyed on now wrongly deletes the release-dated artifact")
}
func TestGetNeededAndroidVersions(t *testing.T) {
tests := []struct {
name string
oses []fleet.OperatingSystem
expected []string
}{
{
name: "empty",
oses: nil,
expected: nil,
},
{
name: "non-android ignored",
oses: []fleet.OperatingSystem{
{Name: "Ubuntu", Version: "22.04", Platform: "ubuntu"},
{Name: "Windows", Version: "10.0.19042", Platform: "windows"},
},
expected: nil,
},
{
name: "bare versions",
oses: []fleet.OperatingSystem{
{Name: "Android", Version: "16", Platform: "android"},
{Name: "Android", Version: "14", Platform: "android"},
},
expected: []string{"16", "14"},
},
{
name: "versions with SPL deduplicated",
oses: []fleet.OperatingSystem{
{Name: "Android", Version: "16 (2026-01-01)", Platform: "android"},
{Name: "Android", Version: "16 (2026-05-01)", Platform: "android"},
{Name: "Android", Version: "14 (2025-03-01)", Platform: "android"},
},
expected: []string{"16", "14"},
},
{
name: "empty version skipped",
oses: []fleet.OperatingSystem{
{Name: "Android", Version: "", Platform: "android"},
{Name: "Android", Version: "16", Platform: "android"},
},
expected: []string{"16"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getNeededAndroidVersions(tt.oses)
require.ElementsMatch(t, tt.expected, result)
})
}
}
func TestAndroidOSVFilename(t *testing.T) {
date := time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC)
tests := []struct {
version string
expected string
}{
{"16", "osv-android-16-2026-07-15.json.gz"},
{"14", "osv-android-14-2026-07-15.json.gz"},
{"8.1", "osv-android-8.1-2026-07-15.json.gz"},
{"12L", "osv-android-12L-2026-07-15.json.gz"},
}
for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
require.Equal(t, tt.expected, androidOSVFilename(tt.version, date))
})
}
}
func TestExtractAndroidMajorVersion(t *testing.T) {
tests := []struct {
version string
expected string
}{
{"16 (2026-05-01)", "16"},
{"14", "14"},
{"8.1 (2021-01-01)", "8.1"},
{"12L (2022-12-01)", "12L"},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
require.Equal(t, tt.expected, extractAndroidMajorVersion(tt.version))
})
}
}
func TestDateFromAssetName(t *testing.T) {
tests := []struct {
name string
expected time.Time
ok bool
}{
{"osv-ubuntu-2204-2026-04-27.json.gz", time.Date(2026, 4, 27, 0, 0, 0, 0, time.UTC), true},
{"osv-rhel-9-2026-04-08.json.gz", time.Date(2026, 4, 8, 0, 0, 0, 0, time.UTC), true},
{"osv-ubuntu-2204.json.gz", time.Time{}, false},
{"some-other-file.json", time.Time{}, false},
{"short", time.Time{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d, ok := dateFromAssetName(tt.name)
require.Equal(t, tt.ok, ok)
require.Equal(t, tt.expected, d)
})
}
}