Skip to content

Commit cbcea34

Browse files
authored
fix(ci): require immutable GitHub Action refs (#111)
2 parents 3b14798 + 342a41d commit cbcea34

3 files changed

Lines changed: 22 additions & 11 deletions

File tree

internal/codeguard/checks/ci/ci_delivery_safety.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
)
1212

1313
var (
14-
workflowUsesPattern = regexp.MustCompile(`(?i)\buses:\s*['"]?([^@\s'"]+)(?:@([^\s#'"]+))?`)
15-
latestImagePattern = regexp.MustCompile(`(?i)\b(?:image:|from)\s+['"]?[^@\s'"]+:latest\b`)
16-
deployImageRunPattern = regexp.MustCompile(`(?i)\b(?:docker|kubectl|helm)\b.*:latest\b`)
14+
workflowUsesPattern = regexp.MustCompile(`(?i)\buses:\s*['"]?([^@\s'"]+)(?:@([^\s#'"]+))?`)
15+
immutableActionRefPattern = regexp.MustCompile(`(?i)^[0-9a-f]{40}$`)
16+
latestImagePattern = regexp.MustCompile(`(?i)\b(?:image:|from)\s+['"]?[^@\s'"]+:latest\b`)
17+
deployImageRunPattern = regexp.MustCompile(`(?i)\b(?:docker|kubectl|helm)\b.*:latest\b`)
1718
)
1819

1920
func missingRequiredGateFindings(env support.Context, target core.TargetConfig) []core.Finding {
@@ -152,12 +153,7 @@ func latestImageFinding(env support.Context, rel string, lineNo int, line string
152153
}
153154

154155
func isMutableActionRef(ref string) bool {
155-
normalized := strings.ToLower(strings.TrimSpace(ref))
156-
switch normalized {
157-
case "", "head", "latest", "main", "master", "develop", "development", "dev", "trunk", "stable":
158-
return true
159-
}
160-
return strings.HasPrefix(normalized, "refs/heads/")
156+
return !immutableActionRefPattern.MatchString(strings.TrimSpace(ref))
161157
}
162158

163159
type ciFile struct {

internal/codeguard/rules/catalog_misc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ var miscCatalog = map[string]core.RuleMetadata{
9292
ExecutionModel: core.RuleExecutionModelLanguageAgnostic,
9393
LanguageCoverage: core.RepositoryWideRuleLanguageCoverage(),
9494
Title: "Mutable deployment reference",
95-
Description: "Fails when deployment automation uses a mutable GitHub Action branch ref or a container image tagged latest.",
95+
Description: "Fails when deployment automation uses a GitHub Action ref other than a full commit SHA or a container image tagged latest.",
9696
HowToFix: "Pin external actions to an immutable reviewed ref and replace latest image tags with versioned tags or digests.",
9797
},
9898
"ci.test-file-location": {

tests/checks/ci_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func TestCIMutableDeploymentReference(t *testing.T) {
111111
assertFindingRulePresent(t, report, "CI/CD", "ci.mutable-deployment-reference")
112112
}
113113

114-
func TestCIMutableDeploymentReferenceAllowsVersionedRefs(t *testing.T) {
114+
func TestCIMutableDeploymentReferenceRejectsVersionedRefs(t *testing.T) {
115115
dir := t.TempDir()
116116
writeFile(t, filepath.Join(dir, ".github", "workflows", "deploy.yml"), "name: deploy\njobs:\n prod:\n steps:\n - uses: actions/checkout@v4\n - run: docker run ghcr.io/acme/service:v1.2.3\n")
117117

@@ -123,6 +123,21 @@ func TestCIMutableDeploymentReferenceAllowsVersionedRefs(t *testing.T) {
123123
t.Fatalf("run: %v", err)
124124
}
125125

126+
assertFindingRulePresent(t, report, "CI/CD", "ci.mutable-deployment-reference")
127+
}
128+
129+
func TestCIMutableDeploymentReferenceAllowsCommitSHA(t *testing.T) {
130+
dir := t.TempDir()
131+
writeFile(t, filepath.Join(dir, ".github", "workflows", "deploy.yml"), "name: deploy\njobs:\n prod:\n steps:\n - uses: actions/checkout@8f3c2b1a4d5e6f7890abc1234567890abc123456\n")
132+
133+
cfg := ciSafetyTestConfig(dir, "ci-immutable-ref")
134+
cfg.Checks.CIRules.RequiredWorkflowFiles = []string{".github/workflows/deploy.yml"}
135+
136+
report, err := codeguard.Run(context.Background(), cfg)
137+
if err != nil {
138+
t.Fatalf("run: %v", err)
139+
}
140+
126141
if messages := ciRuleMessages(report, "ci.mutable-deployment-reference"); len(messages) != 0 {
127142
t.Fatalf("unexpected mutable reference findings: %v", messages)
128143
}

0 commit comments

Comments
 (0)