package security import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) // ── TestDetectOS ───────────────────────────────────────────────────────────── func TestDetectOS_Alpine(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "etc/os-release": `NAME="Alpine Linux" ID=alpine VERSION_ID=3.19.1 PRETTY_NAME="Alpine Linux v3.19" HOME_URL="https://alpinelinux.org/" `, }) name, eco, err := DetectOS(rootfs) if err != nil { t.Fatalf("DetectOS failed: %v", err) } if name != "Alpine Linux v3.19" { t.Errorf("expected 'Alpine Linux v3.19', got %q", name) } if eco != "Alpine" { t.Errorf("expected ecosystem 'Alpine', got %q", eco) } } func TestDetectOS_Debian(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "etc/os-release": `PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" NAME="Debian GNU/Linux" VERSION_ID="12" VERSION="12 (bookworm)" VERSION_CODENAME=bookworm ID=debian `, }) name, eco, err := DetectOS(rootfs) if err != nil { t.Fatalf("DetectOS failed: %v", err) } if name != "Debian GNU/Linux 12 (bookworm)" { t.Errorf("expected 'Debian GNU/Linux 12 (bookworm)', got %q", name) } if eco != "Debian" { t.Errorf("expected ecosystem 'Debian', got %q", eco) } } func TestDetectOS_Ubuntu(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "etc/os-release": `PRETTY_NAME="Ubuntu 24.04.1 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.1 LTS (Noble Numbat)" ID=ubuntu ID_LIKE=debian `, }) name, eco, err := DetectOS(rootfs) if err != nil { t.Fatalf("DetectOS failed: %v", err) } if name != "Ubuntu 24.04.1 LTS" { t.Errorf("expected 'Ubuntu 24.04.1 LTS', got %q", name) } if eco != "Ubuntu" { t.Errorf("expected ecosystem 'Ubuntu', got %q", eco) } } func TestDetectOS_Rocky(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "etc/os-release": `NAME="Rocky Linux" VERSION="9.3 (Blue Onyx)" ID="rocky" VERSION_ID="9.3" PRETTY_NAME="Rocky Linux 9.3 (Blue Onyx)" `, }) name, eco, err := DetectOS(rootfs) if err != nil { t.Fatalf("DetectOS failed: %v", err) } if name != "Rocky Linux 9.3 (Blue Onyx)" { t.Errorf("expected 'Rocky Linux 9.3 (Blue Onyx)', got %q", name) } if eco != "Rocky Linux" { t.Errorf("expected ecosystem 'Rocky Linux', got %q", eco) } } func TestDetectOS_NoFile(t *testing.T) { rootfs := t.TempDir() _, _, err := DetectOS(rootfs) if err == nil { t.Fatal("expected error for missing os-release") } } func TestDetectOS_NoPrettyName(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "etc/os-release": `ID=alpine VERSION_ID=3.19.1 `, }) name, _, err := DetectOS(rootfs) if err != nil { t.Fatalf("DetectOS failed: %v", err) } if name != "alpine 3.19.1" { t.Errorf("expected 'alpine 3.19.1', got %q", name) } } // ── TestListPackagesDpkg ───────────────────────────────────────────────────── func TestListPackagesDpkg(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "var/lib/dpkg/status": `Package: base-files Status: install ok installed Priority: required Section: admin Installed-Size: 338 Maintainer: Santiago Vila Architecture: amd64 Version: 12.4+deb12u5 Description: Debian base system miscellaneous files Package: libc6 Status: install ok installed Priority: optional Section: libs Installed-Size: 13364 Maintainer: GNU Libc Maintainers Architecture: amd64 Multi-Arch: same Version: 2.36-9+deb12u7 Description: GNU C Library: Shared libraries Package: removed-pkg Status: deinstall ok not-installed Priority: optional Section: libs Architecture: amd64 Version: 1.0.0 Description: This should not appear Package: openssl Status: install ok installed Priority: optional Section: utils Installed-Size: 1420 Architecture: amd64 Version: 3.0.11-1~deb12u2 Description: Secure Sockets Layer toolkit `, }) pkgs, err := ListPackages(rootfs) if err != nil { t.Fatalf("ListPackages failed: %v", err) } if len(pkgs) != 3 { t.Fatalf("expected 3 packages, got %d: %+v", len(pkgs), pkgs) } // Check that we got the right packages names := map[string]string{} for _, p := range pkgs { names[p.Name] = p.Version if p.Source != "dpkg" { t.Errorf("expected source 'dpkg', got %q for %s", p.Source, p.Name) } } if names["base-files"] != "12.4+deb12u5" { t.Errorf("wrong version for base-files: %q", names["base-files"]) } if names["libc6"] != "2.36-9+deb12u7" { t.Errorf("wrong version for libc6: %q", names["libc6"]) } if names["openssl"] != "3.0.11-1~deb12u2" { t.Errorf("wrong version for openssl: %q", names["openssl"]) } if _, ok := names["removed-pkg"]; ok { t.Error("removed-pkg should not be listed") } } func TestListPackagesDpkg_NoTrailingNewline(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "var/lib/dpkg/status": `Package: curl Status: install ok installed Version: 7.88.1-10+deb12u5`, }) pkgs, err := ListPackages(rootfs) if err != nil { t.Fatalf("ListPackages failed: %v", err) } if len(pkgs) != 1 { t.Fatalf("expected 1 package, got %d", len(pkgs)) } if pkgs[0].Name != "curl" || pkgs[0].Version != "7.88.1-10+deb12u5" { t.Errorf("unexpected package: %+v", pkgs[0]) } } // ── TestListPackagesApk ────────────────────────────────────────────────────── func TestListPackagesApk(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "lib/apk/db/installed": `C:Q1abc123= P:musl V:1.2.4_git20230717-r4 A:x86_64 S:383152 I:622592 T:the musl c library U:https://musl.libc.org/ L:MIT o:musl m:Natanael Copa t:1700000000 c:abc123 C:Q1def456= P:busybox V:1.36.1-r15 A:x86_64 S:512000 I:924000 T:Size optimized toolbox U:https://busybox.net/ L:GPL-2.0-only o:busybox m:Natanael Copa t:1700000001 c:def456 C:Q1ghi789= P:openssl V:3.1.4-r5 A:x86_64 S:1234567 I:2345678 T:Toolkit for SSL/TLS U:https://www.openssl.org/ L:Apache-2.0 o:openssl m:Natanael Copa t:1700000002 c:ghi789 `, }) pkgs, err := ListPackages(rootfs) if err != nil { t.Fatalf("ListPackages failed: %v", err) } if len(pkgs) != 3 { t.Fatalf("expected 3 packages, got %d: %+v", len(pkgs), pkgs) } names := map[string]string{} for _, p := range pkgs { names[p.Name] = p.Version if p.Source != "apk" { t.Errorf("expected source 'apk', got %q for %s", p.Source, p.Name) } } if names["musl"] != "1.2.4_git20230717-r4" { t.Errorf("wrong version for musl: %q", names["musl"]) } if names["busybox"] != "1.36.1-r15" { t.Errorf("wrong version for busybox: %q", names["busybox"]) } if names["openssl"] != "3.1.4-r5" { t.Errorf("wrong version for openssl: %q", names["openssl"]) } } func TestListPackagesApk_NoTrailingNewline(t *testing.T) { rootfs := createTempRootfs(t, map[string]string{ "lib/apk/db/installed": `P:curl V:8.5.0-r0`, }) pkgs, err := ListPackages(rootfs) if err != nil { t.Fatalf("ListPackages failed: %v", err) } if len(pkgs) != 1 { t.Fatalf("expected 1 package, got %d", len(pkgs)) } if pkgs[0].Name != "curl" || pkgs[0].Version != "8.5.0-r0" { t.Errorf("unexpected package: %+v", pkgs[0]) } } // ── TestListPackages_NoPackageManager ──────────────────────────────────────── func TestListPackages_NoPackageManager(t *testing.T) { rootfs := t.TempDir() _, err := ListPackages(rootfs) if err == nil { t.Fatal("expected error when no package manager found") } if !strings.Contains(err.Error(), "no supported package manager") { t.Errorf("unexpected error: %v", err) } } // ── TestOSVQueryParsing ────────────────────────────────────────────────────── func TestOSVQueryParsing(t *testing.T) { // Recorded OSV response for openssl 3.1.4 on Alpine osvResponse := `{ "vulns": [ { "id": "CVE-2024-0727", "summary": "PKCS12 Decoding crashes", "details": "Processing a maliciously crafted PKCS12 file may lead to OpenSSL crashing.", "severity": [ {"type": "CVSS_V3", "score": "5.5"} ], "affected": [ { "package": {"name": "openssl", "ecosystem": "Alpine"}, "ranges": [ { "type": "ECOSYSTEM", "events": [ {"introduced": "0"}, {"fixed": "3.1.5-r0"} ] } ] } ], "references": [ {"type": "ADVISORY", "url": "https://www.openssl.org/news/secadv/20240125.txt"}, {"type": "WEB", "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0727"} ] }, { "id": "CVE-2024-2511", "summary": "Unbounded memory growth with session handling in TLSv1.3", "severity": [ {"type": "CVSS_V3", "score": "3.7"} ], "affected": [ { "package": {"name": "openssl", "ecosystem": "Alpine"}, "ranges": [ { "type": "ECOSYSTEM", "events": [ {"introduced": "3.1.0"}, {"fixed": "3.1.6-r0"} ] } ] } ], "references": [ {"type": "ADVISORY", "url": "https://www.openssl.org/news/secadv/20240408.txt"} ] } ] }` // Verify our conversion logic var resp osvQueryResponse if err := json.Unmarshal([]byte(osvResponse), &resp); err != nil { t.Fatalf("failed to parse mock OSV response: %v", err) } vulns := convertOSVVulns(resp.Vulns, "openssl", "3.1.4-r5") if len(vulns) != 2 { t.Fatalf("expected 2 vulns, got %d", len(vulns)) } // First vuln: CVE-2024-0727 v1 := vulns[0] if v1.ID != "CVE-2024-0727" { t.Errorf("expected CVE-2024-0727, got %s", v1.ID) } if v1.Package != "openssl" { t.Errorf("expected package 'openssl', got %q", v1.Package) } if v1.Version != "3.1.4-r5" { t.Errorf("expected version '3.1.4-r5', got %q", v1.Version) } if v1.FixedIn != "3.1.5-r0" { t.Errorf("expected fixed in '3.1.5-r0', got %q", v1.FixedIn) } if v1.Severity != "MEDIUM" { t.Errorf("expected severity MEDIUM (CVSS 5.5), got %q", v1.Severity) } if v1.Summary != "PKCS12 Decoding crashes" { t.Errorf("unexpected summary: %q", v1.Summary) } if len(v1.References) != 2 { t.Errorf("expected 2 references, got %d", len(v1.References)) } // Second vuln: CVE-2024-2511 v2 := vulns[1] if v2.ID != "CVE-2024-2511" { t.Errorf("expected CVE-2024-2511, got %s", v2.ID) } if v2.FixedIn != "3.1.6-r0" { t.Errorf("expected fixed in '3.1.6-r0', got %q", v2.FixedIn) } if v2.Severity != "LOW" { t.Errorf("expected severity LOW (CVSS 3.7), got %q", v2.Severity) } } func TestOSVQueryParsing_BatchResponse(t *testing.T) { batchResponse := `{ "results": [ { "vulns": [ { "id": "CVE-2024-0727", "summary": "PKCS12 Decoding crashes", "severity": [{"type": "CVSS_V3", "score": "5.5"}], "affected": [ { "package": {"name": "openssl", "ecosystem": "Alpine"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "3.1.5-r0"}]}] } ], "references": [] } ] }, { "vulns": [] }, { "vulns": [ { "id": "CVE-2024-9681", "summary": "curl: HSTS subdomain overwrites parent cache entry", "severity": [{"type": "CVSS_V3", "score": "6.5"}], "affected": [ { "package": {"name": "curl", "ecosystem": "Alpine"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "8.11.1-r0"}]}] } ], "references": [{"type": "WEB", "url": "https://curl.se/docs/CVE-2024-9681.html"}] } ] } ] }` var resp osvBatchResponse if err := json.Unmarshal([]byte(batchResponse), &resp); err != nil { t.Fatalf("failed to parse batch response: %v", err) } if len(resp.Results) != 3 { t.Fatalf("expected 3 result entries, got %d", len(resp.Results)) } // First result: openssl has vulns vulns0 := convertOSVVulns(resp.Results[0].Vulns, "openssl", "3.1.4") if len(vulns0) != 1 { t.Errorf("expected 1 vuln for openssl, got %d", len(vulns0)) } // Second result: musl has no vulns vulns1 := convertOSVVulns(resp.Results[1].Vulns, "musl", "1.2.4") if len(vulns1) != 0 { t.Errorf("expected 0 vulns for musl, got %d", len(vulns1)) } // Third result: curl has vulns vulns2 := convertOSVVulns(resp.Results[2].Vulns, "curl", "8.5.0") if len(vulns2) != 1 { t.Errorf("expected 1 vuln for curl, got %d", len(vulns2)) } if vulns2[0].FixedIn != "8.11.1-r0" { t.Errorf("expected curl fix 8.11.1-r0, got %q", vulns2[0].FixedIn) } } func TestOSVQueryParsing_DatabaseSpecificSeverity(t *testing.T) { response := `{ "vulns": [ { "id": "DSA-5678-1", "summary": "Some advisory", "database_specific": {"severity": "HIGH"}, "affected": [ { "package": {"name": "libc6", "ecosystem": "Debian"}, "ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "2.36-10"}]}] } ], "references": [] } ] }` var resp osvQueryResponse if err := json.Unmarshal([]byte(response), &resp); err != nil { t.Fatalf("failed to parse: %v", err) } vulns := convertOSVVulns(resp.Vulns, "libc6", "2.36-9") if len(vulns) != 1 { t.Fatalf("expected 1 vuln, got %d", len(vulns)) } if vulns[0].Severity != "HIGH" { t.Errorf("expected HIGH from database_specific, got %q", vulns[0].Severity) } } func TestOSVQueryParsing_DuplicateIDs(t *testing.T) { response := `{ "vulns": [ { "id": "CVE-2024-0727", "summary": "First mention", "affected": [], "references": [] }, { "id": "CVE-2024-0727", "summary": "Duplicate mention", "affected": [], "references": [] } ] }` var resp osvQueryResponse json.Unmarshal([]byte(response), &resp) vulns := convertOSVVulns(resp.Vulns, "openssl", "3.1.4") if len(vulns) != 1 { t.Errorf("expected dedup to 1 vuln, got %d", len(vulns)) } } // ── TestScanReport ─────────────────────────────────────────────────────────── func TestScanReport_Format(t *testing.T) { report := &ScanReport{ Target: "alpine-3.19", OS: "Alpine Linux v3.19", Ecosystem: "Alpine", PackageCount: 42, Vulns: []VulnResult{ { ID: "CVE-2024-0727", Package: "openssl", Version: "3.1.4", FixedIn: "3.1.5", Severity: "CRITICAL", Summary: "PKCS12 crash", }, { ID: "CVE-2024-2511", Package: "openssl", Version: "3.1.4", FixedIn: "3.1.6", Severity: "HIGH", Summary: "TLS memory growth", }, { ID: "CVE-2024-9999", Package: "busybox", Version: "1.36.1", FixedIn: "", Severity: "MEDIUM", Summary: "Buffer overflow", }, }, ScanTime: 1200 * time.Millisecond, } out := FormatReport(report, "") // Check key elements if !strings.Contains(out, "alpine-3.19") { t.Error("report missing target name") } if !strings.Contains(out, "Alpine Linux v3.19") { t.Error("report missing OS name") } if !strings.Contains(out, "42 detected") { t.Error("report missing package count") } if !strings.Contains(out, "CRITICAL") { t.Error("report missing CRITICAL severity") } if !strings.Contains(out, "CVE-2024-0727") { t.Error("report missing CVE ID") } if !strings.Contains(out, "(fixed in 3.1.5)") { t.Error("report missing fixed version") } if !strings.Contains(out, "(no fix available)") { t.Error("report missing 'no fix available' for busybox") } if !strings.Contains(out, "1 critical, 1 high, 1 medium, 0 low (3 total)") { t.Errorf("report summary wrong, got:\n%s", out) } if !strings.Contains(out, "1.2s") { t.Error("report missing scan time") } } func TestScanReport_FormatWithSeverityFilter(t *testing.T) { report := &ScanReport{ Target: "test", OS: "Debian", PackageCount: 10, Vulns: []VulnResult{ {ID: "CVE-1", Severity: "LOW", Package: "pkg1", Version: "1.0"}, {ID: "CVE-2", Severity: "MEDIUM", Package: "pkg2", Version: "2.0"}, {ID: "CVE-3", Severity: "HIGH", Package: "pkg3", Version: "3.0"}, }, ScanTime: 500 * time.Millisecond, } out := FormatReport(report, "high") if strings.Contains(out, "CVE-1") { t.Error("LOW vuln should be filtered out") } if strings.Contains(out, "CVE-2") { t.Error("MEDIUM vuln should be filtered out") } if !strings.Contains(out, "CVE-3") { t.Error("HIGH vuln should be included") } } func TestScanReport_FormatNoVulns(t *testing.T) { report := &ScanReport{ Target: "clean-image", OS: "Alpine", PackageCount: 5, Vulns: nil, ScanTime: 200 * time.Millisecond, } out := FormatReport(report, "") if !strings.Contains(out, "No vulnerabilities found") { t.Error("report should indicate no vulnerabilities") } } func TestScanReport_JSON(t *testing.T) { report := &ScanReport{ Target: "test", OS: "Alpine Linux v3.19", Ecosystem: "Alpine", PackageCount: 3, Vulns: []VulnResult{ { ID: "CVE-2024-0727", Package: "openssl", Version: "3.1.4", FixedIn: "3.1.5", Severity: "MEDIUM", Summary: "PKCS12 crash", References: []string{"https://example.com"}, }, }, ScanTime: 1 * time.Second, } jsonStr, err := FormatReportJSON(report) if err != nil { t.Fatalf("FormatReportJSON failed: %v", err) } // Verify it's valid JSON that round-trips var parsed ScanReport if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil { t.Fatalf("JSON doesn't round-trip: %v", err) } if parsed.Target != "test" { t.Errorf("target mismatch after round-trip: %q", parsed.Target) } if len(parsed.Vulns) != 1 { t.Errorf("expected 1 vuln after round-trip, got %d", len(parsed.Vulns)) } } // ── TestSeverity ───────────────────────────────────────────────────────────── func TestSeverityAtLeast(t *testing.T) { tests := []struct { sev string threshold string expected bool }{ {"CRITICAL", "HIGH", true}, {"HIGH", "HIGH", true}, {"MEDIUM", "HIGH", false}, {"LOW", "MEDIUM", false}, {"CRITICAL", "LOW", true}, {"LOW", "LOW", true}, {"UNKNOWN", "LOW", false}, } for _, tt := range tests { if got := SeverityAtLeast(tt.sev, tt.threshold); got != tt.expected { t.Errorf("SeverityAtLeast(%q, %q) = %v, want %v", tt.sev, tt.threshold, got, tt.expected) } } } func TestCVSSToSeverity(t *testing.T) { tests := []struct { input string expected string }{ {"9.8", "CRITICAL"}, {"9.0", "CRITICAL"}, {"7.5", "HIGH"}, {"7.0", "HIGH"}, {"5.5", "MEDIUM"}, {"4.0", "MEDIUM"}, {"3.7", "LOW"}, {"0.5", "LOW"}, } for _, tt := range tests { if got := cvssToSeverity(tt.input); got != tt.expected { t.Errorf("cvssToSeverity(%q) = %q, want %q", tt.input, got, tt.expected) } } } func TestNormalizeSeverity(t *testing.T) { tests := []struct { input string expected string }{ {"CRITICAL", "CRITICAL"}, {"critical", "CRITICAL"}, {"IMPORTANT", "HIGH"}, {"MODERATE", "MEDIUM"}, {"NEGLIGIBLE", "LOW"}, {"UNIMPORTANT", "LOW"}, {"whatever", "UNKNOWN"}, } for _, tt := range tests { if got := normalizeSeverity(tt.input); got != tt.expected { t.Errorf("normalizeSeverity(%q) = %q, want %q", tt.input, got, tt.expected) } } } // ── TestCountBySeverity ────────────────────────────────────────────────────── func TestCountBySeverity(t *testing.T) { report := &ScanReport{ Vulns: []VulnResult{ {Severity: "CRITICAL"}, {Severity: "CRITICAL"}, {Severity: "HIGH"}, {Severity: "MEDIUM"}, {Severity: "MEDIUM"}, {Severity: "MEDIUM"}, {Severity: "LOW"}, {Severity: "UNKNOWN"}, }, } counts := report.CountBySeverity() if counts.Critical != 2 { t.Errorf("critical: got %d, want 2", counts.Critical) } if counts.High != 1 { t.Errorf("high: got %d, want 1", counts.High) } if counts.Medium != 3 { t.Errorf("medium: got %d, want 3", counts.Medium) } if counts.Low != 1 { t.Errorf("low: got %d, want 1", counts.Low) } if counts.Unknown != 1 { t.Errorf("unknown: got %d, want 1", counts.Unknown) } if counts.Total != 8 { t.Errorf("total: got %d, want 8", counts.Total) } } // ── TestScanRootfs (with mock OSV server) ──────────────────────────────────── func TestScanRootfs_WithMockOSV(t *testing.T) { // Create a mock OSV batch server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/querybatch" { http.Error(w, "not found", 404) return } // Return a canned response: one vuln for openssl, nothing for musl resp := osvBatchResponse{ Results: []osvQueryResponse{ { // openssl result Vulns: []osvVuln{ { ID: "CVE-2024-0727", Summary: "PKCS12 crash", Severity: []struct { Type string `json:"type"` Score string `json:"score"` }{ {Type: "CVSS_V3", Score: "9.8"}, }, Affected: []struct { Package struct { Name string `json:"name"` Ecosystem string `json:"ecosystem"` } `json:"package"` Ranges []struct { Type string `json:"type"` Events []struct { Introduced string `json:"introduced,omitempty"` Fixed string `json:"fixed,omitempty"` } `json:"events"` } `json:"ranges"` }{ { Package: struct { Name string `json:"name"` Ecosystem string `json:"ecosystem"` }{Name: "openssl", Ecosystem: "Alpine"}, Ranges: []struct { Type string `json:"type"` Events []struct { Introduced string `json:"introduced,omitempty"` Fixed string `json:"fixed,omitempty"` } `json:"events"` }{ { Type: "ECOSYSTEM", Events: []struct { Introduced string `json:"introduced,omitempty"` Fixed string `json:"fixed,omitempty"` }{ {Introduced: "0"}, {Fixed: "3.1.5-r0"}, }, }, }, }, }, }, }, }, { // musl result - no vulns Vulns: nil, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) })) defer server.Close() // Patch the batch URL for this test origURL := osvQueryBatchURL // We can't modify the const, so we test via the lower-level functions // Instead, test the integration manually // Create a rootfs with Alpine packages rootfs := createTempRootfs(t, map[string]string{ "etc/os-release": `PRETTY_NAME="Alpine Linux v3.19" ID=alpine VERSION_ID=3.19.1`, "lib/apk/db/installed": `P:openssl V:3.1.4-r5 P:musl V:1.2.4-r4 `, }) // Test DetectOS osName, eco, err := DetectOS(rootfs) if err != nil { t.Fatalf("DetectOS: %v", err) } if osName != "Alpine Linux v3.19" { t.Errorf("OS: got %q", osName) } if eco != "Alpine" { t.Errorf("ecosystem: got %q", eco) } // Test ListPackages pkgs, err := ListPackages(rootfs) if err != nil { t.Fatalf("ListPackages: %v", err) } if len(pkgs) != 2 { t.Fatalf("expected 2 packages, got %d", len(pkgs)) } // Test batch query against mock server using the internal function client := server.Client() _ = origURL // acknowledge to avoid lint vulnMap, err := queryOSVBatchWithURL(client, eco, pkgs, server.URL+"/v1/querybatch") if err != nil { t.Fatalf("queryOSVBatch: %v", err) } // Should have vulns for openssl, not for musl if len(vulnMap) == 0 { t.Fatal("expected some vulnerabilities") } opensslKey := "openssl@3.1.4-r5" if _, ok := vulnMap[opensslKey]; !ok { t.Errorf("expected vulns for %s, keys: %v", opensslKey, mapKeys(vulnMap)) } } // ── TestRpmOutput ──────────────────────────────────────────────────────────── func TestRpmOutputParsing(t *testing.T) { data := []byte("bash\t5.2.15-3.el9\nzlib\t1.2.11-40.el9\nopenssl-libs\t3.0.7-27.el9\n") pkgs, err := parseRpmOutput(data) if err != nil { t.Fatalf("parseRpmOutput: %v", err) } if len(pkgs) != 3 { t.Fatalf("expected 3 packages, got %d", len(pkgs)) } names := map[string]string{} for _, p := range pkgs { names[p.Name] = p.Version if p.Source != "rpm" { t.Errorf("expected source 'rpm', got %q", p.Source) } } if names["bash"] != "5.2.15-3.el9" { t.Errorf("wrong version for bash: %q", names["bash"]) } if names["openssl-libs"] != "3.0.7-27.el9" { t.Errorf("wrong version for openssl-libs: %q", names["openssl-libs"]) } } // ── Helpers ────────────────────────────────────────────────────────────────── // createTempRootfs creates a temporary directory structure mimicking a rootfs. func createTempRootfs(t *testing.T, files map[string]string) string { t.Helper() root := t.TempDir() for relPath, content := range files { fullPath := filepath.Join(root, relPath) if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { t.Fatalf("mkdir %s: %v", filepath.Dir(fullPath), err) } if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { t.Fatalf("write %s: %v", fullPath, err) } } return root } func mapKeys(m map[string][]VulnResult) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys }