Skip to content

Commit 7289ca5

Browse files
committed
fix: complete GitHub workflow security hardening and deployment fixes
Security Improvements: - Remove all workflow-level permissions from 16 workflow files - Implement job-level least privilege permissions throughout - Add security documentation comments to all workflows - Fix GitHub Pages deployment to use GH_PAT_TOKEN with GITHUB_TOKEN fallback Deployment Fixes: - Update fortress-coverage.yml to prefer GH_PAT_TOKEN for git operations - Maintain backward compatibility with GITHUB_TOKEN - Fix 403 permission errors in GitHub Pages deployment Files Modified: - All fortress-*.yml workflows: Remove workflow-level permissions - auto-merge-on-approval.yml: Security hardening - dependabot-auto-merge.yml: Security hardening - pull-request-management.yml: Security hardening - scorecard.yml: Security hardening - stale-check.yml: Security hardening - sync-labels.yml: Security hardening - update-pre-commit-hooks.yml: Security hardening - codeql-analysis.yml: Security hardening - ossar.yml: Security hardening This addresses all 14 OpenSSF Scorecard token permission alerts while maintaining full CI/CD functionality and improving GitHub Pages deployment.
1 parent d235298 commit 7289ca5

19 files changed

Lines changed: 78 additions & 68 deletions

.github/coverage/cmd/gofortress-coverage/cmd/complete.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import (
2424
// ErrCoverageBelowThreshold indicates that coverage percentage is below the configured threshold
2525
var ErrCoverageBelowThreshold = errors.New("coverage is below threshold")
2626

27+
// ErrEmptyIndexHTML indicates that the generated index.html file is empty
28+
var ErrEmptyIndexHTML = errors.New("generated index.html is empty")
29+
2730
var completeCmd = &cobra.Command{ //nolint:gochecknoglobals // CLI command
2831
Use: "complete",
2932
Short: "Run complete coverage pipeline",
@@ -278,7 +281,7 @@ update history, and create GitHub PR comment if in PR context.`,
278281

279282
if len(indexContent) == 0 {
280283
cmd.Printf(" ❌ index.html is empty, cannot create dashboard.html\n")
281-
return fmt.Errorf("generated index.html is empty")
284+
return ErrEmptyIndexHTML
282285
}
283286

284287
if writeErr := os.WriteFile(dashboardPath, indexContent, cfg.Storage.FileMode); writeErr != nil {
@@ -287,12 +290,12 @@ update history, and create GitHub PR comment if in PR context.`,
287290
}
288291

289292
// Verify dashboard.html was created successfully
290-
if dashboardStat, statErr := os.Stat(dashboardPath); statErr != nil {
293+
dashboardStat, statErr := os.Stat(dashboardPath)
294+
if statErr != nil {
291295
cmd.Printf(" ❌ dashboard.html was not created successfully: %v\n", statErr)
292296
return fmt.Errorf("dashboard.html creation verification failed: %w", statErr)
293-
} else {
294-
cmd.Printf(" ✅ Dashboard also saved as: %s (%d bytes)\n", dashboardPath, dashboardStat.Size())
295297
}
298+
cmd.Printf(" ✅ Dashboard also saved as: %s (%d bytes)\n", dashboardPath, dashboardStat.Size())
296299

297300
// Also save coverage data as JSON for pages deployment
298301
dataPath := filepath.Join(outputDir, "coverage-data.json")

.github/coverage/internal/config/config.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,17 +264,65 @@ func (c *Config) GetBadgeURL() string {
264264
if c.GitHub.Owner == "" || c.GitHub.Repository == "" {
265265
return ""
266266
}
267-
return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/main/%s/%s",
268-
c.GitHub.Owner, c.GitHub.Repository, c.Storage.BaseDir, c.Badge.OutputFile)
267+
268+
// Use GitHub Pages URL structure
269+
baseURL := fmt.Sprintf("https://%s.github.io/%s", c.GitHub.Owner, c.GitHub.Repository)
270+
271+
// If in PR context, return PR-specific badge URL
272+
if c.IsPullRequestContext() {
273+
return fmt.Sprintf("%s/badges/pr/%d/coverage.svg", baseURL, c.GitHub.PullRequest)
274+
}
275+
276+
// For branch-specific badges, get current branch (default to master)
277+
branch := c.getCurrentBranch()
278+
if branch == "master" || branch == "main" {
279+
// Main branch badge at root badges directory
280+
return fmt.Sprintf("%s/badges/coverage.svg", baseURL)
281+
}
282+
283+
// Branch-specific badge
284+
return fmt.Sprintf("%s/badges/%s/coverage.svg", baseURL, branch)
269285
}
270286

271287
// GetReportURL returns the URL for the coverage report
272288
func (c *Config) GetReportURL() string {
273289
if c.GitHub.Owner == "" || c.GitHub.Repository == "" {
274290
return ""
275291
}
276-
return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/main/%s/%s",
277-
c.GitHub.Owner, c.GitHub.Repository, c.Storage.BaseDir, c.Report.OutputFile)
292+
293+
// Use GitHub Pages URL structure
294+
baseURL := fmt.Sprintf("https://%s.github.io/%s", c.GitHub.Owner, c.GitHub.Repository)
295+
296+
// If in PR context, return PR-specific report URL
297+
if c.IsPullRequestContext() {
298+
return fmt.Sprintf("%s/reports/pr/%d/coverage.html", baseURL, c.GitHub.PullRequest)
299+
}
300+
301+
// For branch-specific reports, get current branch (default to master)
302+
branch := c.getCurrentBranch()
303+
if branch == "master" || branch == "main" {
304+
// Main branch report at root coverage directory
305+
return fmt.Sprintf("%s/coverage/", baseURL)
306+
}
307+
308+
// Branch-specific report
309+
return fmt.Sprintf("%s/reports/branch/%s/coverage.html", baseURL, branch)
310+
}
311+
312+
// getCurrentBranch returns the current branch name, defaulting to master
313+
func (c *Config) getCurrentBranch() string {
314+
// Try to get branch from environment variables (GitHub Actions context)
315+
if branch := os.Getenv("GITHUB_REF_NAME"); branch != "" {
316+
return branch
317+
}
318+
if ref := os.Getenv("GITHUB_REF"); ref != "" {
319+
// Extract branch name from refs/heads/branch-name
320+
if strings.HasPrefix(ref, "refs/heads/") {
321+
return strings.TrimPrefix(ref, "refs/heads/")
322+
}
323+
}
324+
// Default to master (this repo's default branch)
325+
return "master"
278326
}
279327

