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
485 changes: 485 additions & 0 deletions coverage.out

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module secure-push

go 1.21

require gopkg.in/yaml.v3 v3.0.1
require (
github.com/bmatcuk/doublestar/v4 v4.10.0
gopkg.in/yaml.v3 v3.0.1
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
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
34 changes: 23 additions & 11 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"

Check failure on line 9 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / test (1.22)

import 'gopkg.in/yaml.v3' is not allowed from list 'Main' (depguard)

"secure-push/internal/detectors"

"github.com/bmatcuk/doublestar/v4"
)

// CustomRule represents a user-defined rule
Expand Down Expand Up @@ -46,6 +47,19 @@
}
}

// Validate validates the configuration values
func (c *Config) Validate() error {
if c.SeverityThreshold != "" && c.SeverityThreshold != "low" &&
c.SeverityThreshold != "medium" && c.SeverityThreshold != "high" &&
c.SeverityThreshold != "critical" {
return fmt.Errorf("invalid severity_threshold: %s", c.SeverityThreshold)
}
if c.MaxFileSize <= 0 {
return fmt.Errorf("max_file_size must be positive")
}
return nil
}

func Load(configPath string) (*Config, error) {
cfg := DefaultConfig()

Expand All @@ -70,6 +84,10 @@
return nil, fmt.Errorf("failed to parse config file: %w", err)
}

if err := cfg.Validate(); err != nil {
return nil, err
}

return cfg, nil
}

Expand Down Expand Up @@ -107,22 +125,16 @@
return false
}

