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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Makefile

MODULE_PATH := github.com/edgarpsda/devsecops-kit
VERSION ?= 0.3.0
VERSION ?= 0.4.1

BINARY_NAME := devsecops

Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 |

Expand Down
50 changes: 49 additions & 1 deletion cli/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -18,6 +20,7 @@ var (
scanFailOnThreshold bool
scanOutputFormat string
scanConfigPath string
scanOpenReport bool
)

var scanCmd = &cobra.Command{
Expand All @@ -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 {
Expand Down Expand Up @@ -91,6 +95,8 @@ func runScan() error {
switch scanOutputFormat {
case "json":
return outputJSON(report)
case "html":
return outputHTML(report, scanOpenReport)
case "terminal":
fallthrough
default:
Expand All @@ -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
}
}
208 changes: 208 additions & 0 deletions cli/progress/progress.go
Original file line number Diff line number Diff line change
@@ -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 "❓"
}
}
Loading
Loading