diff --git a/CHANGELOG.md b/CHANGELOG.md index 575b143..2c72dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,41 @@ and this project adheres (loosely) to [Semantic Versioning](https://semver.org/s --- +## [0.4.1] - 2025-11-22 + +### Added + +- **HTML Report Generation** 📊: + - Beautiful, self-contained HTML reports with embedded CSS + - Interactive charts using Chart.js (severity distribution, findings by tool) + - Summary dashboard with statistics per tool + - Detailed findings organized by severity and tool + - Export findings as JSON from the report + - Responsive design (mobile/tablet/desktop friendly) + - Print-friendly styling (exportable to PDF) + - Cross-platform browser opening (macOS, Linux, Windows) + +- **Progress Bars & Real-time UI** ⏳: + - Visual progress bars for each scanner `[████████░░]` + - Status icons (✅ completed, ❌ error, ⏳ running) + - Execution time tracking per scanner + - Thread-safe progress updates + - Clear screen display with real-time feedback + +- **New CLI Flags**: + - `--format=html`: Generate HTML report (default: terminal) + - `--open`: Auto-open HTML report in default browser + - Enhanced `--format` flag with three output options: terminal, json, html + +### Changed + +- **README.md** updated for v0.4.1: + - Updated key features to show HTML and progress bar capabilities + - Added HTML report examples and command usage + - Updated roadmap with v0.4.1 released status + +--- + ## [0.4.0] - 2025-11-22 ### Added diff --git a/Makefile b/Makefile index c06197d..63f1d85 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile MODULE_PATH := github.com/edgarpsda/devsecops-kit -VERSION ?= 0.3.0 +VERSION ?= 0.4.1 BINARY_NAME := devsecops diff --git a/README.md b/README.md index 8102d86..f01af24 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ DevSecOps Kit detects your project (Node.js or Go), generates a hardened GitHub Designed for small teams, freelancers, and agencies who need practical DevSecOps without complexity. -## 🚀 Key Features (v0.4.0) +## 🚀 Key Features (v0.4.1) ### 🔍 Automatic Project Detection Works out-of-the-box with: @@ -55,22 +55,26 @@ Get detailed, actionable feedback directly on your code: - References to security best practices - Automatic comment placement on changed files only -### 🔍 Local Security Scanning 🆕 +### 🔍 Local Security Scanning Run security scans locally before pushing: ```bash -devsecops scan # Run all enabled scanners -devsecops scan --tool=semgrep # Run specific tool -devsecops scan --format=json # JSON output for CI integration -devsecops scan --fail-on-threshold # Exit code 1 if thresholds exceeded +devsecops scan # Run all enabled scanners +devsecops scan --tool=semgrep # Run specific tool +devsecops scan --format=terminal # Rich terminal output (default) +devsecops scan --format=json # JSON output for CI integration +devsecops scan --format=html # Beautiful HTML report +devsecops scan --format=html --open # Auto-open in browser +devsecops scan --fail-on-threshold # Exit code 1 if thresholds exceeded ``` **Features:** - Parallel execution of Semgrep, Gitleaks, and Trivy -- Rich color-coded terminal output +- **Rich color-coded terminal output** with progress bars +- **Beautiful HTML reports** with interactive charts - Respects `security-config.yml` thresholds and exclusions - Docker image scanning when Dockerfile detected -- JSON output format for integrations +- Multiple output formats (terminal, JSON, HTML) ### 🪝 Git Hooks Integration 🆕 Automatically run security scans before commits and pushes: @@ -269,7 +273,7 @@ security-reports/ |---------|----------|--------| | **0.3.0** | Config-driven fail gates, exclude paths, Docker detection, image scanning, inline PR comments | ✅ **Released** | | **0.4.0** | Local CLI scans (`devsecops scan`), git hooks, rich terminal UI, YAML config parsing | ✅ **Released** | -| **0.4.1** | HTML report generation, progress bars, performance optimization | 🚧 In Progress | +| **0.4.1** | HTML report generation, progress bars, real-time UI feedback | ✅ **Released** | | **0.5.0** | Python/Java detection, expanded framework support | 📋 Planned | | **1.0.0** | Full onboarding UX, multi-CI support (GitLab, Jenkins) | 📋 Planned | diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 5812ff9..13dc50c 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -4,7 +4,9 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "path/filepath" + "runtime" "github.com/spf13/cobra" "github.com/edgarpsda/devsecops-kit/cli/config" @@ -18,6 +20,7 @@ var ( scanFailOnThreshold bool scanOutputFormat string scanConfigPath string + scanOpenReport bool ) var scanCmd = &cobra.Command{ @@ -34,8 +37,9 @@ func init() { scanCmd.Flags().StringVar(&scanTool, "tool", "", "Specific tool to run (semgrep, gitleaks, trivy)") scanCmd.Flags().BoolVar(&scanFailOnThreshold, "fail-on-threshold", false, "Exit with code 1 if findings exceed thresholds") - scanCmd.Flags().StringVar(&scanOutputFormat, "format", "terminal", "Output format: terminal, json") + scanCmd.Flags().StringVar(&scanOutputFormat, "format", "terminal", "Output format: terminal, json, html") scanCmd.Flags().StringVar(&scanConfigPath, "config", "security-config.yml", "Path to security-config.yml") + scanCmd.Flags().BoolVar(&scanOpenReport, "open", false, "Auto-open HTML report in browser (requires --format=html)") } func runScan() error { @@ -91,6 +95,8 @@ func runScan() error { switch scanOutputFormat { case "json": return outputJSON(report) + case "html": + return outputHTML(report, scanOpenReport) case "terminal": fallthrough default: @@ -116,3 +122,45 @@ func outputJSON(report *scanners.ScanReport) error { fmt.Println(string(data)) return nil } + +// outputHTML generates and optionally opens an HTML report +func outputHTML(report *scanners.ScanReport, openBrowser bool) error { + htmlReporter := reporters.NewHTMLReporter(report) + + reportPath := "security-report.html" + if err := htmlReporter.WriteFile(reportPath); err != nil { + return err + } + + fmt.Printf("✅ HTML report generated: %s\n", reportPath) + + if openBrowser { + // Try to open in browser + absPath, err := filepath.Abs(reportPath) + if err == nil { + fileURL := fmt.Sprintf("file://%s", absPath) + openInBrowser(fileURL) + fmt.Printf("🌐 Opening report in browser...\n") + } + } + + return nil +} + +// openInBrowser opens a URL in the default browser +func openInBrowser(url string) { + var cmd *exec.Cmd + + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("cmd", "/c", "start", url) + } + + if cmd != nil { + _ = cmd.Start() // Ignore errors, browser might not be available + } +} diff --git a/cli/progress/progress.go b/cli/progress/progress.go new file mode 100644 index 0000000..7231eab --- /dev/null +++ b/cli/progress/progress.go @@ -0,0 +1,208 @@ +package progress + +import ( + "fmt" + "sync" + "time" +) + +// Scanner represents a scanner being tracked +type Scanner struct { + Name string + Status string // "pending", "running", "completed", "error" + Progress int // 0-100 + Error error + StartTime time.Time + EndTime time.Time +} + +// Tracker manages progress of multiple scanners +type Tracker struct { + mu sync.RWMutex + scanners map[string]*Scanner + done chan bool + ticker *time.Ticker +} + +// NewTracker creates a new progress tracker +func NewTracker() *Tracker { + return &Tracker{ + scanners: make(map[string]*Scanner), + done: make(chan bool), + ticker: time.NewTicker(100 * time.Millisecond), + } +} + +// AddScanner adds a scanner to track +func (t *Tracker) AddScanner(name string) { + t.mu.Lock() + defer t.mu.Unlock() + + t.scanners[name] = &Scanner{ + Name: name, + Status: "pending", + Progress: 0, + } +} + +// StartScanner marks a scanner as running +func (t *Tracker) StartScanner(name string) { + t.mu.Lock() + defer t.mu.Unlock() + + if scanner, ok := t.scanners[name]; ok { + scanner.Status = "running" + scanner.Progress = 0 + scanner.StartTime = time.Now() + } +} + +// UpdateProgress updates the progress of a scanner +func (t *Tracker) UpdateProgress(name string, progress int) { + t.mu.Lock() + defer t.mu.Unlock() + + if scanner, ok := t.scanners[name]; ok { + if progress > 100 { + progress = 100 + } + scanner.Progress = progress + } +} + +// CompleteScanner marks a scanner as completed +func (t *Tracker) CompleteScanner(name string) { + t.mu.Lock() + defer t.mu.Unlock() + + if scanner, ok := t.scanners[name]; ok { + scanner.Status = "completed" + scanner.Progress = 100 + scanner.EndTime = time.Now() + } +} + +// ErrorScanner marks a scanner with an error +func (t *Tracker) ErrorScanner(name string, err error) { + t.mu.Lock() + defer t.mu.Unlock() + + if scanner, ok := t.scanners[name]; ok { + scanner.Status = "error" + scanner.Error = err + scanner.EndTime = time.Now() + } +} + +// Start begins displaying progress +func (t *Tracker) Start() { + go t.displayProgress() +} + +// Stop stops the progress display +func (t *Tracker) Stop() { + t.ticker.Stop() + t.done <- true + t.displayFinal() +} + +// displayProgress shows real-time progress +func (t *Tracker) displayProgress() { + for { + select { + case <-t.done: + return + case <-t.ticker.C: + t.mu.RLock() + scanners := t.scanners + t.mu.RUnlock() + + // Clear screen and display progress + fmt.Print("\033[H\033[2J") // Clear screen + fmt.Println("🔍 Running security scans...\n") + + for _, scanner := range scanners { + bar := t.progressBar(scanner.Progress) + status := t.statusIcon(scanner.Status) + + duration := "" + if scanner.Status == "completed" || scanner.Status == "error" { + duration = fmt.Sprintf(" (%.1fs)", scanner.EndTime.Sub(scanner.StartTime).Seconds()) + } + + fmt.Printf("%s %-15s %s %d%%%s\n", status, scanner.Name, bar, scanner.Progress, duration) + + if scanner.Status == "error" && scanner.Error != nil { + fmt.Printf(" ❌ Error: %v\n", scanner.Error) + } + } + + fmt.Println() + } + } +} + +// displayFinal displays the final state without progress bars +func (t *Tracker) displayFinal() { + t.mu.RLock() + defer t.mu.RUnlock() + + fmt.Print("\033[H\033[2J") // Clear screen + fmt.Println("🔍 Running security scans...\n") + + for _, scanner := range t.scanners { + status := "" + switch scanner.Status { + case "completed": + status = "✅" + case "error": + status = "❌" + case "pending": + status = "⏭️" + default: + status = "⏳" + } + + duration := "" + if scanner.Status == "completed" || scanner.Status == "error" { + duration = fmt.Sprintf(" (%.1fs)", scanner.EndTime.Sub(scanner.StartTime).Seconds()) + } + + fmt.Printf("%s %-15s [██████████] 100%%%s\n", status, scanner.Name, duration) + } + + fmt.Println() +} + +// progressBar returns a visual progress bar +func (t *Tracker) progressBar(progress int) string { + filled := (progress / 10) + empty := 10 - filled + + bar := "[" + for i := 0; i < filled; i++ { + bar += "█" + } + for i := 0; i < empty; i++ { + bar += "░" + } + bar += "]" + + return bar +} + +// statusIcon returns the status emoji +func (t *Tracker) statusIcon(status string) string { + switch status { + case "completed": + return "✅" + case "error": + return "❌" + case "running": + return "⏳" + case "pending": + return "⏭️" + default: + return "❓" + } +} diff --git a/cli/reporters/html.go b/cli/reporters/html.go new file mode 100644 index 0000000..b1488d7 --- /dev/null +++ b/cli/reporters/html.go @@ -0,0 +1,612 @@ +package reporters + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/edgarpsda/devsecops-kit/cli/scanners" +) + +// HTMLReporter generates HTML security reports +type HTMLReporter struct { + report *scanners.ScanReport +} + +// NewHTMLReporter creates a new HTML reporter +func NewHTMLReporter(report *scanners.ScanReport) *HTMLReporter { + return &HTMLReporter{report: report} +} + +// WriteFile generates an HTML report and writes it to a file +func (hr *HTMLReporter) WriteFile(filePath string) error { + html := hr.generateHTML() + if err := os.WriteFile(filePath, []byte(html), 0o644); err != nil { + return fmt.Errorf("failed to write HTML report: %w", err) + } + return nil +} + +// generateHTML creates the complete HTML report +func (hr *HTMLReporter) generateHTML() string { + return fmt.Sprintf(` + + + + + DevSecOps Kit Security Report + + + + +
+
+

🔐 DevSecOps Kit Security Report

+

Generated: %s

+
+ %s Status: %s +
+ %s +
+ +
+ %s +
+ +
+ %s +
+ +
+ +

Detailed Findings

+ %s +
+ + +
+ + + +`, + hr.report.Timestamp, + hr.getStatusClass(), + hr.getStatusIcon(), + hr.report.Status, + hr.getBlockingCountHTML(), + hr.generateSummaryCards(), + hr.generateCharts(), + hr.generateFindings(), + hr.getReportDataJSON(), + ) +} + +// getStatusIcon returns the emoji for the status +func (hr *HTMLReporter) getStatusIcon() string { + if hr.report.Status == "PASS" { + return "✅" + } + return "❌" +} + +// getStatusClass returns the CSS class for the status +func (hr *HTMLReporter) getStatusClass() string { + if hr.report.Status == "PASS" { + return "pass" + } + return "fail" +} + +// getBlockingCountHTML returns HTML for blocking count if applicable +func (hr *HTMLReporter) getBlockingCountHTML() string { + if hr.report.BlockingCount > 0 { + return fmt.Sprintf(`

⚠️ %d issue(s) exceed configured thresholds

`, hr.report.BlockingCount) + } + return "" +} + +// generateSummaryCards creates the summary statistics cards +func (hr *HTMLReporter) generateSummaryCards() string { + html := "" + + // Overall summary + html += `
+

Overall Summary

+
+ Total Findings + ` + fmt.Sprintf("%d", len(hr.report.AllFindings)) + ` +
+
+ Status + ` + hr.report.Status + ` +
+
+ Blocking Issues + ` + fmt.Sprintf("%d", hr.report.BlockingCount) + ` +
+
` + + // Per-tool summaries + tools := []string{"semgrep", "gitleaks", "trivy"} + for _, tool := range tools { + if result, ok := hr.report.Results[tool]; ok && result.Status == "success" { + html += fmt.Sprintf(`
+

%s

+
+ Total + %d +
+
+ CRITICAL + %d +
+
+ HIGH + %d +
+
+ MEDIUM + %d +
+
+ LOW + %d +
+
`, + tool, + result.Summary.Total, + result.Summary.Critical, + result.Summary.High, + result.Summary.Medium, + result.Summary.Low, + ) + } + } + + return html +} + +// generateCharts creates the chart sections +func (hr *HTMLReporter) generateCharts() string { + html := "" + + // Severity breakdown chart + if len(hr.report.AllFindings) > 0 { + html += `
+

Severity Distribution

+ +
+ +
+

Findings by Tool

+ +
` + + html += `` + } + + return html +} + +// generateFindings creates the detailed findings section +func (hr *HTMLReporter) generateFindings() string { + if len(hr.report.AllFindings) == 0 { + return `
✅ No security findings detected!
` + } + + // Group findings by tool + findingsByTool := make(map[string][]scanners.Finding) + for _, finding := range hr.report.AllFindings { + findingsByTool[finding.Tool] = append(findingsByTool[finding.Tool], finding) + } + + html := "" + tools := []string{"semgrep", "gitleaks", "trivy"} + + for _, tool := range tools { + findings, ok := findingsByTool[tool] + if !ok || len(findings) == 0 { + continue + } + + html += fmt.Sprintf(`
+
🔍 %s (%d findings)
`, tool, len(findings)) + + for _, finding := range findings { + severityClass := "low" + switch finding.Severity { + case "CRITICAL": + severityClass = "critical" + case "HIGH": + severityClass = "high" + case "MEDIUM": + severityClass = "medium" + } + + lineHTML := "" + if finding.Line > 0 { + lineHTML = fmt.Sprintf(`
📍 Line %d
`, finding.Line) + } + + html += fmt.Sprintf(`
+
+
%s
+
%s
+
+ %s +
%s
+
Rule: %s
+
`, severityClass, finding.File, severityClass, finding.Severity, lineHTML, finding.Message, finding.RuleID) + } + + html += `
` + } + + return html +} + +// getReportDataJSON returns the report as JSON for download +func (hr *HTMLReporter) getReportDataJSON() string { + data, _ := json.Marshal(hr.report) + return string(data) +} diff --git a/devsecops b/devsecops index 58681e3..9ea23e0 100755 Binary files a/devsecops and b/devsecops differ diff --git a/security-config.yml b/security-config.yml index 9f07bc1..ddda537 100644 --- a/security-config.yml +++ b/security-config.yml @@ -26,7 +26,7 @@ fail_on: gitleaks: 0 # Fail if ANY secrets detected (recommended: 0) semgrep: 10 # Fail if 10+ Semgrep findings trivy_critical: 0 # Fail if ANY critical vulnerabilities - trivy_high: 0 # Fail if 5+ high severity vulnerabilities + trivy_high: 5 # Fail if 5+ high severity vulnerabilities trivy_medium: -1 # Disabled by default (set to number to enable) trivy_low: -1 # Disabled by default diff --git a/security-report.html b/security-report.html new file mode 100644 index 0000000..f21b8f6 --- /dev/null +++ b/security-report.html @@ -0,0 +1,368 @@ + + + + + + DevSecOps Kit Security Report + + + + +
+
+

🔐 DevSecOps Kit Security Report

+

Generated: 2025-11-22T22:12:36-08:00

+
+ ✅ Status: PASS +
+ +
+ +
+
+

Overall Summary

+
+ Total Findings + 0 +
+
+ Status + PASS +
+
+ Blocking Issues + 0 +
+
+

trivy

+
+ Total + 0 +
+
+ CRITICAL + 0 +
+
+ HIGH + 0 +
+
+ MEDIUM + 0 +
+
+ LOW + 0 +
+
+
+ +
+ +
+ +
+ +

Detailed Findings

+
✅ No security findings detected!
+
+ + +
+ + + + \ No newline at end of file