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
119 changes: 19 additions & 100 deletions .github/workflows/static-analysis-report.lock.yml

Large diffs are not rendered by default.

82 changes: 26 additions & 56 deletions .github/workflows/static-analysis-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,51 +18,16 @@ tools:
cache-memory: true
timeout: 600
safe-outputs:
create-discussion:
expires: 1d
category: "security"
max: 1
close-older-discussions: true
create-issue:
expires: 7d
title-prefix: "[runner-guard] "
title-prefix: "[static-analysis] "
labels: [security, automation]
max: 3
max: 4
close-older-issues: true
timeout-minutes: 45
strict: true
imports:
- shared/reporting.md
jobs:
runner_guard:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6.0.2
with:
persist-credentials: false
- name: Install runner-guard
run: go install github.com/Vigilant-LLC/runner-guard/v2/cmd/runner-guard@v2.6.0
- name: Run runner-guard scan
run: |
RUNNER_GUARD="$(go env GOPATH)/bin/runner-guard"
if [ ! -x "$RUNNER_GUARD" ]; then
echo '{"findings":[],"error":"runner-guard binary not found after install"}' > /tmp/runner-guard-results.json
else
"$RUNNER_GUARD" scan . --format json > /tmp/runner-guard-results.json 2>/tmp/runner-guard-stderr.log || true
# If output is empty or not valid JSON, write empty result
if ! python3 -c "import json,sys; json.load(open('/tmp/runner-guard-results.json'))" 2>/dev/null; then
echo '{"findings":[],"stderr":"'"$(cat /tmp/runner-guard-stderr.log | head -20 | tr '"' "'")"'"}' > /tmp/runner-guard-results.json
fi
fi
- name: Upload runner-guard results
if: always()
uses: actions/upload-artifact@v7
with:
name: runner-guard-results
path: /tmp/runner-guard-results.json
retention-days: 1
steps:
- name: Install gh-aw CLI
env:
Expand All @@ -87,6 +52,10 @@ steps:
echo "Pulling poutine image..."
docker pull ghcr.io/boostsecurityio/poutine:latest

# Pull runner-guard Docker image
echo "Pulling runner-guard image..."
docker pull ghcr.io/vigilant-llc/runner-guard:latest

echo "All static analysis Docker images pulled successfully"
- name: Verify static analysis tools
run: |
Expand All @@ -101,6 +70,10 @@ steps:
echo "Testing poutine..."
docker run --rm ghcr.io/boostsecurityio/poutine:latest --version || echo "Warning: poutine version check failed"

# Verify runner-guard
echo "Testing runner-guard..."
docker run --rm ghcr.io/vigilant-llc/runner-guard:latest --version || echo "Warning: runner-guard version check failed"

echo "Static analysis tools verification complete"
- name: Run compile with security tools
run: |
Expand All @@ -109,15 +82,10 @@ steps:

# Run compile with all security scanner flags to download Docker images
# Store the output in a file for inspection
gh aw compile --zizmor --poutine --actionlint 2>&1 | tee /tmp/gh-aw/compile-output.txt
gh aw compile --zizmor --poutine --actionlint --runner-guard 2>&1 | tee /tmp/gh-aw/compile-output.txt

echo "Compile with security tools completed"
echo "Output saved to /tmp/gh-aw/compile-output.txt"
- name: Download runner-guard results
uses: actions/download-artifact@v8.0.1
with:
name: runner-guard-results
path: /tmp/gh-aw/
---

# Static Analysis Report
Expand Down Expand Up @@ -288,11 +256,11 @@ Use the cache memory folder `/tmp/gh-aw/cache-memory/` to build persistent knowl
```
```

### Phase 5: Create Discussion Report
### Phase 5: Create Issue Report

**ALWAYS create a comprehensive discussion report** with your static analysis findings, regardless of whether issues were found or not.
**ALWAYS create a comprehensive issue report** with your static analysis findings, regardless of whether issues were found or not.

Create a discussion with:
Create an issue with:
- **Summary**: Overview of static analysis findings from all three tools
- **Statistics**: Total findings by tool, by severity, by type
- **Clustered Findings**: Issues grouped by tool and type with counts
Expand All @@ -301,7 +269,7 @@ Create a discussion with:
- **Recommendations**: Prioritized actions to improve security and code quality
- **Historical Trends**: Comparison with previous scans

**Discussion Template**:
**Issue Template**:
```markdown
# 🔍 Static Analysis Report - [DATE]

Expand Down Expand Up @@ -424,12 +392,14 @@ Issues created: [list of issue links for Critical/High findings, or "none"]
- [ ] Consider adding all three tools to pre-commit hooks
```

