-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Markdown output format #4650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Markdown output format #4650
Changes from all commits
550fbc2
1344e5f
f748149
37926ab
2ed3217
3224270
4c2a879
8af9543
bf80503
904e72f
c531c2b
cfa382d
34b82a0
c9733ee
d306116
65af12a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package output | ||
|
|
||
| import "fmt" | ||
|
|
||
| // extractFileLine walks over the metadata map created by structToMap and | ||
| // extracts the file name and line number when present. This logic is shared by | ||
| // multiple printers so that they stay consistent. | ||
| func extractFileLine(meta map[string]map[string]any) (file string, hasFile bool, line string, lineNum int, hasLine bool) { | ||
| file = "n/a" | ||
| line = "n/a" | ||
|
|
||
| for _, data := range meta { | ||
| for k, v := range data { | ||
| if k == "line" { | ||
| if l, ok := v.(float64); ok { | ||
| lineNum = int(l) | ||
| line = fmt.Sprintf("%d", lineNum) | ||
| hasLine = true | ||
| } | ||
| } | ||
| if k == "file" { | ||
| if filename, ok := v.(string); ok { | ||
| file = filename | ||
| hasFile = true | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| package output | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "sort" | ||
| "strings" | ||
| "sync" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/context" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| ) | ||
|
|
||
| // MarkdownPrinter renders TruffleHog findings into a Markdown document with | ||
| // dedicated tables for verified and unverified secrets. It buffers the rows | ||
| // while scanning and flushes the final report when Close is invoked. | ||
| type MarkdownPrinter struct { | ||
| mu sync.Mutex | ||
|
|
||
| out io.Writer | ||
|
|
||
| verified []markdownRow | ||
| unverified []markdownRow | ||
|
|
||
| seenVerified map[string]struct{} | ||
| seenUnverified map[string]struct{} | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // markdownRow represents a single table entry in the Markdown report. | ||
| type markdownRow struct { | ||
| Detector string | ||
| File string | ||
| Line string | ||
| Redacted string | ||
|
|
||
| lineNum int | ||
| hasLine bool | ||
| } | ||
|
|
||
| func (r markdownRow) key() string { | ||
| return fmt.Sprintf("%s|%s|%s|%s", r.Detector, r.File, r.Line, r.Redacted) | ||
| } | ||
|
|
||
| // NewMarkdownPrinter builds a MarkdownPrinter that writes to out. When out is | ||
| // nil, stdout is used. | ||
| func NewMarkdownPrinter(out io.Writer) *MarkdownPrinter { | ||
| if out == nil { | ||
| out = os.Stdout | ||
| } | ||
| return &MarkdownPrinter{ | ||
| out: out, | ||
| seenVerified: make(map[string]struct{}), | ||
| seenUnverified: make(map[string]struct{}), | ||
| } | ||
| } | ||
|
|
||
| // Print collects each result so the final Markdown doc can include per-section | ||
| // counts and tables before the buffered results are rendered in Close. | ||
| func (p *MarkdownPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata) error { | ||
| meta, err := structToMap(r.SourceMetadata.Data) | ||
| if err != nil { | ||
| return fmt.Errorf("could not marshal result: %w", err) | ||
| } | ||
|
|
||
| file, hasFile, line, lineNum, hasLine := extractFileLine(meta) | ||
|
|
||
| if !hasFile { | ||
| file = "n/a" | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant fallback already handled by helper functionLow Severity The check |
||
|
|
||
| row := markdownRow{ | ||
| Detector: sanitize(r.DetectorType.String()), | ||
| File: sanitize(file), | ||
| Line: sanitize(line), | ||
| Redacted: sanitize(r.Redacted), | ||
| lineNum: lineNum, | ||
| hasLine: hasLine, | ||
| } | ||
|
|
||
| p.mu.Lock() | ||
| defer p.mu.Unlock() | ||
|
|
||
| key := row.key() | ||
|
|
||
| if r.Verified { | ||
| if _, ok := p.seenVerified[key]; ok { | ||
| return nil | ||
| } | ||
| p.seenVerified[key] = struct{}{} | ||
| p.verified = append(p.verified, row) | ||
| } else { | ||
| if _, ok := p.seenUnverified[key]; ok { | ||
| return nil | ||
| } | ||
| p.seenUnverified[key] = struct{}{} | ||
| p.unverified = append(p.unverified, row) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Close renders the buffered findings to Markdown. Close should be invoked by | ||
| // the output manager once scanning finishes. | ||
| func (p *MarkdownPrinter) Close() error { | ||
| p.mu.Lock() | ||
| defer p.mu.Unlock() | ||
|
|
||
| doc := renderMarkdown(p.verified, p.unverified) | ||
| if doc == "" { | ||
| return nil | ||
| } | ||
| if _, err := fmt.Fprint(p.out, doc); err != nil { | ||
| return fmt.Errorf("write markdown: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // renderMarkdown mirrors templates/trufflehog_report.py by emitting a title, | ||
| // optional sections for verified/unverified findings, and per-section counts. | ||
| func renderMarkdown(verified, unverified []markdownRow) string { | ||
| if len(verified) == 0 && len(unverified) == 0 { | ||
| return "" | ||
| } | ||
|
|
||
| var buf bytes.Buffer | ||
| buf.WriteString("# TruffleHog Findings\n\n") | ||
| writeSection := func(title string, rows []markdownRow) { | ||
| if len(rows) == 0 { | ||
| return | ||
| } | ||
| sort.SliceStable(rows, func(i, j int) bool { | ||
| if rows[i].Detector != rows[j].Detector { | ||
| return rows[i].Detector < rows[j].Detector | ||
| } | ||
| if rows[i].File != rows[j].File { | ||
| return rows[i].File < rows[j].File | ||
| } | ||
| if rows[i].hasLine != rows[j].hasLine { | ||
| return rows[i].hasLine | ||
| } | ||
| if rows[i].lineNum != rows[j].lineNum { | ||
| return rows[i].lineNum < rows[j].lineNum | ||
| } | ||
| return rows[i].Line < rows[j].Line | ||
| }) | ||
|
|
||
| fmt.Fprintf(&buf, "## %s (%d)\n", title, len(rows)) | ||
| buf.WriteString("| Detector | File | Line | Redacted |\n") | ||
| buf.WriteString("| --- | --- | --- | --- |\n") | ||
| for _, row := range rows { | ||
| fmt.Fprintf(&buf, "| %s | %s | %s | %s |\n", row.Detector, row.File, row.Line, row.Redacted) | ||
| } | ||
| buf.WriteString("\n") | ||
| } | ||
|
|
||
| writeSection("Verified Findings", verified) | ||
| writeSection("Unverified Findings", unverified) | ||
|
|
||
| return strings.TrimRight(buf.String(), "\n") + "\n" | ||
| } | ||
|
|
||
| var sanitizer = strings.NewReplacer("\r", " ", "\n", " ", "|", "\\|", " ", " ") | ||
|
|
||
| func sanitize(value string) string { | ||
| value = strings.TrimSpace(value) | ||
| if value == "" { | ||
| return "n/a" | ||
| } | ||
| return sanitizer.Replace(value) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,14 @@ func GetTrufflehogConfiguration() truffleCmdModel { | |
| Placeholder: "false", | ||
| } | ||
|
|
||
| markdownOutput := textinputs.InputConfig{ | ||
| Label: "Markdown output", | ||
| Key: "markdown", | ||
| Required: false, | ||
| Help: "Output results to Markdown", | ||
| Placeholder: "false", | ||
| } | ||
|
|
||
| excludeDetectors := textinputs.InputConfig{ | ||
| Label: "Exclude detectors", | ||
| Key: "exclude_detectors", | ||
|
|
@@ -53,13 +61,17 @@ func GetTrufflehogConfiguration() truffleCmdModel { | |
| Placeholder: strconv.Itoa(runtime.NumCPU()), | ||
| } | ||
|
|
||
| return truffleCmdModel{textinputs.New([]textinputs.InputConfig{jsonOutput, verification, verifiedResults, excludeDetectors, concurrency}).SetSkip(true)} | ||
| return truffleCmdModel{textinputs.New([]textinputs.InputConfig{jsonOutput, markdownOutput, verification, verifiedResults, excludeDetectors, concurrency}).SetSkip(true)} | ||
| } | ||
|
|
||
| func (m truffleCmdModel) Cmd() string { | ||
| var command []string | ||
| inputs := m.GetInputs() | ||
|
|
||
| if isTrue(inputs["markdown"].Value) { | ||
| command = append(command, "--markdown") | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TUI allows conflicting output format flags simultaneouslyLow Severity The TUI configuration allows users to enable both Additional Locations (1)There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's common pattern for all printers. If neccesary, should be fixed in separate PR. |
||
| if isTrue(inputs["json"].Value) { | ||
| command = append(command, "--json") | ||
| } | ||
|
|
@@ -86,7 +98,7 @@ func (m truffleCmdModel) Cmd() string { | |
|
|
||
| func (m truffleCmdModel) Summary() string { | ||
| summary := strings.Builder{} | ||
| keys := []string{"no-verification", "only-verified", "json", "exclude_detectors", "concurrency"} | ||
| keys := []string{"no-verification", "only-verified", "json", "markdown", "exclude_detectors", "concurrency"} | ||
|
|
||
| inputs := m.GetInputs() | ||
| labels := m.GetLabels() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.