220 lines
4.4 KiB
Go
220 lines
4.4 KiB
Go
// ©AngelaMos | 2026
|
|
// terminal.go
|
|
|
|
package reporter
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/CarterPerez-dev/portia/internal/ui"
|
|
"github.com/CarterPerez-dev/portia/pkg/types"
|
|
)
|
|
|
|
type Terminal struct{}
|
|
|
|
type errWriter struct {
|
|
w io.Writer
|
|
err error
|
|
}
|
|
|
|
func (ew *errWriter) printf(format string, args ...any) {
|
|
if ew.err != nil {
|
|
return
|
|
}
|
|
_, ew.err = fmt.Fprintf(ew.w, format, args...)
|
|
}
|
|
|
|
func (ew *errWriter) println(args ...any) {
|
|
if ew.err != nil {
|
|
return
|
|
}
|
|
_, ew.err = fmt.Fprintln(ew.w, args...)
|
|
}
|
|
|
|
func (t *Terminal) Report(
|
|
w io.Writer, result *types.ScanResult,
|
|
) error {
|
|
ew := &errWriter{w: w}
|
|
|
|
if len(result.Findings) == 0 {
|
|
ew.printf("\n%s %s\n\n",
|
|
ui.Check,
|
|
ui.Green("No secrets detected"),
|
|
)
|
|
t.printSummary(ew, result)
|
|
return ew.err
|
|
}
|
|
|
|
ew.printf("\n%s %s\n\n",
|
|
ui.Warning,
|
|
ui.RedBold(fmt.Sprintf(
|
|
"%d secret(s) detected",
|
|
len(result.Findings),
|
|
)),
|
|
)
|
|
|
|
sorted := make([]types.Finding, len(result.Findings))
|
|
copy(sorted, result.Findings)
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return sorted[i].Severity.Rank() < sorted[j].Severity.Rank()
|
|
})
|
|
|
|
for i, f := range sorted {
|
|
if i > 0 {
|
|
ew.println()
|
|
}
|
|
t.printFinding(ew, &f)
|
|
}
|
|
|
|
t.printSummary(ew, result)
|
|
return ew.err
|
|
}
|
|
|
|
func (t *Terminal) printFinding(
|
|
ew *errWriter, f *types.Finding,
|
|
) {
|
|
sevColor := severityColor(f.Severity)
|
|
|
|
ew.printf("%s %s - %s\n",
|
|
ui.HiGreen(ui.Diamond),
|
|
sevColor(f.Severity.String()),
|
|
ui.HiWhite(f.Description),
|
|
)
|
|
|
|
ew.printf(" %s %s %s\n",
|
|
ui.Green(ui.Arrow), ui.HiBlue("Rule: "), ui.Cyan(f.RuleID))
|
|
ew.printf(" %s %s %s\n",
|
|
ui.Green(ui.Arrow), ui.HiBlue("File: "),
|
|
ui.WhiteBold(fmt.Sprintf("%s:%d", f.FilePath, f.LineNumber)))
|
|
ew.printf(
|
|
" %s %s %s\n",
|
|
ui.Green(
|
|
ui.Arrow,
|
|
),
|
|
ui.HiBlue("Secret: "),
|
|
ui.YellowBold(maskSecret(f.Secret)),
|
|
)
|
|
|
|
if f.Entropy > 0 {
|
|
ew.printf(" %s %s %s\n",
|
|
ui.Green(ui.Arrow), ui.HiBlue("Entropy: "),
|
|
ui.HiGreenUnderline(fmt.Sprintf("%.2f bits", f.Entropy)))
|
|
}
|
|
|
|
if f.CommitSHA != "" {
|
|
ew.printf(" %s %s %s\n",
|
|
ui.Green(ui.Arrow), ui.HiBlue("Commit: "),
|
|
ui.Magenta(truncateSHA(f.CommitSHA)))
|
|
}
|
|
if f.Author != "" {
|
|
ew.printf(" %s %s %s\n",
|
|
ui.Green(ui.Arrow), ui.HiBlue("Author: "), f.Author)
|
|
}
|
|
|
|
if f.HIBPStatus == types.HIBPBreached {
|
|
ew.printf(" %s Breached: %s (%d occurrences)\n",
|
|
ui.Warning,
|
|
ui.RedBold("FOUND IN BREACHES"),
|
|
f.BreachCount,
|
|
)
|
|
} else if f.HIBPStatus == types.HIBPClean {
|
|
ew.printf(" %s HIBP: %s\n",
|
|
ui.Shield, ui.Green("not found in breaches"))
|
|
}
|
|
|
|
if f.LineContent != "" {
|
|
ew.printf(" %s %s %s\n",
|
|
ui.Green(ui.Arrow), ui.HiBlue("Context: "),
|
|
ui.RedItalic(truncateLine(f.LineContent, 100)))
|
|
}
|
|
}
|
|
|
|
func (t *Terminal) printSummary(
|
|
ew *errWriter, result *types.ScanResult,
|
|
) {
|
|
ew.printf("\n%s\n", ui.HRule(50))
|
|
ew.printf(" %s %s\n",
|
|
ui.HiBlueBold("Scan completed in"),
|
|
ui.GreenItalic(result.Duration.Round(1e6).String()))
|
|
ew.printf(" %s %s\n",
|
|
ui.HiBlueBold("Files: "),
|
|
ui.HiCyanItalic(fmt.Sprintf("%d", result.TotalFiles)))
|
|
ew.printf(" %s %s\n",
|
|
ui.HiBlueBold("Rules: "),
|
|
ui.HiCyanItalic(fmt.Sprintf("%d", result.TotalRules)))
|
|
ew.printf(" %s %s\n",
|
|
ui.HiBlueBold("Findings:"),
|
|
findingsColor(len(result.Findings)))
|
|
|
|
if result.HIBPChecked > 0 {
|
|
ew.printf(" HIBP: %d checked, %s breached\n",
|
|
result.HIBPChecked,
|
|
breachColor(result.HIBPBreached))
|
|
}
|
|
ew.printf("%s\n\n", ui.HRule(50))
|
|
}
|
|
|
|
func severityColor(
|
|
sev types.Severity,
|
|
) func(a ...interface{}) string {
|
|
switch sev {
|
|
case types.SeverityCritical:
|
|
return ui.RedBold
|
|
case types.SeverityHigh:
|
|
return ui.Red
|
|
case types.SeverityMedium:
|
|
return ui.Yellow
|
|
case types.SeverityLow:
|
|
return ui.Blue
|
|
default:
|
|
return ui.Blue
|
|
}
|
|
}
|
|
|
|
func findingsColor(count int) string {
|
|
s := fmt.Sprintf("%d", count)
|
|
if count > 0 {
|
|
return ui.RedBold(s)
|
|
}
|
|
return ui.Green(s)
|
|
}
|
|
|
|
func breachColor(count int) string {
|
|
s := fmt.Sprintf("%d", count)
|
|
if count > 0 {
|
|
return ui.RedBold(s)
|
|
}
|
|
return ui.Green(s)
|
|
}
|
|
|
|
func maskSecret(secret string) string {
|
|
if len(secret) <= 8 {
|
|
return strings.Repeat("*", len(secret))
|
|
}
|
|
visible := 4
|
|
if len(secret) > 20 {
|
|
visible = 6
|
|
}
|
|
return secret[:visible] +
|
|
strings.Repeat("*", len(secret)-visible*2) +
|
|
secret[len(secret)-visible:]
|
|
}
|
|
|
|
func truncateSHA(sha string) string {
|
|
if len(sha) > 8 {
|
|
return sha[:8]
|
|
}
|
|
return sha
|
|
}
|
|
|
|
func truncateLine(line string, maxLen int) string {
|
|
line = strings.TrimSpace(line)
|
|
if len(line) <= maxLen {
|
|
return line
|
|
}
|
|
return line[:maxLen-3] + "..."
|
|
}
|