diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bd59e4..86115aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,7 @@ jobs: - name: Post PR comment uses: actions/github-script@v8 with: + retries: 3 script: | const fs = require('fs'); const summary = fs.readFileSync('coverage/coverage.txt', 'utf8'); diff --git a/go.mod b/go.mod index ffca7e7..6e527c0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/distribution/reference v0.6.0 - github.com/go-git/go-git/v5 v5.17.0 + github.com/go-git/go-git/v5 v5.17.2 github.com/mattn/go-runewidth v0.0.21 github.com/muesli/termenv v0.16.0 github.com/schollz/progressbar/v3 v3.19.0 diff --git a/go.sum b/go.sum index abf06dd..d60c4ff 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= -github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= +github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/scan/repo/ignore.go b/internal/scan/repo/ignore.go index 03ddd38..091eaa6 100644 --- a/internal/scan/repo/ignore.go +++ b/internal/scan/repo/ignore.go @@ -1,6 +1,8 @@ package repo import ( + "fmt" + "io" "os" "path/filepath" "strings" @@ -8,6 +10,9 @@ import ( "github.com/go-git/go-git/v5/plumbing/format/gitignore" ) +// maxIgnoreFileSize is the maximum allowed size for a .armisignore file (1 MB). +const maxIgnoreFileSize = 1 << 20 + // IgnoreMatcher matches files against ignore patterns. type IgnoreMatcher struct { patterns []gitignore.Pattern @@ -28,6 +33,9 @@ func LoadIgnorePatterns(repoRoot string) (*IgnoreMatcher, error) { } if !info.IsDir() && info.Name() == ".armisignore" { + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf(".armisignore is a symlink (rejected): %s", path) + } patterns, err := loadIgnoreFile(path, repoRoot) if err != nil { return err @@ -50,10 +58,21 @@ func LoadIgnorePatterns(repoRoot string) (*IgnoreMatcher, error) { } func loadIgnoreFile(ignoreFilePath, repoRoot string) ([]gitignore.Pattern, error) { - data, err := os.ReadFile(ignoreFilePath) // #nosec G304 - ignore file path is constructed internally + f, err := os.Open(ignoreFilePath) // #nosec G304 - ignore file path is constructed internally if err != nil { return nil, err } + defer f.Close() //nolint:errcheck // read-only file + + // Read up to maxIgnoreFileSize+1 to detect files exceeding the limit. + limited := io.LimitReader(f, maxIgnoreFileSize+1) + data, err := io.ReadAll(limited) + if err != nil { + return nil, err + } + if len(data) > maxIgnoreFileSize { + return nil, fmt.Errorf(".armisignore file too large (max %d bytes): %s", maxIgnoreFileSize, ignoreFilePath) + } ignoreDir := filepath.Dir(ignoreFilePath) relDir, err := filepath.Rel(repoRoot, ignoreDir) diff --git a/internal/scan/repo/ignore_test.go b/internal/scan/repo/ignore_test.go index f7ac0fd..298f630 100644 --- a/internal/scan/repo/ignore_test.go +++ b/internal/scan/repo/ignore_test.go @@ -3,6 +3,7 @@ package repo import ( "os" "path/filepath" + "strings" "testing" ) @@ -97,3 +98,49 @@ func TestLoadIgnorePatternsNested(t *testing.T) { t.Error("Expected nested pattern to match") } } + +func TestLoadIgnorePatternsSymlinkRejected(t *testing.T) { + tmpDir := t.TempDir() + + // Create a real file that the symlink will point to. + realFile := filepath.Join(tmpDir, "real-ignore") + if err := os.WriteFile(realFile, []byte("*.log\n"), 0600); err != nil { + t.Fatalf("Failed to create real ignore file: %v", err) + } + + // Create a symlink named .armisignore pointing to the real file. + symlinkPath := filepath.Join(tmpDir, ".armisignore") + if err := os.Symlink(realFile, symlinkPath); err != nil { + t.Skipf("Symlink creation not supported: %v", err) + } + + _, err := LoadIgnorePatterns(tmpDir) + if err == nil { + t.Fatal("expected error for symlinked .armisignore file") + } + if !strings.Contains(err.Error(), "symlink") { + t.Fatalf("expected 'symlink' error, got: %v", err) + } +} + +func TestLoadIgnorePatternsOversizedFile(t *testing.T) { + tmpDir := t.TempDir() + + oversized := make([]byte, maxIgnoreFileSize+1) + for i := range oversized { + oversized[i] = 'a' + } + + ignoreFile := filepath.Join(tmpDir, ".armisignore") + if err := os.WriteFile(ignoreFile, oversized, 0600); err != nil { + t.Fatalf("Failed to create oversized ignore file: %v", err) + } + + _, err := LoadIgnorePatterns(tmpDir) + if err == nil { + t.Fatal("expected error for oversized .armisignore file") + } + if !strings.Contains(err.Error(), "too large") { + t.Fatalf("expected 'too large' error, got: %v", err) + } +}