diff --git a/.gitignore b/.gitignore index 2745d3afc..2d66461c9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ _testmain.go /scanner data/cvedb.regular +data/govulndb.zip /task/scannerTask_test1.go diff --git a/Makefile b/Makefile index fac40199d..fb9dd4402 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ REPO ?= neuvector IMAGE = $(REPO)/scanner:$(TAG) BUILD_ACTION = --load -.PHONY: all copy_scan build +.PHONY: all copy_scan build ARCH := $(shell uname -p) @@ -107,12 +107,14 @@ gen_license: copy_scan_slsa: mkdir -p ${STAGE_DIR}/usr/local/bin/ mkdir -p ${STAGE_DIR}/etc/neuvector/db + mkdir -p ${STAGE_DIR}/etc/neuvector/govulndb # cp monitor/monitor ${STAGE_DIR}/usr/local/bin/ cp scanner ${STAGE_DIR}/usr/local/bin/ cp task/scannerTask ${STAGE_DIR}/usr/local/bin/ cp sigstore-interface/sigstore-interface ${STAGE_DIR}/usr/local/bin/sigstore-interface cp data/cvedb.regular ${STAGE_DIR}/etc/neuvector/db/cvedb + cp data/govulndb.zip ${STAGE_DIR}/etc/neuvector/ buildx-machine: docker buildx ls diff --git a/common/govulndb.go b/common/govulndb.go new file mode 100644 index 000000000..4dca7327e --- /dev/null +++ b/common/govulndb.go @@ -0,0 +1,89 @@ +package common + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + scanUtils "github.com/neuvector/neuvector/share/scan" + log "github.com/sirupsen/logrus" +) + +func ExtractGovulnDB() error { + // Check if govulndb directory already exists and is not empty + if info, err := os.Stat(scanUtils.GovulcheckDBPath); err == nil && info.IsDir() { + return nil + } + + zipPath := fmt.Sprintf("%s.zip", scanUtils.GovulcheckDBPath) + + if _, err := os.Stat(zipPath); os.IsNotExist(err) { + log.WithFields(log.Fields{"path": zipPath}).Error("govulndb.zip not found, skipping extraction") + return err + } + + if err := os.RemoveAll(scanUtils.GovulcheckDBPath); err != nil { + return fmt.Errorf("failed to remove existing govulndb directory: %w", err) + } + + if err := os.MkdirAll(scanUtils.GovulcheckDBPath, 0o755); err != nil { + return fmt.Errorf("failed to create govulndb directory: %w", err) + } + + if err := unzipFile(zipPath, scanUtils.GovulcheckDBPath); err != nil { + return fmt.Errorf("failed to extract govulndb.zip: %w", err) + } + + log.WithFields(log.Fields{"target": scanUtils.GovulcheckDBPath}).Info("govulndb extracted successfully") + return nil +} + +func unzipFile(zipPath, targetPath string) error { + reader, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("failed to open zip file: %w", err) + } + defer reader.Close() + + for _, file := range reader.File { + if err := extractZipFile(file, targetPath); err != nil { + return fmt.Errorf("failed to extract %s: %w", file.Name, err) + } + } + + return nil +} + +func extractZipFile(file *zip.File, targetPath string) error { + filePath := filepath.Join(targetPath, file.Name) + + if !strings.HasPrefix(filePath, filepath.Clean(targetPath)+string(os.PathSeparator)) { + return nil + } + + if file.FileInfo().IsDir() { + return os.MkdirAll(filePath, file.Mode()) + } + + if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { + return err + } + + outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) + if err != nil { + return err + } + defer outFile.Close() + + rc, err := file.Open() + if err != nil { + return err + } + defer rc.Close() + + _, err = io.Copy(outFile, rc) + return err +} diff --git a/cvetools/apps.go b/cvetools/apps.go index d97b8f3c8..db0cd834c 100644 --- a/cvetools/apps.go +++ b/cvetools/apps.go @@ -63,6 +63,9 @@ func (cv *ScanTools) DetectAppVul(path string, apps []detectors.AppFeatureVersio func checkForVulns(app detectors.AppFeatureVersion, appIndex int, apps []detectors.AppFeatureVersion, mv []common.AppModuleVul) []vulFullReport { vuls := make([]vulFullReport, 0) for _, v := range mv { + if isGovulncheckFalsePositive(app, v.VulName) { + continue + } if common.Debugs.Enabled { if common.Debugs.CVEs.Contains(v.VulName) && common.Debugs.Features.Contains(app.AppName) { @@ -120,6 +123,25 @@ func checkForVulns(app detectors.AppFeatureVersion, appIndex int, apps []detecto return vuls } +func isGovulncheckFalsePositive(app detectors.AppFeatureVersion, vulName string) bool { + if app.AppName != "golang" { + return false + } + + for _, finding := range app.GovulncheckFindings { + if finding.OSV == vulName { + return false + } + for _, alias := range finding.Aliases { + if alias == vulName { + return false + } + } + } + + return true +} + func appVul2FullVul(app detectors.AppFeatureVersion, mv common.AppModuleVul) vulFullReport { var fv vulFullReport fv.Vf.Name = mv.VulName diff --git a/cvetools/apps_test.go b/cvetools/apps_test.go index 6427a2cbe..ac87b73b8 100644 --- a/cvetools/apps_test.go +++ b/cvetools/apps_test.go @@ -3,8 +3,10 @@ package cvetools import ( "testing" + "github.com/neuvector/neuvector/share/scan" "github.com/neuvector/neuvector/share/utils" "github.com/neuvector/scanner/common" + "github.com/neuvector/scanner/detectors" ) type versionTestCase struct { @@ -67,3 +69,84 @@ func TestFixedVersion(t *testing.T) { } } } + +func TestIsGovulncheckFalsePositive(t *testing.T) { + app := detectors.AppFeatureVersion{ + AppPackage: scan.AppPackage{ + AppName: "golang", + ModuleName: "go:github.com/docker/docker", + Version: "28.5.2+incompatible", + GovulncheckFindings: []scan.GovulnFinding{ + { + OSV: "GO-2026-4883", + Aliases: []string{"CVE-2026-33997", "GHSA-pxq6-2prw-chj9"}, + }, + }, + }, + } + + if isGovulncheckFalsePositive(app, "GO-2026-4883") { + t.Fatal("OSV should not be treated as false positive") + } + if isGovulncheckFalsePositive(app, "CVE-2026-33997") { + t.Fatal("CVE alias should not be treated as false positive") + } + if isGovulncheckFalsePositive(app, "GHSA-pxq6-2prw-chj9") { + t.Fatal("GHSA alias should not be treated as false positive") + } + if !isGovulncheckFalsePositive(app, "CVE-2015-3627") { + t.Fatal("unexpected vuln should be treated as false positive") + } +} + +func TestCheckForVulnsFilteredByGovulncheck(t *testing.T) { + apps := []detectors.AppFeatureVersion{ + { + AppPackage: scan.AppPackage{ + AppName: "golang", + ModuleName: "go:github.com/docker/docker", + Version: "28.5.2+incompatible", + FileName: "usr/bin/example", + GovulncheckFindings: []scan.GovulnFinding{ + { + OSV: "GO-2026-4883", + Aliases: []string{"CVE-2026-33997"}, + }, + }, + }, + }, + } + + dbVuls := []common.AppModuleVul{ + { + VulName: "CVE-2026-33997", + ModuleName: "go:github.com/docker/docker", + Severity: "High", + Description: "kept", + AffectedVer: []common.AppModuleVersion{{OpCode: "", Version: "28.5.2+incompatible"}}, + FixedVer: []common.AppModuleVersion{{OpCode: "", Version: "28.5.3"}}, + }, + { + VulName: "CVE-2015-3627", + ModuleName: "go:github.com/docker/docker", + Severity: "High", + Description: "filtered", + AffectedVer: []common.AppModuleVersion{{OpCode: "", Version: "28.5.2+incompatible"}}, + FixedVer: []common.AppModuleVersion{{OpCode: "", Version: "28.5.3"}}, + }, + } + + got := checkForVulns(apps[0], 0, apps, dbVuls) + if len(got) != 1 { + t.Fatalf("checkForVulns() count = %d, want 1", len(got)) + } + if got[0].Vf.Name != "CVE-2026-33997" { + t.Fatalf("kept vuln = %q, want CVE-2026-33997", got[0].Vf.Name) + } + if len(apps[0].ModuleVuls) != 1 { + t.Fatalf("ModuleVuls count = %d, want 1", len(apps[0].ModuleVuls)) + } + if apps[0].ModuleVuls[0].Name != "CVE-2026-33997" { + t.Fatalf("ModuleVuls[0] = %q, want CVE-2026-33997", apps[0].ModuleVuls[0].Name) + } +} diff --git a/data/govulndb.zip.local b/data/govulndb.zip.local new file mode 100644 index 000000000..15c958a63 Binary files /dev/null and b/data/govulndb.zip.local differ diff --git a/package/Dockerfile b/package/Dockerfile index 46cfc994c..c172d2944 100644 --- a/package/Dockerfile +++ b/package/Dockerfile @@ -1,5 +1,6 @@ ARG VULNDB_VERSION=4.148 ARG VULNDB_CHECKSUM=8781ee7a9c80db7f945d915584be5dcc7bb4e4440e49a3e21b77a6d77da83505 +ARG GOVULNDB_CHECKSUM=be132d3a8f0405e0720e83c7b5921fb96b23b0478fda27761bf13a25198e8736 ARG SIGSTORE_VERSION=426a5b0c9dff026dbc1961c81e232509a7c335cb # # Builder @@ -7,6 +8,7 @@ ARG SIGSTORE_VERSION=426a5b0c9dff026dbc1961c81e232509a7c335cb FROM registry.suse.com/bci/golang:1.26.2 AS builder ARG VERSION ARG VULNDB_CHECKSUM +ARG GOVULNDB_CHECKSUM ARG SIGSTORE_VERSION RUN zypper in -y wget @@ -24,6 +26,7 @@ COPY Makefile go.mod go.sum *.go genlic.sh /src/ WORKDIR /src RUN git clone https://github.com/neuvector/sigstore-interface --single-branch sigstore-interface && cd sigstore-interface && git checkout ${SIGSTORE_VERSION} && make RUN if [ -f "data/cvedb.regular" ]; then echo "using cvedb.regular"; echo "$VULNDB_CHECKSUM data/cvedb.regular" | sha256sum --check --status; else echo "using cvedb"; cp "data/cvedb" "data/cvedb.regular"; fi +RUN if [ -f "data/govulndb.zip" ]; then echo "using govulndb.zip"; echo "$GOVULNDB_CHECKSUM data/govulndb.zip" | sha256sum --check --status; else echo "using govulndb.zip.local"; cp "data/govulndb.zip.local" "data/govulndb.zip"; fi RUN make slsa_all # diff --git a/scanner.go b/scanner.go index c94594182..ec7b64faa 100644 --- a/scanner.go +++ b/scanner.go @@ -193,6 +193,11 @@ func main() { log.SetLevel(log.InfoLevel) log.SetFormatter(&utils.LogFormatter{Module: "SCN"}) + if err := common.ExtractGovulnDB(); err != nil { + log.WithFields(log.Fields{"error": err}).Error("Failed to extract govulndb") + os.Exit(1) + } + dbPath := flag.String("d", "./dbgen/", "cve database file directory") join := flag.String("j", "", "Controller join address") joinPort := flag.Uint("join_port", 0, "Controller join port")