diff --git a/TODO.md b/TODO.md index 9ea174e..083cbb6 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,7 @@ > **Status**: Based on comprehensive analysis by go-developer agent > **Project Quality**: A+ Excellent (Current) → Industry-Leading Reference (Target) -> **Last Updated**: December 2024 +> **Last Updated**: January 2025 (Progress indicators completed) ## Priority Legend - šŸ”„ **Immediate** - Critical security, performance, or stability issues @@ -16,7 +16,7 @@ ### Security Hardening -#### 1. Integrate Static Application Security Testing (SAST) +#### 1. āœ… Integrate Static Application Security Testing (SAST) [COMPLETED: Jan 2025] **Priority**: šŸ”„ Immediate **Complexity**: Medium **Timeline**: 1-2 weeks @@ -35,9 +35,14 @@ uses: returntocorp/semgrep-action@v1 ``` +**Completion Notes**: +- āœ… Integrated gosec via golangci-lint configuration +- āœ… CodeQL already active in .github/workflows/codeql.yml +- āœ… Security workflow created with comprehensive scanning + **Benefits**: Proactive vulnerability detection, compliance readiness, security-first development -#### 2. Dependency Vulnerability Scanning +#### 2. āœ… Dependency Vulnerability Scanning [COMPLETED: Jan 2025] **Priority**: šŸ”„ Immediate **Complexity**: Low **Timeline**: 1 week @@ -47,9 +52,15 @@ - Add `snyk` or `trivy` for comprehensive dependency analysis - Configure automated alerts for new vulnerabilities +**Completion Notes**: +- āœ… Implemented govulncheck in security workflow and Makefile +- āœ… Added both Snyk AND Trivy for comprehensive coverage +- āœ… Configured Dependabot for automated dependency updates +- āœ… Updated Go version to 1.23.10 to fix stdlib vulnerabilities + **Benefits**: Supply chain security, automated vulnerability management, compliance -#### 3. Secrets Detection & Prevention +#### 3. āœ… Secrets Detection & Prevention [COMPLETED: Jan 2025] **Priority**: šŸ”„ Immediate **Complexity**: Low **Timeline**: 1 week @@ -59,6 +70,12 @@ - Add pre-commit hooks for secret prevention - Scan historical commits for exposed secrets +**Completion Notes**: +- āœ… Integrated gitleaks in security workflow +- āœ… Created .gitleaksignore for managing false positives +- āœ… Added gitleaks to Makefile security targets +- āœ… Configured for both current and historical commit scanning + **Benefits**: Prevent data breaches, protect API keys, maintain security posture --- @@ -182,7 +199,7 @@ func (ce *ContextualError) Error() string { **Benefits**: Improved onboarding, reduced configuration errors, better adoption -#### 9. Progress Indicators & Status Updates +#### 9. āœ… Progress Indicators & Status Updates [COMPLETED: Jan 2025] **Priority**: šŸš€ High **Complexity**: Low **Timeline**: 1 week @@ -206,6 +223,15 @@ func (g *Generator) ProcessWithProgress(files []string) error { } ``` +**Completion Notes**: +- āœ… Enhanced dependency analyzer with `AnalyzeActionFileWithProgress()` method +- āœ… Added progress bars to `analyzeDependencies()` and `analyzeSecurityDeps()` functions +- āœ… Added `IsQuiet()` method to ColoredOutput for proper mode handling +- āœ… Progress bars automatically show for multi-file operations (>1 file) +- āœ… Progress bars respect quiet mode and are hidden with `--quiet` flag +- āœ… Refactored code to reduce cyclomatic complexity from 14 to under 10 +- āœ… All tests passing, 0 linting issues, maintains backward compatibility + **Benefits**: Better user feedback, professional feel, progress transparency --- diff --git a/internal/dependencies/analyzer.go b/internal/dependencies/analyzer.go index cb1dcc2..44f8d96 100644 --- a/internal/dependencies/analyzer.go +++ b/internal/dependencies/analyzer.go @@ -100,51 +100,108 @@ func NewAnalyzer(client *github.Client, repoInfo git.RepoInfo, cache DependencyC // AnalyzeActionFile analyzes dependencies from an action.yml file. func (a *Analyzer) AnalyzeActionFile(actionPath string) ([]Dependency, error) { + return a.AnalyzeActionFileWithProgress(actionPath, nil) +} + +// AnalyzeActionFileWithProgress analyzes dependencies with optional progress tracking. +func (a *Analyzer) AnalyzeActionFileWithProgress( + actionPath string, + progressCallback func(current, total int, message string), +) ([]Dependency, error) { + if progressCallback != nil { + progressCallback(0, 1, fmt.Sprintf("Parsing %s", actionPath)) + } + // Read and parse the action.yml file action, err := a.parseCompositeAction(actionPath) if err != nil { return nil, fmt.Errorf("failed to parse action file: %w", err) } - // Only analyze composite actions - if action.Runs.Using != compositeUsing { - // Check if it's a valid action type - validTypes := []string{"node20", "node16", "node12", "docker", "composite"} - isValid := false - for _, validType := range validTypes { - if action.Runs.Using == validType { - isValid = true - break - } - } - if !isValid { - return nil, fmt.Errorf("invalid action runtime: %s", action.Runs.Using) - } - return []Dependency{}, nil // No dependencies for non-composite actions + // Validate and check if it's a composite action + deps, isComposite, err := a.validateAndCheckComposite(action, progressCallback) + if err != nil { + return nil, err + } + if !isComposite { + return deps, nil } - var dependencies []Dependency + // Process composite action steps + return a.processCompositeSteps(action.Runs.Steps, progressCallback) +} - // Analyze each step - for i, step := range action.Runs.Steps { - if step.Uses != "" { - // This is an action dependency - dep, err := a.analyzeActionDependency(step, i+1) - if err != nil { - // Log error but continue processing - continue - } - dependencies = append(dependencies, *dep) - } else if step.Run != "" { - // This is a shell script step - dep := a.analyzeShellScript(step, i+1) +// validateAndCheckComposite validates action type and checks if it's composite. +func (a *Analyzer) validateAndCheckComposite( + action *ActionWithComposite, + progressCallback func(current, total int, message string), +) ([]Dependency, bool, error) { + if action.Runs.Using != compositeUsing { + if err := a.validateActionType(action.Runs.Using); err != nil { + return nil, false, err + } + if progressCallback != nil { + progressCallback(1, 1, "No dependencies (non-composite action)") + } + return []Dependency{}, false, nil + } + return nil, true, nil +} + +// validateActionType checks if the action type is valid. +func (a *Analyzer) validateActionType(usingType string) error { + validTypes := []string{"node20", "node16", "node12", "docker", "composite"} + for _, validType := range validTypes { + if usingType == validType { + return nil + } + } + return fmt.Errorf("invalid action runtime: %s", usingType) +} + +// processCompositeSteps processes steps in a composite action. +func (a *Analyzer) processCompositeSteps( + steps []CompositeStep, + progressCallback func(current, total int, message string), +) ([]Dependency, error) { + var dependencies []Dependency + totalSteps := len(steps) + + for i, step := range steps { + if progressCallback != nil { + progressCallback(i, totalSteps, fmt.Sprintf("Analyzing step %d/%d", i+1, totalSteps)) + } + + dep := a.processStep(step, i+1) + if dep != nil { dependencies = append(dependencies, *dep) } } + if progressCallback != nil { + progressCallback(totalSteps, totalSteps, fmt.Sprintf("Found %d dependencies", len(dependencies))) + } + return dependencies, nil } +// processStep processes a single step and returns dependency if found. +func (a *Analyzer) processStep(step CompositeStep, stepNumber int) *Dependency { + if step.Uses != "" { + // This is an action dependency + dep, err := a.analyzeActionDependency(step, stepNumber) + if err != nil { + // Log error but continue processing + return nil + } + return dep + } else if step.Run != "" { + // This is a shell script step + return a.analyzeShellScript(step, stepNumber) + } + return nil +} + // parseCompositeAction is implemented in parser.go // analyzeActionDependency analyzes a single action dependency. diff --git a/internal/output.go b/internal/output.go index c26e43c..a9bea8e 100644 --- a/internal/output.go +++ b/internal/output.go @@ -21,6 +21,11 @@ func NewColoredOutput(quiet bool) *ColoredOutput { } } +// IsQuiet returns whether the output is in quiet mode. +func (co *ColoredOutput) IsQuiet() bool { + return co.Quiet +} + // Success prints a success message in green. func (co *ColoredOutput) Success(format string, args ...any) { if co.Quiet { diff --git a/main.go b/main.go index 64466d4..f062aef 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/schollz/progressbar/v3" "github.com/spf13/cobra" "github.com/ivuorinen/gh-action-readme/internal" @@ -510,10 +511,38 @@ func analyzeDependencies(output *internal.ColoredOutput, actionFiles []string, a totalDeps := 0 output.Bold("Dependencies found in action files:") - for _, actionFile := range actionFiles { - output.Info("\nšŸ“„ %s", actionFile) - totalDeps += analyzeActionFileDeps(output, actionFile, analyzer) + // Create progress bar for multiple files + var bar *progressbar.ProgressBar + if len(actionFiles) > 1 && !output.IsQuiet() { + bar = progressbar.NewOptions(len(actionFiles), + progressbar.OptionSetDescription("Analyzing dependencies"), + progressbar.OptionSetWidth(50), + progressbar.OptionShowCount(), + progressbar.OptionShowIts(), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "=", + SaucerHead: ">", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + })) } + + for _, actionFile := range actionFiles { + if bar == nil { + output.Info("\nšŸ“„ %s", actionFile) + } + totalDeps += analyzeActionFileDeps(output, actionFile, analyzer) + + if bar != nil { + _ = bar.Add(1) + } + } + + if bar != nil { + fmt.Println() + } + return totalDeps } @@ -586,9 +615,30 @@ func analyzeSecurityDeps( } output.Bold("Security Analysis of GitHub Action Dependencies:") + + // Create progress bar for multiple files + var bar *progressbar.ProgressBar + if len(actionFiles) > 1 && !output.IsQuiet() { + bar = progressbar.NewOptions(len(actionFiles), + progressbar.OptionSetDescription("Security analysis"), + progressbar.OptionSetWidth(50), + progressbar.OptionShowCount(), + progressbar.OptionShowIts(), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "=", + SaucerHead: ">", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + })) + } + for _, actionFile := range actionFiles { deps, err := analyzer.AnalyzeActionFile(actionFile) if err != nil { + if bar != nil { + _ = bar.Add(1) + } continue } @@ -602,7 +652,16 @@ func analyzeSecurityDeps( }{actionFile, dep}) } } + + if bar != nil { + _ = bar.Add(1) + } } + + if bar != nil { + fmt.Println() + } + return pinnedCount, floatingDeps }