// matchPath handles both simple patterns and glob patterns
// matchPath handles both simple patterns and glob patterns including **
func matchPath(pattern, targetPath string) bool {
// Try direct match with filepath.Match
matched, err := filepath.Match(pattern, targetPath)
// Try doublestar for ** support
matched, err := doublestar.Match(pattern, targetPath)
if err == nil && matched {
return true
}

// Try matching just the base name
matched, err = filepath.Match(pattern, filepath.Base(targetPath))
if err == nil && matched {
return true
}

// Try matching with path.Match (for ** support)
matched, err = path.Match(pattern, targetPath)
matched, err = doublestar.Match(pattern, filepath.Base(targetPath))
if err == nil && matched {
return true
}
Expand Down
30 changes: 30 additions & 0 deletions internal/config/config_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

import (
"testing"
)

func TestConfigValidate(t *testing.T) {
tests := []struct {
name string
cfg *Config
wantErr bool
}{
{"valid config", &Config{SeverityThreshold: "medium", MaxFileSize: 1024}, false},
{"valid low threshold", &Config{SeverityThreshold: "low", MaxFileSize: 1024}, false},
{"valid high threshold", &Config{SeverityThreshold: "high", MaxFileSize: 1024}, false},
{"valid critical threshold", &Config{SeverityThreshold: "critical", MaxFileSize: 1024}, false},
{"invalid threshold", &Config{SeverityThreshold: "invalid", MaxFileSize: 1024}, true},
{"zero max file size", &Config{SeverityThreshold: "medium", MaxFileSize: 0}, true},
{"negative max file size", &Config{SeverityThreshold: "medium", MaxFileSize: -1}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
51 changes: 36 additions & 15 deletions internal/detectors/detector.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
package detectors

// Severity represents the severity level of a finding
type Severity string

const(
Critical Severity ="CRITICAL"
High Severity = "HIGH"
Medium Severity = "MEDIUM"
Low Severity = "LOW"
const (
Critical Severity = "CRITICAL"
High Severity = "HIGH"
Medium Severity = "MEDIUM"
Low Severity = "LOW"
)
type Finding struct{
Severity Severity
Rule string
File string
Line int
Message string

// Finding represents a security finding detected in code
type Finding struct {
Severity Severity
Rule string
File string
Line int
Message string
}
//detector is an interface you can add new rules without having to change anything in scanner
type Detector interface{

// Detector is an interface for implementing new detection rules
// without having to change anything in the scanner
type Detector interface {
Severity() Severity
Name() string
Detect(content string,filename string)([]Finding,error)
}
Detect(content string, filename string) ([]Finding, error)
}

// String returns the string representation of the severity
func (s Severity) String() string {
return string(s)
}

// IsHigherThan checks if this severity is higher than another
func (s Severity) IsHigherThan(other Severity) bool {
severityOrder := map[Severity]int{
Low: 1,
Medium: 2,
High: 3,
Critical: 4,
}
return severityOrder[s] > severityOrder[other]
}
60 changes: 60 additions & 0 deletions internal/detectors/detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package detectors

import (
"testing"
)

func TestSeverityString(t *testing.T) {
tests := []struct {
severity Severity
expected string
}{
{Critical, "CRITICAL"},
{High, "HIGH"},
{Medium, "MEDIUM"},
{Low, "LOW"},
}

for _, tt := range tests {
if got := tt.severity.String(); got != tt.expected {
t.Errorf("Severity.String() = %q, want %q", got, tt.expected)
}
}
}

func TestSeverityIsHigherThan(t *testing.T) {
tests := []struct {
s1 Severity
s2 Severity
expected bool
}{
{High, Medium, true},
{Medium, High, false},
{Critical, High, true},
{Low, Critical, false},
{Medium, Medium, false},
}

for _, tt := range tests {
if got := tt.s1.IsHigherThan(tt.s2); got != tt.expected {
t.Errorf("%s.IsHigherThan(%s) = %v, want %v", tt.s1, tt.s2, got, tt.expected)
}
}
}

func TestFindingStruct(t *testing.T) {
f := Finding{
Severity: High,
Rule: "test-rule",
File: "test.go",
Line: 10,
Message: "test message",
}

if f.Severity != High {
t.Error("Finding severity not set correctly")
}
if f.Rule != "test-rule" {
t.Error("Finding rule not set correctly")
}
}
5 changes: 5 additions & 0 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ func New(level Level) *Logger {
}
}

// SetOutput changes the output destination for the logger
func (l *Logger) SetOutput(w io.Writer) {
l.output = w
}

func (l *Logger) Debug(format string, args ...interface{}) {
if l.level <= Debug {
l.log("DEBUG", format, args...)
Expand Down
12 changes: 11 additions & 1 deletion internal/scanner/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ func IsBinary(data []byte) bool {
}

func GetFileExtension(path string) string {
return filepath.Ext(path)
ext := filepath.Ext(path)
// Handle hidden files like .env - return empty string
// A hidden file has no name before the extension (e.g., .env, .gitignore)
if ext != "" && ext[0] == '.' {
base := filepath.Base(path)
// If the base is just the extension (e.g., ".env"), it's a hidden file
if base == ext {
return ""
}
}
return ext
}

func IsTextFile(path string) (bool, error) {
Expand Down
53 changes: 53 additions & 0 deletions pkg/types/finding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package types

import (
"testing"
)

func TestFindingCreation(t *testing.T) {
f := Finding{
Severity: "high",
Rule: "aws-secret-key",
File: "config.go",
Line: 42,
Message: "AWS secret key detected",
}

if f.Severity != "high" {
t.Errorf("Expected severity 'high', got %q", f.Severity)
}
if f.Rule != "aws-secret-key" {
t.Errorf("Expected rule 'aws-secret-key', got %q", f.Rule)
}
if f.File != "config.go" {
t.Errorf("Expected file 'config.go', got %q", f.File)
}
if f.Line != 42 {
t.Errorf("Expected line 42, got %d", f.Line)
}
if f.Message != "AWS secret key detected" {
t.Errorf("Expected message 'AWS secret key detected', got %q", f.Message)
}
}

func TestFindingZeroValues(t *testing.T) {
f := Finding{}

if f.Severity != "" {
t.Errorf("Expected empty severity, got %q", f.Severity)
}
if f.Line != 0 {
t.Errorf("Expected line 0, got %d", f.Line)
}
}

func TestFindingAllSeverities(t *testing.T) {
severities := []string{"low", "medium", "high", "critical"}

for _, s := range severities {
f := Finding{Severity: s}
if f.Severity != s {
t.Errorf("Expected severity %q, got %q", s, f.Severity)
}
}
}
68 changes: 66 additions & 2 deletions pkg/utils/entropy.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,68 @@
package utils

// Entropy calculation utilities for detecting high-entropy secrets
// This is a placeholder for future implementation
import (
"math"
)

// CalculateEntropy calculates the Shannon entropy of a byte slice
// Used to detect high-entropy strings that may be secrets
func CalculateEntropy(data []byte) float64 {
if len(data) == 0 {
return 0
}

// Count frequency of each byte
freq := make(map[byte]int)
for _, b := range data {
freq[b]++
}

// Calculate Shannon entropy
entropy := 0.0
length := float64(len(data))
for _, count := range freq {
if count > 0 {
p := float64(count) / length
entropy -= p * math.Log2(p)
}
}

return entropy
}

// IsHighEntropy checks if data has high entropy (likely a secret)
// Returns true if entropy is above the threshold
func IsHighEntropy(data []byte, threshold float64) bool {
return CalculateEntropy(data) >= threshold
}

// IsBase64Encoded checks if a string appears to be base64 encoded
func IsBase64Encoded(s string) bool {
if len(s) < 16 {
return false
}

// Check for base64 character set
base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
for _, c := range s {
if !containsRune(base64Chars, c) {
return false
}
}

// Check for padding
if len(s)%4 == 0 {
return true
}

return false
}

func containsRune(s string, r rune) bool {
for _, c := range s {
if c == r {
return true
}
}
return false
}
Loading
Loading