package report import ( "fmt" "math" "strings" "github.com/charmbracelet/lipgloss" "github.com/zricethezav/gitleaks/v8/sources" ) // Finding contains a whole bunch of information about a secret finding. // Plenty of real estate in this bad boy so fillerup as needed. type Finding struct { // Rule is the name of the rule that was matched RuleID string Description string StartLine int EndLine int StartColumn int EndColumn int Line string `json:"-"` Match string // Captured secret Secret string // File is the name of the file containing the finding File string SymlinkFile string Commit string Link string `json:",omitempty"` // Entropy is the shannon entropy of Value Entropy float32 Author string Email string Date string Message string Tags []string // unique identifier Fingerprint string // Fragment used for multi-part rule checking, CEL filtering, // and eventually ML validation Fragment *sources.Fragment `json:",omitempty"` // TODO keeping private for now to during experimental phase requiredFindings []*RequiredFinding } type RequiredFinding struct { // contains a subset of the Finding fields // only used for reporting RuleID string StartLine int EndLine int StartColumn int EndColumn int Line string `json:"-"` Match string Secret string } func (f *Finding) AddRequiredFindings(afs []*RequiredFinding) { if f.requiredFindings == nil { f.requiredFindings = make([]*RequiredFinding, 0) } f.requiredFindings = append(f.requiredFindings, afs...) } // Redact removes sensitive information from a finding. func (f *Finding) Redact(percent uint) { secret := maskSecret(f.Secret, percent) if percent >= 100 { secret = "REDACTED" } f.Line = strings.ReplaceAll(f.Line, f.Secret, secret) f.Match = strings.ReplaceAll(f.Match, f.Secret, secret) f.Secret = secret } func maskSecret(secret string, percent uint) string { if percent > 100 { percent = 100 } len := float64(len(secret)) if len <= 0 { return secret } prc := float64(100 - percent) lth := int64(math.RoundToEven(len * prc / float64(100))) return secret[:lth] + "..." } func (f *Finding) PrintRequiredFindings() { if len(f.requiredFindings) == 0 { return } fmt.Printf("%-12s ", "Required:") // Create orange style for secrets orangeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#bf9478")) for i, aux := range f.requiredFindings { auxSecret := strings.TrimSpace(aux.Secret) // Truncate long secrets for readability if len(auxSecret) > 40 { auxSecret = auxSecret[:37] + "..." } // Format: rule-id:line:secret if i == 0 { fmt.Printf("%s:%d:%s\n", aux.RuleID, aux.StartLine, orangeStyle.Render(auxSecret)) } else { fmt.Printf("%-12s %s:%d:%s\n", "", aux.RuleID, aux.StartLine, orangeStyle.Render(auxSecret)) } } }