Use the title `[static-analysis] Report - [DATE]` for the issue.

### Phase 6: Analyze Runner-Guard Findings

Runner-guard has performed source-to-sink vulnerability scanning on the repository's GitHub Actions workflows. The results are available at `/tmp/gh-aw/runner-guard-results.json`.
Runner-guard has performed source-to-sink vulnerability scanning as part of the compile step. The results are included in the compilation output at `/tmp/gh-aw/compile-output.txt`.

1. **Read Runner-Guard Output**:
Read the file `/tmp/gh-aw/runner-guard-results.json` which contains findings from runner-guard's taint analysis (detection rules covering fork checkout exploits, expression injection, secret exfiltration, unpinned actions, AI config injection, and supply chain steganography).
Parse the runner-guard findings from `/tmp/gh-aw/compile-output.txt` — runner-guard findings are included alongside zizmor, poutine, and actionlint results (detection rules covering fork checkout exploits, expression injection, secret exfiltration, unpinned actions, AI config injection, and supply chain steganography).

2. **Analyze Findings**:
- Parse the JSON to extract findings
Expand All @@ -441,14 +411,14 @@ Runner-guard has performed source-to-sink vulnerability scanning on the reposito
For up to 3 of the most critical findings (by severity, then rule ID), create a GitHub issue.

Before creating issues:
- Search for existing open issues whose title contains `[runner-guard]` and the rule ID (e.g. `RGS-001`) to avoid duplicates
- Search for existing open issues whose title contains `[static-analysis]` and the rule ID (e.g. `RGS-001`) to avoid duplicates
- Only create issues for Critical and High severity findings
- Do not create an issue if a matching open issue already exists for the same rule ID
- Maximum 3 issues total across all runner-guard findings per run

Issue format:
```
Title: [runner-guard] <RuleID>: <FindingName> in <AffectedFile>
Title: [static-analysis] <RuleID>: <FindingName> in <AffectedFile>

## 🚨 Runner-Guard Security Finding

Expand All @@ -472,7 +442,7 @@ Runner-guard has performed source-to-sink vulnerability scanning on the reposito
```

4. **Add to Discussion**:
Include a "Runner-Guard Analysis" section in the Phase 5 discussion report (see updated discussion template below).
Include a "Runner-Guard Analysis" section in the Phase 5 issue report.

## Important Guidelines

Expand Down Expand Up @@ -514,7 +484,7 @@ Organize your persistent data in `/tmp/gh-aw/cache-memory/`:

## Output Requirements

Your output must be well-structured and actionable. **You must create a discussion** for every scan with the findings from all three tools.
Your output must be well-structured and actionable. **You must create an issue** for every scan with the findings from all three tools.

Update cache memory with today's scan data for future reference and trend analysis.

Expand All @@ -525,13 +495,13 @@ A successful static analysis scan:
- ✅ Clusters findings by tool and issue type
- ✅ Generates a detailed fix prompt for at least one issue type
- ✅ Updates cache memory with findings from all tools
- ✅ Creates a comprehensive discussion report with findings
- ✅ Creates a comprehensive issue report with findings
- ✅ Provides actionable recommendations
- ✅ Maintains historical context for trend analysis
- ✅ Reads and analyzes runner-guard source-to-sink findings
- ✅ Creates up to 3 GitHub issues for Critical/High runner-guard findings (avoiding duplicates)

Begin your static analysis scan now. Read and parse the compilation output from `/tmp/gh-aw/compile-output.txt`, analyze the findings from all four tools (zizmor, poutine, actionlint, runner-guard), cluster them, generate fix suggestions, create up to 3 issues for critical runner-guard findings, and create a discussion with your complete analysis.
Begin your static analysis scan now. Read and parse the compilation output from `/tmp/gh-aw/compile-output.txt`, analyze the findings from all four tools (zizmor, poutine, actionlint, runner-guard), cluster them, generate fix suggestions, create up to 3 issues for critical runner-guard findings, and create an issue with your complete analysis.

**Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures.

