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
3 changes: 3 additions & 0 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Examples:
logicalRepo, _ := cmd.Flags().GetString("logical-repo")
dependabot, _ := cmd.Flags().GetBool("dependabot")
forceOverwrite, _ := cmd.Flags().GetBool("force")
zizmor, _ := cmd.Flags().GetBool("zizmor")
verbose, _ := cmd.Flags().GetBool("verbose")
if err := validateEngine(engineOverride); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
Expand All @@ -175,6 +176,7 @@ Examples:
Strict: strict,
Dependabot: dependabot,
ForceOverwrite: forceOverwrite,
Zizmor: zizmor,
}
if _, err := cli.CompileWorkflows(config); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
Expand Down Expand Up @@ -282,6 +284,7 @@ func init() {
compileCmd.Flags().String("logical-repo", "", "Repository to simulate workflow execution against (for trial mode)")
compileCmd.Flags().Bool("dependabot", false, "Generate dependency manifests (package.json, requirements.txt, go.mod) and Dependabot config when dependencies are detected")
compileCmd.Flags().Bool("force", false, "Force overwrite of existing files (e.g., dependabot.yml)")
compileCmd.Flags().Bool("zizmor", false, "Run zizmor security scanner on generated .lock.yml files")
rootCmd.AddCommand(compileCmd)

// Add flags to remove command
Expand Down
116 changes: 115 additions & 1 deletion pkg/cli/compile_command.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package cli

import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
Expand Down Expand Up @@ -96,6 +98,7 @@ type CompileConfig struct {
Strict bool // Enable strict mode validation
Dependabot bool // Generate Dependabot manifests for npm dependencies
ForceOverwrite bool // Force overwrite of existing files (dependabot.yml)
Zizmor bool // Run zizmor security scanner on generated .lock.yml files
}

// CompilationStats tracks the results of workflow compilation
Expand All @@ -120,8 +123,9 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
strict := config.Strict
dependabot := config.Dependabot
forceOverwrite := config.ForceOverwrite
zizmor := config.Zizmor

compileLog.Printf("Starting workflow compilation: files=%d, validate=%v, watch=%v, noEmit=%v, dependabot=%v", len(markdownFiles), validate, watch, noEmit, dependabot)
compileLog.Printf("Starting workflow compilation: files=%d, validate=%v, watch=%v, noEmit=%v, dependabot=%v, zizmor=%v", len(markdownFiles), validate, watch, noEmit, dependabot, zizmor)

// Track compilation statistics
stats := &CompilationStats{}
Expand Down Expand Up @@ -301,6 +305,25 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
return workflowDataList, fmt.Errorf("compilation failed")
}

// Run zizmor security scanner if requested and compilation was successful
if zizmor && !noEmit {
// Resolve workflow directory path
if workflowDir == "" {
workflowDir = ".github/workflows"
}
absWorkflowDir := workflowDir
if !filepath.IsAbs(absWorkflowDir) {
gitRoot, err := findGitRoot()
if err == nil {
absWorkflowDir = filepath.Join(gitRoot, workflowDir)
}
}

if err := runZizmor(absWorkflowDir, verbose); err != nil {
return workflowDataList, fmt.Errorf("zizmor security scan failed: %w", err)
}
}

return workflowDataList, nil
}

Expand Down Expand Up @@ -461,6 +484,13 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
return workflowDataList, fmt.Errorf("compilation failed")
}

// Run zizmor security scanner if requested and compilation was successful
if zizmor && !noEmit {
if err := runZizmor(workflowsDir, verbose); err != nil {
return workflowDataList, fmt.Errorf("zizmor security scan failed: %w", err)
}
}

return workflowDataList, nil
}

Expand Down Expand Up @@ -799,3 +829,87 @@ func printCompilationSummary(stats *CompilationStats) {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(summary))
}
}

// runZizmor runs the zizmor security scanner on generated .lock.yml files using Docker
func runZizmor(workflowsDir string, verbose bool) error {
compileLog.Print("Running zizmor security scanner")

if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Running zizmor security scanner on generated .lock.yml files..."))
}

// Find git root to get the absolute path for Docker volume mount
gitRoot, err := findGitRoot()
if err != nil {
return fmt.Errorf("failed to find git root: %w", err)
}

// Get the absolute path of the workflows directory
var absWorkflowsDir string
if filepath.IsAbs(workflowsDir) {
absWorkflowsDir = workflowsDir
} else {
absWorkflowsDir = filepath.Join(gitRoot, workflowsDir)
}

