Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a66d80f
fix: update go.mod module name and dependencies for compatibility
StephenJarso Jun 13, 2026
3b98185
fix: add placeholder content to pkg/types/finding.go
StephenJarso Jun 13, 2026
a900b47
fix: add placeholder content to pkg/utils/entropy.go
StephenJarso Jun 13, 2026
5e3b2ee
fix: add placeholder content to pkg/utils/regex.go
StephenJarso Jun 13, 2026
ccde4b3
fix: improve binary file detection in scanner
StephenJarso Jun 13, 2026
32ae3d2
fix: remove unused severityOrder variable in console reporter
StephenJarso Jun 13, 2026
1cc777f
feat: update config struct to match documentation
StephenJarso Jun 13, 2026
65a005b
test: update config tests for new config struct
StephenJarso Jun 13, 2026
10b718d
fix: make config detector filenames case-insensitive
StephenJarso Jun 13, 2026
5187a34
test: fix config detector edge case test expectations
StephenJarso Jun 13, 2026
4986b56
test: fix integration test expectations
StephenJarso Jun 13, 2026
c4d12d0
feat: add pre-commit and install commands to CLI
StephenJarso Jun 13, 2026
69fa106
docs: add parallel scanning constants for future use
StephenJarso Jun 13, 2026
5e2d6d6
test: add unit tests for logger package
StephenJarso Jun 13, 2026
6226203
test: add unit tests for reporters package
StephenJarso Jun 13, 2026
6d98d2b
test: add unit tests for scanner walk utilities
StephenJarso Jun 13, 2026
f7d7400
test: add comprehensive tests for file detection utilities
StephenJarso Jun 13, 2026
d21116b
docs: update detectors guide with complete documentation
StephenJarso Jun 13, 2026
13199e3
docs: enhance configuration guide with complete options
StephenJarso Jun 13, 2026
8bc40ee
feat: add CSV reporter for machine-readable output
StephenJarso Jun 13, 2026
d867165
docs: update reporters guide to include CSV reporter
StephenJarso Jun 13, 2026
abe9d76
feat: add CSV output format and implement getStagedFiles for pre-commit
StephenJarso Jun 13, 2026
19f63f0
fix: fix reporter tests to avoid os.Exit calls
StephenJarso Jun 13, 2026
d3bc431
test: add CSV reporter test
StephenJarso Jun 13, 2026
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
167 changes: 156 additions & 11 deletions cmd/secure-push/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,75 @@
"flag"
"fmt"
"os"
"os/exec"
"strings"

"secure-push/internal/config"

Check failure on line 10 in cmd/secure-push/main.go

View workflow job for this annotation

GitHub Actions / test (1.22)

import 'secure-push/internal/config' is not allowed from list 'Main' (depguard)
"secure-push/internal/detectors"

Check failure on line 11 in cmd/secure-push/main.go

View workflow job for this annotation

GitHub Actions / test (1.22)

import 'secure-push/internal/detectors' is not allowed from list 'Main' (depguard)
"secure-push/internal/logger"

Check failure on line 12 in cmd/secure-push/main.go

View workflow job for this annotation

GitHub Actions / test (1.22)

import 'secure-push/internal/logger' is not allowed from list 'Main' (depguard)
"secure-push/internal/reporters"

Check failure on line 13 in cmd/secure-push/main.go

View workflow job for this annotation

GitHub Actions / test (1.22)

import 'secure-push/internal/reporters' is not allowed from list 'Main' (depguard)
"secure-push/internal/scanner"

Check failure on line 14 in cmd/secure-push/main.go

View workflow job for this annotation

GitHub Actions / test (1.22)

import 'secure-push/internal/scanner' is not allowed from list 'Main' (depguard)
)

func main() {
configPath := flag.String("config", "", "Path to configuration file")
outputFormat := flag.String("output", "console", "Output format: console, json, github")
verbose := flag.Bool("verbose", false, "Enable verbose logging")
flag.Parse()

args := flag.Args()
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "Usage: secure-push [options] <path>")
fmt.Fprintln(os.Stderr, "Options:")
flag.PrintDefaults()
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}

path := args[0]
subcommand := os.Args[1]

