Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions cmd/scanner/main.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module sentinel-scanner

go 1.26.1
go 1.26.4
9 changes: 5 additions & 4 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down
30 changes: 19 additions & 11 deletions internal/extractor/extractor.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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.")
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)

Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions internal/matcher/matcher.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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)
Expand Down
Loading