Compare commits
2 Commits
98ea91f2df
...
6919061240
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6919061240 | ||
|
|
7b739e04c8 |
@@ -23,6 +23,8 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Hardened `coverage-gate` file input handling by validating and normalizing policy/profile paths before opening files, resolving `G304` findings in `coverage-gate/parse.go`.
|
||||||
|
|
||||||
## [1.1.0] - 2026-03-21
|
## [1.1.0] - 2026-03-21
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ type PackageResult struct {
|
|||||||
|
|
||||||
// LoadPolicy reads policy JSON from disk.
|
// LoadPolicy reads policy JSON from disk.
|
||||||
func LoadPolicy(path string) (Policy, error) {
|
func LoadPolicy(path string) (Policy, error) {
|
||||||
f, err := os.Open(path)
|
f, err := openValidatedReadOnlyFile(path, ".json", "policy")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Policy{}, fmt.Errorf("open policy: %w", err)
|
return Policy{}, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ func LoadPolicy(path string) (Policy, error) {
|
|||||||
|
|
||||||
// ParseCoverProfile parses a Go coverprofile and aggregates package coverage.
|
// ParseCoverProfile parses a Go coverprofile and aggregates package coverage.
|
||||||
func ParseCoverProfile(profilePath string, policy Policy) (map[string]Coverage, error) {
|
func ParseCoverProfile(profilePath string, policy Policy) (map[string]Coverage, error) {
|
||||||
f, err := os.Open(profilePath)
|
f, err := openValidatedReadOnlyFile(profilePath, "", "coverage profile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open coverage profile: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -121,6 +121,41 @@ func ParseCoverProfile(profilePath string, policy Policy) (map[string]Coverage,
|
|||||||
return coverage, nil
|
return coverage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openValidatedReadOnlyFile(path string, requiredExt string, label string) (*os.File, error) {
|
||||||
|
cleaned := filepath.Clean(strings.TrimSpace(path))
|
||||||
|
if cleaned == "" || cleaned == "." {
|
||||||
|
return nil, fmt.Errorf("invalid %s path", label)
|
||||||
|
}
|
||||||
|
|
||||||
|
if requiredExt != "" {
|
||||||
|
ext := strings.ToLower(filepath.Ext(cleaned))
|
||||||
|
if ext != strings.ToLower(requiredExt) {
|
||||||
|
return nil, fmt.Errorf("invalid %s file extension: got %q, want %q", label, ext, requiredExt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
absPath, err := filepath.Abs(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve %s path: %w", label, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stat %s: %w", label, err)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("%s path must be a file, got directory", label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #nosec G304 -- path is explicitly cleaned, normalized, and pre-validated as an existing file.
|
||||||
|
f, err := os.Open(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open %s: %w", label, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
// EvaluateCoverage evaluates package coverage against policy thresholds.
|
// EvaluateCoverage evaluates package coverage against policy thresholds.
|
||||||
func EvaluateCoverage(packages []string, byPackage map[string]Coverage, policy Policy) []PackageResult {
|
func EvaluateCoverage(packages []string, byPackage map[string]Coverage, policy Policy) []PackageResult {
|
||||||
results := make([]PackageResult, 0, len(packages))
|
results := make([]PackageResult, 0, len(packages))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,3 +87,33 @@ func TestEvaluateCoverage_NoStatementsPasses(t *testing.T) {
|
|||||||
t.Fatalf("expected pass for no-statement package, got %+v", results[0])
|
t.Fatalf("expected pass for no-statement package, got %+v", results[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadPolicy_RejectsNonJSONPath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tmp := t.TempDir()
|
||||||
|
policyPath := filepath.Join(tmp, "policy.yaml")
|
||||||
|
if err := os.WriteFile(policyPath, []byte("minimum_statement_coverage: 80\n"), 0600); err != nil {
|
||||||
|
t.Fatalf("write policy file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := LoadPolicy(policyPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected LoadPolicy to fail for non-json extension")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "invalid policy file extension") {
|
||||||
|
t.Fatalf("expected extension error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCoverProfile_RejectsDirectoryPath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := ParseCoverProfile(t.TempDir(), Policy{MinimumStatementCoverage: 80})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected ParseCoverProfile to fail for directory path")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "coverage profile path must be a file") {
|
||||||
|
t.Fatalf("expected directory path error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user