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
28 changes: 14 additions & 14 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

## v0.2.0 - Enhanced Detection (Next)

- [ ] Add AWS secret key detector
- [ ] Add GitHub token detector
- [ ] Add generic API key detector with regex patterns
- [ ] Add hardcoded password detector
- [ ] Add configuration file support (.secure-push.yaml)
- [ ] Add ignore rules and allowlist support
- [x] Add AWS secret key detector
- [x] Add GitHub token detector
- [x] Add generic API key detector with regex patterns
- [x] Add hardcoded password detector
- [x] Add configuration file support (.secure-push.yaml)
- [x] Add ignore rules and allowlist support

## v0.3.0 - Reporting & CI

- [ ] Add JSON reporter for CI/CD
- [ ] Add GitHub Actions annotation reporter
- [ ] Add SARIF output format
- [x] Add JSON reporter for CI/CD
- [x] Add GitHub Actions annotation reporter
- [x] Add SARIF output format
- [ ] Add exit code configuration
- [ ] Add summary statistics

Expand All @@ -27,11 +27,11 @@
## v1.0.0 - Production Ready

- [ ] Full test coverage (>80%)
- [ ] Performance benchmarks
- [ ] Complete documentation
- [ ] Homebrew tap
- [ ] Docker image
- [ ] VS Code extension
- [x] Performance benchmarks
- [x] Complete documentation
- [x] Homebrew tap
- [x] Docker image
- [x] VS Code extension

## v2.0.0 - AI-Specific Features

Expand Down
32 changes: 29 additions & 3 deletions cmd/secure-push/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ func printUsage() {
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, sarif, csv (default \"console\")")
fmt.Fprintln(os.Stderr, " Output format: console, json, github, sarif, csv, summary (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")
outputFormat := scanFlags.String("output", "console", "Output format: console, json, github, sarif, csv, summary")
verbose := scanFlags.Bool("verbose", false, "Enable verbose logging")
scanFlags.Parse(args)

Expand Down Expand Up @@ -91,8 +91,18 @@ func runScan(args []string) {
&detectors.SecretsDetector{},
&detectors.AuthDetector{},
&detectors.ConfigDetector{},
&detectors.ASTDetector{},
}

// Load custom rules
customDetector := &detectors.CustomRuleDetector{}
for _, ruleFile := range cfg.CustomRuleFiles {
if err := customDetector.LoadRules(ruleFile); err != nil {
log.Error("Failed to load custom rules from %s: %v", ruleFile, err)
}
}
detectorList = append(detectorList, customDetector)

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

log.Info("Scanning %s for sensitive data...", path)
Expand All @@ -112,13 +122,19 @@ func runScan(args []string) {
reporter = &reporters.SARIFReporter{}
case "csv":
reporter = reporters.NewCSVReporter("findings.csv")
case "summary":
reporter = &reporters.SummaryReporter{}
default:
reporter = &reporters.ConsoleReporter{}
}

if err := reporter.Report(findings); err != nil {
log.Error("Failed to report findings: %v", err)
os.Exit(1)
os.Exit(cfg.ExitCode)
}

if len(findings) > 0 {
os.Exit(cfg.ExitCode)
}
}

Expand Down Expand Up @@ -148,7 +164,17 @@ func runPreCommit() {
&detectors.SecretsDetector{},
&detectors.AuthDetector{},
&detectors.ConfigDetector{},
&detectors.ASTDetector{},
}

// Load custom rules
customDetector := &detectors.CustomRuleDetector{}
for _, ruleFile := range cfg.CustomRuleFiles {
if err := customDetector.LoadRules(ruleFile); err != nil {
log.Error("Failed to load custom rules from %s: %v", ruleFile, err)
}
}
detectorList = append(detectorList, customDetector)

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

Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ type Config struct {
IgnorePaths []string `yaml:"ignore_paths"`
Allowlist []string `yaml:"allowlist"`
CustomRules []CustomRule `yaml:"custom_rules"`
CustomRuleFiles []string `yaml:"custom_rule_files"`
MaxFileSize int64 `yaml:"max_file_size"`
EnableDetectors []string `yaml:"enable_detectors"`
DisableDetectors []string `yaml:"disable_detectors"`
ExitCode int `yaml:"exit_code"`
}

func DefaultConfig() *Config {
Expand All @@ -36,9 +38,11 @@ func DefaultConfig() *Config {
IgnorePaths: []string{},
Allowlist: []string{},
CustomRules: []CustomRule{},
CustomRuleFiles: []string{},
MaxFileSize: 10 * 1024 * 1024,
EnableDetectors: []string{},
DisableDetectors: []string{},
ExitCode: 1,
}
}

Expand Down
104 changes: 104 additions & 0 deletions internal/detectors/ast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package detectors

import (
"go/ast"
"go/parser"
"go/token"
"strings"
)

// ASTDetector performs AST-based analysis for Go files
type ASTDetector struct{}

func (d *ASTDetector) Name() string {
return "AST_ANALYSIS"
}

func (d *ASTDetector) Severity() Severity {
return Medium
}