switch subcommand {
case "scan":
runScan(os.Args[2:])
case "pre-commit":
runPreCommit()
case "install":
runInstall()
case "version":
fmt.Println("secure-push version 0.1.0")
case "help", "-h", "--help":
printUsage()
default:
// Treat as scan with path argument
runScan(os.Args[1:])
}
}

func printUsage() {
fmt.Fprintln(os.Stderr, "Secure Push - Security scanner for your codebase")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Usage:")
fmt.Fprintln(os.Stderr, " secure-push scan [options] <path> Scan a directory for security issues")
fmt.Fprintln(os.Stderr, " secure-push pre-commit Run in pre-commit mode")
fmt.Fprintln(os.Stderr, " secure-push install Install pre-commit hook")
fmt.Fprintln(os.Stderr, " secure-push version Show version")
fmt.Fprintln(os.Stderr, " secure-push help Show help")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Options:")
fmt.Fprintln(os.Stderr, " -config string")
fmt.Fprintln(os.Stderr, " Path to configuration file")
fmt.Fprintln(os.Stderr, " -output string")
fmt.Fprintln(os.Stderr, " Output format: console, json, github (default \"console\")")
fmt.Fprintln(os.Stderr, " -verbose")
fmt.Fprintln(os.Stderr, " Enable verbose logging")
}

func runScan(args []string) {
scanFlags := flag.NewFlagSet("scan", flag.ExitOnError)
configPath := scanFlags.String("config", "", "Path to configuration file")
outputFormat := scanFlags.String("output", "console", "Output format: console, json, github")
verbose := scanFlags.Bool("verbose", false, "Enable verbose logging")
scanFlags.Parse(args)

Check failure on line 66 in cmd/secure-push/main.go

View workflow job for this annotation

GitHub Actions / test (1.22)

Error return value of `scanFlags.Parse` is not checked (errcheck)

remainingArgs := scanFlags.Args()
if len(remainingArgs) == 0 {
fmt.Fprintln(os.Stderr, "Error: path argument required")
printUsage()
os.Exit(1)
}

path := remainingArgs[0]

logLevel := logger.Info
if *verbose {
Expand Down Expand Up @@ -62,6 +108,8 @@
reporter = &reporters.JSONReporter{}
case "github":
reporter = &reporters.GitHubReporter{}
case "csv":
reporter = reporters.NewCSVReporter("findings.csv")
default:
reporter = &reporters.ConsoleReporter{}
}
Expand All @@ -71,3 +119,100 @@
os.Exit(1)
}
}

func runPreCommit() {
log := logger.New(logger.Info)

// Get staged files from git
files, err := getStagedFiles()
if err != nil {
log.Error("Failed to get staged files: %v", err)
os.Exit(1)
}

if len(files) == 0 {
log.Info("No staged files to scan")
return
}

cfg, err := config.Load("")
if err != nil {
log.Error("Failed to load config: %v", err)
os.Exit(1)
}

detectorList := []detectors.Detector{
&detectors.EnvDetector{},
&detectors.SecretsDetector{},
&detectors.AuthDetector{},
&detectors.ConfigDetector{},
}

s := scanner.New(detectorList, cfg, log)

var allFindings []detectors.Finding
for _, file := range files {
findings, err := s.ScanFile(file)
if err != nil {
log.Error("Error scanning file %s: %v", file, err)
continue
}
allFindings = append(allFindings, findings...)
}

if len(allFindings) > 0 {
fmt.Fprintln(os.Stderr, "🚫 Commit blocked by Secure Push")
fmt.Fprintln(os.Stderr, "")
for _, f := range allFindings {
fmt.Fprintf(os.Stderr, "%s [%s] %s:%d\n", f.Rule, f.Severity, f.File, f.Line)
fmt.Fprintf(os.Stderr, " %s\n", f.Message)
}
os.Exit(1)
}

fmt.Println("✓ No security issues found in staged files")
}

func runInstall() {
hookPath := ".git/hooks/pre-commit"

// Check if .git directory exists
if _, err := os.Stat(".git"); os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, "Error: not a git repository")
os.Exit(1)
}

// Create hooks directory if it doesn't exist
if err := os.MkdirAll(".git/hooks", 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating hooks directory: %v\n", err)
os.Exit(1)
}

// Check if hook already exists
if _, err := os.Stat(hookPath); err == nil {
fmt.Fprintln(os.Stderr, "Warning: pre-commit hook already exists, skipping")
return
}