Expand Down
3 changes: 3 additions & 0 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ Examples:
zizmor, _ := cmd.Flags().GetBool("zizmor")
poutine, _ := cmd.Flags().GetBool("poutine")
actionlint, _ := cmd.Flags().GetBool("actionlint")
runnerGuard, _ := cmd.Flags().GetBool("runner-guard")
jsonOutput, _ := cmd.Flags().GetBool("json")
fix, _ := cmd.Flags().GetBool("fix")
stats, _ := cmd.Flags().GetBool("stats")
Expand Down Expand Up @@ -333,6 +334,7 @@ Examples:
Zizmor: zizmor,
Poutine: poutine,
Actionlint: actionlint,
RunnerGuard: runnerGuard,
JSONOutput: jsonOutput,
Stats: stats,
FailFast: failFast,
Expand Down Expand Up @@ -679,6 +681,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
compileCmd.Flags().Bool("zizmor", false, "Run zizmor security scanner on generated .lock.yml files")
compileCmd.Flags().Bool("poutine", false, "Run poutine security scanner on generated .lock.yml files")
compileCmd.Flags().Bool("actionlint", false, "Run actionlint linter on generated .lock.yml files")
compileCmd.Flags().Bool("runner-guard", false, "Run runner-guard taint analysis scanner on generated .lock.yml files (uses Docker image "+cli.RunnerGuardImage+")")
compileCmd.Flags().Bool("fix", false, "Apply automatic codemod fixes to workflows before compiling")
compileCmd.Flags().BoolP("json", "j", false, "Output results in JSON format")
compileCmd.Flags().Bool("stats", false, "Display statistics table sorted by workflow file size (shows jobs, steps, scripts, and shells)")
Expand Down
23 changes: 23 additions & 0 deletions pkg/cli/compile_batch_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ func RunPoutineOnDirectory(workflowDir string, verbose bool, strict bool) error
return runPoutineOnDirectory(workflowDir, verbose, strict)
}

// RunRunnerGuardOnDirectory runs runner-guard taint analysis scanner once on a directory.
// Runner-guard scans all workflows in a directory, so it only needs to run once.
func RunRunnerGuardOnDirectory(workflowDir string, verbose bool, strict bool) error {
return runRunnerGuardOnDirectory(workflowDir, verbose, strict)
}

// runBatchLockFileTool runs a batch tool on lock files with uniform error handling
func runBatchLockFileTool(toolName string, lockFiles []string, verbose bool, strict bool, runner func([]string, bool, bool) error) error {
if len(lockFiles) == 0 {
Expand Down Expand Up @@ -110,6 +116,23 @@ func runBatchPoutine(workflowDir string, verbose bool, strict bool) error {
return nil
}

// runBatchRunnerGuard runs runner-guard taint analysis scanner once for the entire directory
func runBatchRunnerGuard(workflowDir string, verbose bool, strict bool) error {
compileBatchOperationsLog.Printf("Running batch runner-guard on directory: %s", workflowDir)

if err := RunRunnerGuardOnDirectory(workflowDir, verbose, strict); err != nil {
if strict {
return fmt.Errorf("runner-guard taint analysis failed: %w", err)
}
// In non-strict mode, runner-guard errors are warnings
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("runner-guard warnings: %v", err)))
}
}

return nil
}