compileLog.Printf("Running zizmor on directory: %s", absWorkflowsDir)

// Build the Docker command
// docker run --rm -v "$(pwd)":/workdir -w /workdir ghcr.io/zizmorcore/zizmor:latest .
cmd := exec.Command(
"docker",
"run",
"--rm",
"-v", fmt.Sprintf("%s:/workdir", gitRoot),
"-w", "/workdir",
"ghcr.io/zizmorcore/zizmor:latest",
".",
)

// Capture output
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

// Run the command
err = cmd.Run()

// Always show zizmor output
if stdout.Len() > 0 {
fmt.Fprint(os.Stderr, stdout.String())
}
if stderr.Len() > 0 {
fmt.Fprint(os.Stderr, stderr.String())
}

// Check if the error is due to findings (expected) or actual failure
if err != nil {
// zizmor uses exit codes to indicate findings:
// 0 = no findings
// 10-13 = findings at different severity levels
// 14 = findings with mixed severities
// Other codes = actual errors
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode := exitErr.ExitCode()
compileLog.Printf("Zizmor exited with code %d", exitCode)
// Exit codes 10-14 indicate findings, not failures
// Treat these as success but log them
if exitCode >= 10 && exitCode <= 14 {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Zizmor found security findings (see output above)"))
}
return nil
}
// Other exit codes are actual errors
return fmt.Errorf("zizmor failed with exit code %d", exitCode)
}
// Non-ExitError errors (e.g., command not found)
return fmt.Errorf("zizmor failed: %w", err)
}

if verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Zizmor security scan completed - no findings"))
}

return nil
}
81 changes: 81 additions & 0 deletions pkg/cli/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,84 @@ Please check the repository for any open issues and create a summary.

t.Logf("Integration test passed - successfully compiled workflow to %s", lockFilePath)
}

// TestCompileWithZizmor tests the compile command with --zizmor flag
func TestCompileWithZizmor(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

// Initialize git repository for zizmor to work (it needs git root)
gitInitCmd := exec.Command("git", "init")
gitInitCmd.Dir = setup.tempDir
if output, err := gitInitCmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to initialize git repository: %v\nOutput: %s", err, string(output))
}

// Configure git user for the repository
gitConfigEmail := exec.Command("git", "config", "user.email", "test@test.com")
gitConfigEmail.Dir = setup.tempDir
if output, err := gitConfigEmail.CombinedOutput(); err != nil {
t.Fatalf("Failed to configure git user email: %v\nOutput: %s", err, string(output))
}

gitConfigName := exec.Command("git", "config", "user.name", "Test User")
gitConfigName.Dir = setup.tempDir
if output, err := gitConfigName.CombinedOutput(); err != nil {
t.Fatalf("Failed to configure git user name: %v\nOutput: %s", err, string(output))
}

// Create a test markdown workflow file
testWorkflow := `---
name: Zizmor Test Workflow
on:
workflow_dispatch:
permissions:
contents: read
engine: copilot
---

# Zizmor Test Workflow

This workflow tests the zizmor security scanner integration.
`

testWorkflowPath := filepath.Join(setup.workflowsDir, "zizmor-test.md")
if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil {
t.Fatalf("Failed to write test workflow file: %v", err)
}

// First compile without zizmor to create the lock file
compileCmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath)
if output, err := compileCmd.CombinedOutput(); err != nil {
t.Fatalf("Initial compile failed: %v\nOutput: %s", err, string(output))
}

// Check that the lock file was created
lockFilePath := filepath.Join(setup.workflowsDir, "zizmor-test.lock.yml")
if _, err := os.Stat(lockFilePath); os.IsNotExist(err) {
t.Fatalf("Expected lock file %s was not created", lockFilePath)
}

// Now compile with --zizmor flag
zizmorCmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath, "--zizmor", "--verbose")
output, err := zizmorCmd.CombinedOutput()

// The command should succeed even if zizmor finds issues
if err != nil {
t.Fatalf("Compile with --zizmor failed: %v\nOutput: %s", err, string(output))
}

outputStr := string(output)

// Check that zizmor was run
if !strings.Contains(outputStr, "zizmor") && !strings.Contains(outputStr, "Zizmor") {
t.Errorf("Output should mention zizmor scanner")
}

// The lock file should still exist after zizmor scan
if _, err := os.Stat(lockFilePath); os.IsNotExist(err) {
t.Fatalf("Lock file was removed after zizmor scan")
}

t.Logf("Integration test passed - zizmor flag works correctly")
}
Loading