From 410266a281fe7ac85e2e124036d97b887cf4c76c Mon Sep 17 00:00:00 2001 From: Binan Ibrahim <148448996+BlackEnma@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:45:26 +0300 Subject: [PATCH] Fix CI: bump toolchain and migrate golangci-lint to v2 The CI Lint and Govulncheck jobs were failing because go.mod pinned an outdated go 1.26.1: - golangci-lint v1.62.2 (built with go1.23) refused to run against the go1.26 target. - govulncheck reported 9 stdlib vulns all fixed in go1.26.2-1.26.4. Changes: - go.mod: go 1.26.1 -> 1.26.4 (clears all govulncheck findings). - ci.yml: golangci-lint-action v6 -> v9, lint version v1.62.2 -> v2.12.2. - .golangci.yml: convert to v2 schema; exclude gosec G204/G304 (this CLI intentionally shells out to docker and opens files by computed path). - Resolve real findings surfaced by enabling the linters on existing code: errcheck (unchecked Close/RemoveAll/MkdirAll), revive (package comments), gosec G301 (dir perms 0755 -> 0750). Annotate the already-guarded Zip-Slip Join (G305) and the local docker-save copy (G110) with nolint. Validated locally: gofmt, go vet, go test -race, golangci-lint run (0 issues), and govulncheck (no vulnerabilities). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 4 ++-- .golangci.yml | 12 +++++++++++- cmd/scanner/main.go | 2 ++ go.mod | 2 +- internal/analyzer/analyzer.go | 9 +++++---- internal/extractor/extractor.go | 30 +++++++++++++++++++----------- internal/matcher/matcher.go | 5 +++-- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48a579d..499ca6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,9 +43,9 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: go.mod - - uses: golangci/golangci-lint-action@v6 + - uses: golangci/golangci-lint-action@v9 with: - version: v1.62.2 + version: v2.12.2 vulncheck: name: Govulncheck diff --git a/.golangci.yml b/.golangci.yml index cd63ea8..8147e39 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,6 @@ -# golangci-lint configuration. Mirrors the linters required by CONVENTIONS.md. +# golangci-lint configuration (v2 schema). Mirrors the linters required by CONVENTIONS.md. +version: "2" + run: timeout: 5m @@ -9,6 +11,14 @@ linters: - staticcheck # static analysis - revive # style / lint rules - gosec # security issues + exclusions: + rules: + # This is a CLI that, by design, shells out to the `docker` binary and + # opens files by paths it computes while unpacking image layers. + - linters: [gosec] + text: "G204" # subprocess launched with variable (docker pull/save) + - linters: [gosec] + text: "G304" # file inclusion via variable path (layer/manifest reads) issues: # Don't cap the number of reported issues per linter/file. diff --git a/cmd/scanner/main.go b/cmd/scanner/main.go index 7d42546..3e217f3 100644 --- a/cmd/scanner/main.go +++ b/cmd/scanner/main.go @@ -1,3 +1,5 @@ +// Command scanner is the Sentinel Scanner CLI entry point: it pulls an Alpine +// Docker image, builds its SBOM, and reports matching CVEs from Alpine SecDB. package main import ( diff --git a/go.mod b/go.mod index 1ead4f8..d35640c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module sentinel-scanner -go 1.26.1 +go 1.26.4 diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 82b015c..4d7820e 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -1,4 +1,5 @@ -// internal/analyzer/analyzer.go +// Package analyzer reads unpacked image layers to locate and parse the Alpine +// package database, producing the software bill of materials (SBOM). package analyzer import ( @@ -28,7 +29,7 @@ func GetImageLayers(extractPath string) ([]string, error) { if err != nil { return nil, fmt.Errorf("could not open manifest.json: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() var manifest Manifest if err := json.NewDecoder(file).Decode(&manifest); err != nil { @@ -65,7 +66,7 @@ func BuildSBOM(extractPath string, layers []string, verbose bool) ([]Package, er if err != nil { return fmt.Errorf("failed to open layer %s: %w", layer, err) } - defer file.Close() // Safely closes at the end of this anonymous function + defer func() { _ = file.Close() }() // Safely closes at the end of this anonymous function // We use bufio.Reader so we can "peek" ahead into the file stream without consuming it. br := bufio.NewReader(file) @@ -85,7 +86,7 @@ func BuildSBOM(extractPath string, layers []string, verbose bool) ([]Package, er if err != nil { return fmt.Errorf("failed to create gzip reader: %w", err) } - defer gzr.Close() + defer func() { _ = gzr.Close() }() tr = tar.NewReader(gzr) } else { // Layer is UNCOMPRESSED plain tar. diff --git a/internal/extractor/extractor.go b/internal/extractor/extractor.go index bea238c..7a0cfee 100644 --- a/internal/extractor/extractor.go +++ b/internal/extractor/extractor.go @@ -1,4 +1,5 @@ -// internal/extractor/extractor.go +// Package extractor saves a Docker image via the docker CLI and unpacks its +// layers into a temporary directory for analysis. package extractor import ( @@ -22,7 +23,7 @@ func ExtractImage(imageName string, verbose bool) (string, func(), error) { // Setup our cleanup function to wipe the temp files when we are done cleanup := func() { - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) if verbose { fmt.Println("Cleaned up temporary files.") } @@ -60,7 +61,7 @@ func ExtractImage(imageName string, verbose bool) (string, func(), error) { } // 3. Unpack the tarball - if err := os.Mkdir(extractPath, 0755); err != nil { + if err := os.Mkdir(extractPath, 0750); err != nil { return "", cleanup, fmt.Errorf("failed to create extract dir: %w", err) } @@ -80,7 +81,7 @@ func untar(tarball, targetDir string) error { if err != nil { return err } - defer reader.Close() + defer func() { _ = reader.Close() }() tr := tar.NewReader(reader) @@ -93,29 +94,36 @@ func untar(tarball, targetDir string) error { return err } - // SECURITY BONUS: Prevent "Zip-Slip" vulnerability (Path Traversal) - targetPath := filepath.Join(targetDir, header.Name) + // SECURITY: Prevent "Zip-Slip" vulnerability (Path Traversal). The + // gosec G305 finding on the Join below is mitigated by this prefix check. + targetPath := filepath.Join(targetDir, header.Name) //nolint:gosec // G305 guarded by the prefix check immediately below if !strings.HasPrefix(targetPath, filepath.Clean(targetDir)+string(os.PathSeparator)) { return fmt.Errorf("illegal file path: %s", header.Name) } switch header.Typeflag { case tar.TypeDir: - if err := os.MkdirAll(targetPath, 0755); err != nil { + if err := os.MkdirAll(targetPath, 0750); err != nil { return err } case tar.TypeReg: // Ensure parent directories exist - os.MkdirAll(filepath.Dir(targetPath), 0755) + if err := os.MkdirAll(filepath.Dir(targetPath), 0750); err != nil { + return err + } outFile, err := os.Create(targetPath) if err != nil { return err } - if _, err := io.Copy(outFile, tr); err != nil { - outFile.Close() + // G110: layers come from a local `docker save`; an unbounded copy is + // acceptable here since image layers are legitimately large. + if _, err := io.Copy(outFile, tr); err != nil { //nolint:gosec // G110 decompression bomb: input is local docker save output + _ = outFile.Close() + return err + } + if err := outFile.Close(); err != nil { return err } - outFile.Close() } } return nil diff --git a/internal/matcher/matcher.go b/internal/matcher/matcher.go index 857e0bf..bbce97b 100644 --- a/internal/matcher/matcher.go +++ b/internal/matcher/matcher.go @@ -1,4 +1,5 @@ -// internal/matcher/matcher.go +// Package matcher fetches the Alpine SecDB advisories and matches them against +// the SBOM to report vulnerable packages. package matcher import ( @@ -80,7 +81,7 @@ func fetchSingleSecDB(client *http.Client, url string) (*SecDB, error) { if err != nil { return nil, fmt.Errorf("failed to fetch SecDB: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("SecDB returned non-200 status: %d", resp.StatusCode)