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
10 changes: 8 additions & 2 deletions cmd/envguard/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package main

import "fmt"
import (
"os"

"github.com/joaooncode/envguard/internal/cli"
)

func main() {
fmt.Println("Hello, World! Bem-vindo ao envguard 🛡️")
code := cli.Run(os.Args[1:], os.Stdout, os.Stderr)
os.Exit(code)
}

94 changes: 94 additions & 0 deletions internal/cli/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cli

import (
"flag"
"fmt"
"io"
"strings"

"github.com/joaooncode/envguard/internal/reporter"
"github.com/joaooncode/envguard/internal/scanner"
)

func runCheckCommand(args []string, stdout, stderr io.Writer, scannerInstance *scanner.Scanner) int {
fs := flag.NewFlagSet("check", flag.ContinueOnError)
fs.SetOutput(stderr)

var cfg scanConfig
fs.StringVar(&cfg.path, "path", ".", "Target directory path to check")
fs.StringVar(&cfg.path, "p", ".", "Target directory path to check (shorthand)")
fs.StringVar(&cfg.format, "format", "text", "Output format: text|terminal|json")
fs.StringVar(&cfg.format, "f", "text", "Output format (shorthand)")
fs.StringVar(&cfg.severity, "severity", "all", "Minimum severity level to trigger check failure (info, warning, high, critical)")
fs.StringVar(&cfg.severity, "s", "all", "Minimum severity level (shorthand)")
fs.BoolVar(&cfg.noColor, "no-color", false, "Disable ANSI color escape codes in terminal output")

if err := fs.Parse(args); err != nil {
return ExitCodeUsageError
}

// Validate format
var repFormat reporter.Format
switch strings.ToLower(strings.TrimSpace(cfg.format)) {
case "text", "terminal", "":
repFormat = reporter.FormatTerminal
case "json":
repFormat = reporter.FormatJSON
default:
fmt.Fprintf(stderr, "Error: invalid format %q. Supported formats: text, terminal, json\n", cfg.format)
return ExitCodeUsageError
}

// Validate severity
minSev, ok := parseSeverity(cfg.severity)
if !ok {
fmt.Fprintf(stderr, "Error: invalid severity %q. Supported levels: info, warning, high, critical, all\n", cfg.severity)
return ExitCodeUsageError
}

if scannerInstance == nil {
scannerInstance = scanner.DefaultScanner
}

result, err := scannerInstance.Scan(cfg.path)
if err != nil {
fmt.Fprintf(stderr, "Error: failed to scan directory %s: %v\n", cfg.path, err)
return ExitCodeInternalError
}

result.Version = Version

if minSev != "" {
minRank := severityRank(minSev)
filtered := make([]scanner.Finding, 0, len(result.Findings))
for _, f := range result.Findings {
if severityRank(f.Severity) >= minRank {
filtered = append(filtered, f)
}
}
result.Findings = filtered
result.CalculateSummary()
}

rep, err := reporter.New(
repFormat,
reporter.WithNoColor(cfg.noColor),
reporter.WithVersion(Version),
reporter.WithPretty(true),
)
if err != nil {
fmt.Fprintf(stderr, "Error: failed to initialize reporter: %v\n", err)
return ExitCodeInternalError
}

if err := rep.Render(result, stdout); err != nil {
fmt.Fprintf(stderr, "Error: failed to render check results: %v\n", err)
return ExitCodeInternalError
}

if !result.Summary.Passed {
return ExitCodeFindingsFound
}

return ExitCodeSuccess
}
111 changes: 111 additions & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cli

import (
"fmt"
"io"
"strings"

"github.com/joaooncode/envguard/internal/scanner"
)

// App encapsulates the CLI execution environment and dependencies.
type App struct {
stdout io.Writer
stderr io.Writer
scanner *scanner.Scanner
}

// Option configures an App instance.
type Option func(*App)

// WithScanner sets a custom Scanner instance (useful for testing).
func WithScanner(s *scanner.Scanner) Option {
return func(a *App) {
a.scanner = s
}
}

// New creates a new App configured with stdout and stderr writers.
func New(stdout, stderr io.Writer, opts ...Option) *App {
app := &App{
stdout: stdout,
stderr: stderr,
}
for _, opt := range opts {
opt(app)
}
return app
}

// Run executes the command line application with the provided arguments and returns an exit code.
func Run(args []string, stdout, stderr io.Writer, opts ...Option) int {
app := New(stdout, stderr, opts...)
return app.Run(args)
}

// Run parses arguments, dispatches commands, and returns deterministic exit codes.
func (a *App) Run(args []string) int {
if len(args) == 0 {
a.printHelp()
return ExitCodeSuccess
}

cmd := strings.ToLower(strings.TrimSpace(args[0]))

switch cmd {
case "version", "-v", "--version", "-version":
fmt.Fprintf(a.stdout, "%s\n", VersionString())
return ExitCodeSuccess

case "help", "-h", "--help", "-help":
a.printHelp()
return ExitCodeSuccess

case "scan":
return runScanCommand(args[1:], a.stdout, a.stderr, a.scanner)

case "check":
return runCheckCommand(args[1:], a.stdout, a.stderr, a.scanner)

default:
fmt.Fprintf(a.stderr, "Error: unknown command or flag %q\n\n", args[0])
a.printHelpTo(a.stderr)
return ExitCodeUsageError
}
}

func (a *App) printHelp() {
a.printHelpTo(a.stdout)
}

func (a *App) printHelpTo(w io.Writer) {
help := `🛡️ envguard - Prevent .env files and environment secrets from reaching Git

Usage:
envguard <command> [flags]

Available Commands:
scan Scan a directory for unprotected environment files
check Run verification optimized for CI/CD pipelines
version Show current envguard version
help Show help for envguard commands

Flags:
-h, --help Show help for envguard
-v, --version Show envguard version

Scan & Check Flags:
-p, --path Target directory path to scan (default: ".")
-f, --format Output format: text|terminal|json (default: "text")
-s, --severity Minimum severity level: info|warning|high|critical|all (default: "all")
--no-color Disable ANSI color escape codes in terminal output

Examples:
envguard scan
envguard scan --path ./my-project --format json
envguard scan --severity warning
envguard check --path . --severity high
envguard version
`
fmt.Fprint(w, help)
}
Loading