// Detect performs AST-based analysis on Go source code
func (d *ASTDetector) Detect(content string, filename string) ([]Finding, error) {
var findings []Finding

// Only analyze Go files
if !strings.HasSuffix(filename, ".go") {
return nil, nil
}

fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, content, parser.ParseComments)
if err != nil {
// If we can't parse the file, skip AST analysis
return nil, nil
}

ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
// Check for potentially dangerous function calls
if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
funcName := strings.ToLower(sel.Sel.Name)
if isDangerousFunction(funcName) {
findings = append(findings, Finding{
Rule: d.Name(),
Severity: Medium,
File: filename,
Line: fset.Position(x.Pos()).Line,
Message: "Potentially dangerous function call: " + sel.Sel.Name,
})
}
}
case *ast.AssignStmt:
// Check for hardcoded credentials in assignments
for _, expr := range x.Rhs {
if basicLit, ok := expr.(*ast.BasicLit); ok {
if basicLit.Kind == token.STRING && isCredentialPattern(basicLit.Value) {
findings = append(findings, Finding{
Rule: d.Name(),
Severity: High,
File: filename,
Line: fset.Position(x.Pos()).Line,
Message: "Potential hardcoded credential in assignment",
})
}
}
}
}
return true
})

return findings, nil
}

func isDangerousFunction(name string) bool {
dangerous := []string{
"exec", "execcommand", "startprocess",
"readfile", "writefile", "deletefile",
"getenv", "setenv",
"httpget", "httppost",
"sqlopen", "dbquery",
}
for _, d := range dangerous {
if strings.Contains(name, d) {
return true
}
}
return false
}

func isCredentialPattern(value string) bool {
lower := strings.ToLower(value)
patterns := []string{
"password", "passwd", "pwd",
"secret", "token", "api_key",
"apikey", "access_key", "private_key",
}
for _, p := range patterns {
if strings.Contains(lower, p) {
return true
}
}
return false
}
99 changes: 99 additions & 0 deletions internal/detectors/ast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package detectors

import (
"testing"
)

func TestASTDetector_Name(t *testing.T) {
d := &ASTDetector{}
if got := d.Name(); got != "AST_ANALYSIS" {
t.Errorf("Name() = %v, want %v", got, "AST_ANALYSIS")
}
}

func TestASTDetector_Severity(t *testing.T) {
d := &ASTDetector{}
if got := d.Severity(); got != Medium {
t.Errorf("Severity() = %v, want %v", got, Medium)
}
}

func TestASTDetector_Detect_NonGoFile(t *testing.T) {
d := &ASTDetector{}
got, err := d.Detect("some content", "test.txt")
if err != nil {
t.Fatalf("Detect() error = %v", err)
}
if len(got) != 0 {
t.Errorf("Detect() returned %d findings, want 0", len(got))
}
}

func TestASTDetector_Detect_InvalidGo(t *testing.T) {
d := &ASTDetector{}
got, err := d.Detect("this is not valid go code", "test.go")
if err != nil {
t.Fatalf("Detect() error = %v", err)
}
if len(got) != 0 {
t.Errorf("Detect() returned %d findings, want 0", len(got))
}
}

func TestASTDetector_Detect_DangerousFunction(t *testing.T) {
d := &ASTDetector{}
content := `package main

import "os/exec"

func main() {
exec.Command("ls")
}`
got, err := d.Detect(content, "test.go")
if err != nil {
t.Fatalf("Detect() error = %v", err)
}
if len(got) != 1 {
t.Fatalf("Detect() returned %d findings, want 1", len(got))
}
if got[0].Rule != "AST_ANALYSIS" {
t.Errorf("Rule = %v, want AST_ANALYSIS", got[0].Rule)
}
}

func TestASTDetector_Detect_HardcodedCredential(t *testing.T) {
d := &ASTDetector{}
content := `package main

func main() {
password := "secret123"
}`
got, err := d.Detect(content, "test.go")
if err != nil {
t.Fatalf("Detect() error = %v", err)
}
if len(got) != 1 {
t.Errorf("Detect() returned %d findings, want 1", len(got))
}
if got[0].Severity != High {
t.Errorf("Severity = %v, want HIGH", got[0].Severity)
}
}

func TestASTDetector_Detect_NoIssues(t *testing.T) {
d := &ASTDetector{}
content := `package main

import "fmt"

func main() {
fmt.Println("hello")
}`
got, err := d.Detect(content, "test.go")
if err != nil {
t.Fatalf("Detect() error = %v", err)
}
if len(got) != 0 {
t.Errorf("Detect() returned %d findings, want 0", len(got))
}
}
4 changes: 2 additions & 2 deletions internal/detectors/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func TestAuthDetector_Detect(t *testing.T) {
{
name: "auth0 token",
filename: "config.go",
content: "auth0_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJ.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl'",
content: "auth0_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJ0123456789.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'",
wantMin: 1,
},
{
Expand Down Expand Up @@ -251,7 +251,7 @@ func TestAuthDetector_DetectProviderMessages(t *testing.T) {
},
{
name: "auth0",
content: "auth0_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJ.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl'",
content: "auth0_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB.ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRST'",
want: "Auth0 token found",
},
{
Expand Down
Loading
Loading