// purgeOrphanedLockFiles removes orphaned .lock.yml files
// These are lock files that exist but don't have a corresponding .md file
func purgeOrphanedLockFiles(workflowsDir string, expectedLockFiles []string, verbose bool) error {
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type CompileConfig struct {
Zizmor bool // Run zizmor security scanner on generated .lock.yml files
Poutine bool // Run poutine security scanner on generated .lock.yml files
Actionlint bool // Run actionlint linter on generated .lock.yml files
RunnerGuard bool // Run runner-guard taint analysis scanner on generated .lock.yml files
JSONOutput bool // Output validation results as JSON
ActionMode string // Action script inlining mode: inline, dev, or release
ActionTag string // Override action SHA or tag for actions/setup (overrides action-mode to release)
Expand Down
34 changes: 31 additions & 3 deletions pkg/cli/compile_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func compileSpecificFiles(
var errorCount int
var lockFilesForActionlint []string
var lockFilesForZizmor []string
var lockFilesForDirTools []string // lock files for directory-based tools (poutine, runner-guard)

// Compile each specified file
for _, markdownFile := range config.MarkdownFiles {
Expand Down Expand Up @@ -122,6 +123,9 @@ func compileSpecificFiles(
if config.Zizmor {
lockFilesForZizmor = append(lockFilesForZizmor, fileResult.lockFile)
}
if config.Poutine || config.RunnerGuard {
lockFilesForDirTools = append(lockFilesForDirTools, fileResult.lockFile)
}
}
}
}
Expand Down Expand Up @@ -149,15 +153,26 @@ func compileSpecificFiles(

// Run batch poutine once on the workflow directory
// Get the directory from the first lock file (all should be in same directory)
if config.Poutine && !config.NoEmit && len(lockFilesForZizmor) > 0 {
workflowDir := filepath.Dir(lockFilesForZizmor[0])
if config.Poutine && !config.NoEmit && len(lockFilesForDirTools) > 0 {
workflowDir := filepath.Dir(lockFilesForDirTools[0])
if err := runBatchPoutine(workflowDir, config.Verbose && !config.JSONOutput, config.Strict); err != nil {
if config.Strict {
return workflowDataList, err
}
}
}

// Run batch runner-guard once on the workflow directory
// Get the directory from the first lock file (all should be in same directory)
if config.RunnerGuard && !config.NoEmit && len(lockFilesForDirTools) > 0 {
workflowDir := filepath.Dir(lockFilesForDirTools[0])
if err := runBatchRunnerGuard(workflowDir, config.Verbose && !config.JSONOutput, config.Strict); err != nil {
if config.Strict {
return workflowDataList, err
}
}
}

// Get warning count from compiler
stats.Warnings = compiler.GetWarningCount()

Expand Down Expand Up @@ -247,6 +262,7 @@ func compileAllFilesInDirectory(
var errorCount int
var lockFilesForActionlint []string
var lockFilesForZizmor []string
var lockFilesForDirTools []string // lock files for directory-based tools (poutine, runner-guard)

for _, file := range mdFiles {
stats.Total++
Expand Down Expand Up @@ -280,6 +296,9 @@ func compileAllFilesInDirectory(
if config.Zizmor {
lockFilesForZizmor = append(lockFilesForZizmor, fileResult.lockFile)
}
if config.Poutine || config.RunnerGuard {
lockFilesForDirTools = append(lockFilesForDirTools, fileResult.lockFile)
}
}
}
}
Expand All @@ -306,14 +325,23 @@ func compileAllFilesInDirectory(
}

// Run batch poutine once on the workflow directory
if config.Poutine && !config.NoEmit && len(lockFilesForZizmor) > 0 {
if config.Poutine && !config.NoEmit && len(lockFilesForDirTools) > 0 {
if err := runBatchPoutine(workflowsDir, config.Verbose && !config.JSONOutput, config.Strict); err != nil {
if config.Strict {
return workflowDataList, err
}
}
}

// Run batch runner-guard once on the workflow directory
if config.RunnerGuard && !config.NoEmit && len(lockFilesForDirTools) > 0 {
if err := runBatchRunnerGuard(workflowsDir, config.Verbose && !config.JSONOutput, config.Strict); err != nil {
if config.Strict {
return workflowDataList, err
}
}
}

// Get warning count from compiler
stats.Warnings = compiler.GetWarningCount()

Expand Down
17 changes: 12 additions & 5 deletions pkg/cli/docker_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ func (e *DockerUnavailableError) Error() string {

// DockerImages defines the Docker images used by the compile tool's static analysis scanners
const (
ZizmorImage = "ghcr.io/zizmorcore/zizmor:latest"
PoutineImage = "ghcr.io/boostsecurityio/poutine:latest"
ActionlintImage = "rhysd/actionlint:latest"
ZizmorImage = "ghcr.io/zizmorcore/zizmor:latest"
PoutineImage = "ghcr.io/boostsecurityio/poutine:latest"
ActionlintImage = "rhysd/actionlint:latest"
RunnerGuardImage = "ghcr.io/vigilant-llc/runner-guard:latest"
)

// dockerPullState tracks the state of docker pull operations
Expand Down Expand Up @@ -204,9 +205,9 @@ func StartDockerImageDownload(ctx context.Context, image string) bool {
// Returns:
// - nil if all required images are available
// - error if Docker is unavailable or images are downloading/need to be downloaded
func CheckAndPrepareDockerImages(ctx context.Context, useZizmor, usePoutine, useActionlint bool) error {
func CheckAndPrepareDockerImages(ctx context.Context, useZizmor, usePoutine, useActionlint, useRunnerGuard bool) error {
// If no tools requested, nothing to do
if !useZizmor && !usePoutine && !useActionlint {
if !useZizmor && !usePoutine && !useActionlint && !useRunnerGuard {
return nil
}

Expand All @@ -229,6 +230,11 @@ func CheckAndPrepareDockerImages(ctx context.Context, useZizmor, usePoutine, use
requestedTools = append(requestedTools, tool)
paramsList = append(paramsList, tool+": false")
}
if useRunnerGuard {
tool := "runner-guard"
requestedTools = append(requestedTools, tool)
paramsList = append(paramsList, tool+": false")
}
verb := "requires"
if len(requestedTools) > 1 {
verb = "require"
Expand All @@ -250,6 +256,7 @@ func CheckAndPrepareDockerImages(ctx context.Context, useZizmor, usePoutine, use
{useZizmor, ZizmorImage, "zizmor"},
{usePoutine, PoutineImage, "poutine"},
{useActionlint, ActionlintImage, "actionlint"},
{useRunnerGuard, RunnerGuardImage, "runner-guard"},
}

for _, img := range imagesToCheck {
Expand Down
Loading