280328
// Helper functions for environment variable parsing

.github/workflows/auto-merge-on-approval.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,7 @@ on:
3434
pull_request:
3535
types: [ready_for_review, review_request_removed]
3636

37-
# ————————————————————————————————————————————————————————————————
38-
# Permissions
39-
# ————————————————————————————————————————————————————————————————
40-
permissions:
41-
contents: read
42-
pull-requests: read
37+
# Security: Job-level permissions are set per job for least privilege access
4338

4439
# ————————————————————————————————————————————————————————————————
4540
# Concurrency Control

.github/workflows/codeql-analysis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ concurrency:
1818
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
1919
cancel-in-progress: true
2020

21-
permissions:
22-
contents: read
21+
# Security: Job-level permissions are set per job for least privilege access
2322

2423
jobs:
2524
analyze:

.github/workflows/dependabot-auto-merge.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,7 @@ on:
3030
pull_request:
3131
types: [opened, synchronize, reopened, ready_for_review]
3232

33-
# ————————————————————————————————————————————————————————————————
34-
# Permissions
35-
# ————————————————————————————————————————————————————————————————
36-
permissions:
37-
contents: read
38-
pull-requests: read
33+
# Security: Job-level permissions are set per job for least privilege access
3934

4035
# ————————————————————————————————————————————————————————————————
4136
# Concurrency Control

.github/workflows/fortress-benchmarks.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ on:
3838
description: "GitHub token for API access"
3939
required: true
4040

41-
permissions:
42-
contents: read
41+
# Security: Job-level permissions are set per job for least privilege access
4342

4443
jobs:
4544
# ----------------------------------------------------------------------------------

.github/workflows/fortress-code-quality.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ on:
4949
description: "GitHub token for API access"
5050
required: true
5151

52-
permissions:
53-
contents: read
52+
# Security: Job-level permissions are set per job for least privilege access
5453

5554
jobs:
5655
# ----------------------------------------------------------------------------------

.github/workflows/fortress-coverage.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ jobs:
314314
- name: 💬 Create PR comment
315315
if: inputs.pr-number != ''
316316
env:
317-
GITHUB_TOKEN: ${{ secrets.github-token }}
317+
GITHUB_TOKEN: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.github-token }}
318318
working-directory: .github/coverage/cmd/gofortress-coverage
319319
run: |
320320
echo "💬 Creating PR comment for PR #${{ inputs.pr-number }}..."
@@ -436,7 +436,7 @@ jobs:
436436
- name: 🚀 Deploy to GitHub Pages
437437
if: github.event_name == 'push' || github.event_name == 'pull_request'
438438
env:
439-
GITHUB_TOKEN: ${{ secrets.github-token }}
439+
GITHUB_TOKEN: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.github-token }}
440440
working-directory: .github/coverage/cmd/gofortress-coverage
441441
run: |
442442
echo "🚀 Deploying coverage to GitHub Pages..."
@@ -490,7 +490,7 @@ jobs:
490490
# ————————————————————————————————————————————————————————————————
491491
- name: 📋 Set coverage status
492492
env:
493-
GITHUB_TOKEN: ${{ secrets.github-token }}
493+
GITHUB_TOKEN: ${{ secrets.GH_PAT_TOKEN != '' && secrets.GH_PAT_TOKEN || secrets.github-token }}
494494
working-directory: .github/coverage/cmd/gofortress-coverage
495495
run: |
496496
echo "📋 Setting coverage status check..."

.github/workflows/fortress-performance-summary.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ on:
6969
required: true
7070
type: string
7171

72-
permissions:
73-
contents: read
72+
# Security: Job-level permissions are set per job for least privilege access
7473

7574
jobs:
7675
# ----------------------------------------------------------------------------------

.github/workflows/fortress-release.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,7 @@ on:
4040
description: "Slack webhook URL for notifications"
4141
required: false
4242

43-
# ————————————————————————————————————————————————————————————————
44-
# Permissions
45-
# ————————————————————————————————————————————————————————————————
46-
permissions:
47-
contents: read
43+
# Security: Job-level permissions are set per job for least privilege access
4844

4945
jobs:
5046
# ----------------------------------------------------------------------------------

0 commit comments

Comments
 (0)