go-devops/devkit/scan_secrets.go

110 lines
2.4 KiB
Go
Raw Permalink Normal View History

package devkit
import (
"context"
"encoding/csv"
"os/exec"
"strconv"
"strings"
)
var scanSecretsRunner = runGitleaksDetect
// ScanSecrets runs gitleaks against the supplied directory and parses the CSV report.
func ScanSecrets(dir string) ([]Finding, error) {
output, err := scanSecretsRunner(dir)
findings, parseErr := parseGitleaksCSV(output)
if parseErr != nil {
return nil, parseErr
}
if err != nil && len(findings) == 0 {
return nil, err
}
return findings, nil
}
func runGitleaksDetect(dir string) ([]byte, error) {
bin, err := exec.LookPath("gitleaks")
if err != nil {
return nil, err
}
cmd := exec.CommandContext(context.Background(), bin,
"detect",
"--no-banner",
"--no-color",
"--no-git",
"--source", dir,
"--report-format", "csv",
"--report-path", "-",
)
return cmd.Output()
}
func parseGitleaksCSV(data []byte) ([]Finding, error) {
if len(data) == 0 {
return nil, nil
}
reader := csv.NewReader(strings.NewReader(string(data)))
reader.FieldsPerRecord = -1
rows, err := reader.ReadAll()
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
header := make(map[string]int, len(rows[0]))
for idx, name := range rows[0] {
header[normalizeCSVHeader(name)] = idx
}
var findings []Finding
for _, row := range rows[1:] {
finding := Finding{
Path: csvField(row, header, "file", "path"),
Line: csvIntField(row, header, "startline", "line"),
Column: csvIntField(row, header, "startcolumn", "column"),
Rule: csvField(row, header, "ruleid", "rule", "name"),
Snippet: csvField(row, header, "match", "secret", "description", "message"),
}
if finding.Snippet == "" {
finding.Snippet = csvField(row, header, "filename")
}
findings = append(findings, finding)
}
return findings, nil
}
func normalizeCSVHeader(name string) string {
return strings.ToLower(strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(name, "_", ""), " ", "")))
}
func csvField(row []string, header map[string]int, names ...string) string {
for _, name := range names {
if idx, ok := header[name]; ok && idx < len(row) {
return strings.TrimSpace(row[idx])
}
}
return ""
}
func csvIntField(row []string, header map[string]int, names ...string) int {
value := csvField(row, header, names...)
if value == "" {
return 0
}
n, err := strconv.Atoi(value)
if err != nil {
return 0
}
return n
}