131 lines
3.4 KiB
Go
131 lines
3.4 KiB
Go
package shhh
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type compiledRule struct {
|
|
name string
|
|
regex *regexp.Regexp
|
|
replacement string
|
|
severity Severity
|
|
tags []string
|
|
}
|
|
|
|
type matchRecord struct {
|
|
value string
|
|
}
|
|
|
|
func (r *compiledRule) apply(in string) (string, []matchRecord) {
|
|
indices := r.regex.FindAllStringSubmatchIndex(in, -1)
|
|
if len(indices) == 0 {
|
|
return in, nil
|
|
}
|
|
|
|
var builder strings.Builder
|
|
builder.Grow(len(in))
|
|
|
|
matches := make([]matchRecord, 0, len(indices))
|
|
last := 0
|
|
for _, loc := range indices {
|
|
start, end := loc[0], loc[1]
|
|
builder.WriteString(in[last:start])
|
|
replaced := r.regex.ExpandString(nil, r.replacement, in, loc)
|
|
builder.Write(replaced)
|
|
matches = append(matches, matchRecord{value: in[start:end]})
|
|
last = end
|
|
}
|
|
builder.WriteString(in[last:])
|
|
|
|
return builder.String(), matches
|
|
}
|
|
|
|
func buildDefaultRuleConfigs(placeholder string) []RuleConfig {
|
|
if placeholder == "" {
|
|
placeholder = "[REDACTED]"
|
|
}
|
|
return []RuleConfig{
|
|
{
|
|
Name: "bearer-token",
|
|
Pattern: `(?i)(authorization\s*:\s*bearer\s+)([A-Za-z0-9\-._~+/]+=*)`,
|
|
ReplacementTemplate: "$1" + placeholder,
|
|
Severity: SeverityMedium,
|
|
Tags: []string{"token", "http"},
|
|
},
|
|
{
|
|
Name: "api-key",
|
|
Pattern: `(?i)((?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?)([A-Za-z0-9\-._~+/]{8,})(["']?)`,
|
|
ReplacementTemplate: "$1" + placeholder + "$3",
|
|
Severity: SeverityHigh,
|
|
Tags: []string{"credentials"},
|
|
},
|
|
{
|
|
Name: "openai-secret",
|
|
Pattern: `(sk-[A-Za-z0-9]{20,})`,
|
|
ReplacementTemplate: placeholder,
|
|
Severity: SeverityHigh,
|
|
Tags: []string{"llm", "api"},
|
|
},
|
|
{
|
|
Name: "oauth-refresh-token",
|
|
Pattern: `(?i)(refresh_token"?\s*[:=]\s*["']?)([A-Za-z0-9\-._~+/]{8,})(["']?)`,
|
|
ReplacementTemplate: "$1" + placeholder + "$3",
|
|
Severity: SeverityMedium,
|
|
Tags: []string{"oauth"},
|
|
},
|
|
{
|
|
Name: "private-key-block",
|
|
Pattern: `(?s)(-----BEGIN [^-]+ PRIVATE KEY-----)[^-]+(-----END [^-]+ PRIVATE KEY-----)`,
|
|
ReplacementTemplate: "$1\n" + placeholder + "\n$2",
|
|
Severity: SeverityHigh,
|
|
Tags: []string{"pem", "key"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func compileRules(cfg Config, placeholder string) ([]*compiledRule, error) {
|
|
configs := make([]RuleConfig, 0)
|
|
if !cfg.DisableDefaultRules {
|
|
configs = append(configs, buildDefaultRuleConfigs(placeholder)...)
|
|
}
|
|
configs = append(configs, cfg.CustomRules...)
|
|
|
|
rules := make([]*compiledRule, 0, len(configs))
|
|
for _, rc := range configs {
|
|
if rc.Name == "" || rc.Pattern == "" {
|
|
continue
|
|
}
|
|
replacement := rc.ReplacementTemplate
|
|
if replacement == "" {
|
|
replacement = placeholder
|
|
}
|
|
re, err := regexp.Compile(rc.Pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
compiled := &compiledRule{
|
|
name: rc.Name,
|
|
replacement: replacement,
|
|
regex: re,
|
|
severity: rc.Severity,
|
|
tags: append([]string(nil), rc.Tags...),
|
|
}
|
|
rules = append(rules, compiled)
|
|
}
|
|
|
|
sort.SliceStable(rules, func(i, j int) bool {
|
|
return rules[i].name < rules[j].name
|
|
})
|
|
|
|
return rules, nil
|
|
}
|
|
|
|
func hashSecret(value string) string {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return base64.RawStdEncoding.EncodeToString(sum[:])
|
|
}
|