// Create the hook script
hookContent := `#!/bin/sh
# Secure Push pre-commit hook
secure-push pre-commit
`
if err := os.WriteFile(hookPath, []byte(hookContent), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating pre-commit hook: %v\n", err)
os.Exit(1)
}

fmt.Println("✓ Pre-commit hook installed successfully")
}

func getStagedFiles() ([]string, error) {
cmd := exec.Command("git", "diff", "--cached", "--name-only")
out, err := cmd.Output()
if err != nil {
return nil, err
}
files := strings.Fields(string(out))
return files, nil
}
62 changes: 62 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ allowlist:
custom_rules:
- path: rules/custom-secrets.yaml
severity: CRITICAL

max_file_size: 10485760

enable_detectors:
- ENV_FILE
- SECRETS
```

## Configuration Options
Expand Down Expand Up @@ -79,6 +85,33 @@ custom_rules:
severity: CRITICAL
```

### max_file_size

Maximum file size in bytes to scan. Files larger than this are skipped.

```yaml
max_file_size: 10485760 # 10MB
```

### enable_detectors

List of specific detectors to enable. If set, only these detectors will run.

```yaml
enable_detectors:
- ENV_FILE
- SECRETS
```

### disable_detectors

List of specific detectors to disable.

```yaml
disable_detectors:
- CONFIG_FILE
```

## Environment Variables

| Variable | Description | Default |
Expand All @@ -93,3 +126,32 @@ custom_rules:
2. Environment variables
3. `.secure-push.yaml` config file
4. Default values

## Common Configuration Patterns

### Minimal Configuration

```yaml
severity_threshold: HIGH
ignore_paths:
- vendor/
- node_modules/
```

### Development Environment

```yaml
severity_threshold: MEDIUM
ignore_rules:
- GENERIC_API_KEY
allowlist:
- testdata/
```

### CI/CD Configuration

```yaml
severity_threshold: CRITICAL
ignore_paths:
- "**/*.min.js"
- "**/*.map"
9 changes: 9 additions & 0 deletions docs/detectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type Detector interface {
| AWS_SECRET_KEY | CRITICAL | Detects AWS secret keys |
| GENERIC_API_KEY | HIGH | Detects generic API keys |
| HARDCODED_PASSWORD | CRITICAL | Detects hardcoded passwords |
| AUTH_CREDENTIALS | CRITICAL | Detects various auth credentials (GitHub tokens, SSH keys, etc.) |
| CONFIG_FILE | HIGH | Detects config files that may contain sensitive data |

## Creating a Custom Detector

Expand Down Expand Up @@ -109,3 +111,10 @@ func TestGenericAPIKeyDetector(t *testing.T) {
}
}
```

## Detector Best Practices

- Keep patterns specific to reduce false positives
- Test with real-world code samples
- Document the patterns used in comments
- Consider performance for large files
42 changes: 42 additions & 0 deletions docs/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Reporter interface {
| Console | Human-readable terminal output | Local development |
| JSON | Machine-readable JSON | CI/CD pipelines |
| GitHub | GitHub Actions annotation format | GitHub Actions |
| CSV | Comma-separated values | Data analysis, spreadsheets |

## Creating a Custom Reporter

Expand Down Expand Up @@ -77,3 +78,44 @@ func (r *CSVReporter) Report(findings []detectors.Finding) error {
return nil
}
```

## Reporter Output Examples

### Console Output

```
✗ Found 2 potential security issues:

1. 🔴 [CRITICAL] .env:1
Rule: ENV_FILE
.env file should not be committed
```

### JSON Output

```json
{
"total": 2,
"findings": [
{
"severity": "CRITICAL",
"rule": "ENV_FILE",
"file": ".env",
"line": 1,
"message": ".env file should not be committed"
}
]
}
```

### GitHub Output

```
::error file=.env,line=1,title=ENV_FILE [CRITICAL]::.env file should not be committed
```

### CSV Output

```csv
Rule,Severity,File,Line,Message
ENV_FILE,CRITICAL,.env,1,.env file should not be committed
7 changes: 2 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
module github.com/stephenjarso/secure-push
module secure-push

go 1.21

require (
golang.org/x/sync v0.20.0
gopkg.in/yaml.v3 v3.0.1
)
require gopkg.in/yaml.v3 v3.0.1
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
Loading
Loading