Skip to content

Commit 550fbc2

Browse files
committed
Markdown output format
1 parent a1d9973 commit 550fbc2

File tree

5 files changed

+186
-5
lines changed

5 files changed

+186
-5
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,20 @@ Timestamp: 2022-06-16 10:17:40 -0700 PDT
212212
trufflehog github --org=trufflesecurity --results=verified
213213
```
214214

215-
## 3: Scan a GitHub Repo for only verified secrets and get JSON output
215+
## 3: Scan a GitHub Repo for only verified secrets and get structured output
216216

217-
Command:
217+
JSON:
218218

219219
```bash
220220
trufflehog git https://github.com/trufflesecurity/test_keys --results=verified --json
221221
```
222222

223+
Or Markdown report that you can drop into documentation or ticketing system:
224+
225+
```bash
226+
trufflehog git https://github.com/trufflesecurity/test_keys --results=verified --markdown > findings.md
227+
```
228+
223229
Expected output:
224230

225231
```
@@ -448,6 +454,7 @@ Flags:
448454
--log-level=0 Logging verbosity on a scale of 0 (info) to 5 (trace). Can be disabled with "-1".
449455
--profile Enables profiling and sets a pprof and fgprof server on :18066.
450456
-j, --json Output in JSON format.
457+
--markdown Output in Markdown format.
451458
--json-legacy Use the pre-v3.0 JSON format. Only works with git, gitlab, and github sources.
452459
--github-actions Output in GitHub Actions format.
453460
--concurrency=20 Number of concurrent workers.

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ trufflehog filesystem --config=$PWD/generic.yml $PWD
1111
1212
# to filter so that _only_ generic credentials are logged:
1313
trufflehog filesystem --config=$PWD/generic.yml --json --no-verification $PWD | awk '/generic-api-key/{print $0}'
14+
15+
# capture a Markdown report:
16+
trufflehog filesystem --config=$PWD/generic.yml --markdown --no-verification $PWD > findings.md
1417
```

main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ var (
5353
profile = cli.Flag("profile", "Enables profiling and sets a pprof and fgprof server on :18066.").Bool()
5454
localDev = cli.Flag("local-dev", "Hidden feature to disable overseer for local dev.").Hidden().Bool()
5555
jsonOut = cli.Flag("json", "Output in JSON format.").Short('j').Bool()
56+
markdownOut = cli.Flag("markdown", "Output in Markdown format.").Bool()
5657
jsonLegacy = cli.Flag("json-legacy", "Use the pre-v3.0 JSON format. Only works with git, gitlab, and github sources.").Bool()
5758
gitHubActionsFormat = cli.Flag("github-actions", "Output in GitHub Actions format.").Bool()
5859
concurrency = cli.Flag("concurrency", "Number of concurrent workers.").Default(strconv.Itoa(runtime.NumCPU())).Int()
@@ -508,13 +509,15 @@ func run(state overseer.State) {
508509
printer = new(output.LegacyJSONPrinter)
509510
case *jsonOut:
510511
printer = new(output.JSONPrinter)
512+
case *markdownOut:
513+
printer = output.NewMarkdownPrinter(nil)
511514
case *gitHubActionsFormat:
512515
printer = new(output.GitHubActionsPrinter)
513516
default:
514517
printer = new(output.PlainPrinter)
515518
}
516519

517-
if !*jsonLegacy && !*jsonOut {
520+
if !*jsonLegacy && !*jsonOut && !*markdownOut {
518521
fmt.Fprintf(os.Stderr, "🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷\n\n")
519522
}
520523

@@ -571,13 +574,19 @@ func run(state overseer.State) {
571574
if err := compareScans(ctx, cmd, engConf); err != nil {
572575
logFatal(err, "error comparing detection strategies")
573576
}
577+
if err := closePrinter(printer); err != nil {
578+
logFatal(err, "failed to close printer")
579+
}
574580
return
575581
}
576582

577583
metrics, err := runSingleScan(ctx, cmd, engConf)
578584
if err != nil {
579585
logFatal(err, "error running scan")
580586
}
587+
if err := closePrinter(printer); err != nil {
588+
logFatal(err, "failed to close printer")
589+
}
581590

582591
verificationCacheMetricsSnapshot := struct {
583592
Hits int32
@@ -1172,6 +1181,13 @@ func commaSeparatedToSlice(s []string) []string {
11721181
return result
11731182
}
11741183

1184+
func closePrinter(printer engine.Printer) error {
1185+
if closer, ok := printer.(interface{ Close() error }); ok {
1186+
return closer.Close()
1187+
}
1188+
return nil
1189+
}
1190+
11751191
func printAverageDetectorTime(e *engine.Engine) {
11761192
fmt.Fprintln(
11771193
os.Stderr,

pkg/output/markdown.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package output
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"os"
8+
"sort"
9+
"strings"
10+
"sync"
11+
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
14+
)
15+
16+
// MarkdownPrinter renders TruffleHog findings into a Markdown document with
17+
// dedicated tables for verified and unverified secrets. It buffers the rows
18+
// while scanning and flushes the final report when Close is invoked.
19+
type MarkdownPrinter struct {
20+
mu sync.Mutex
21+
out io.Writer
22+
verified []markdownRow
23+
unverified []markdownRow
24+
}
25+
26+
// markdownRow represents a single table entry in the Markdown report.
27+
type markdownRow struct {
28+
Detector string
29+
File string
30+
Line string
31+
Redacted string
32+
}
33+
34+
// NewMarkdownPrinter builds a MarkdownPrinter that writes to out. When out is
35+
// nil, stdout is used.
36+
func NewMarkdownPrinter(out io.Writer) *MarkdownPrinter {
37+
if out == nil {
38+
out = os.Stdout
39+
}
40+
return &MarkdownPrinter{out: out}
41+
}
42+
43+
// Print collects each result so the final Markdown doc can include per-section
44+
// counts and tables before the buffered results are rendered in Close.
45+
func (p *MarkdownPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata) error {
46+
file := "n/a"
47+
line := "n/a"
48+
49+
meta, err := structToMap(r.SourceMetadata.Data)
50+
if err != nil {
51+
return fmt.Errorf("could not marshal result: %w", err)
52+
}
53+
54+
for _, data := range meta {
55+
for k, v := range data {
56+
if k == "line" {
57+
if l, ok := v.(float64); ok {
58+
line = fmt.Sprintf("%d", int64(l))
59+
}
60+
}
61+
if k == "file" {
62+
if filename, ok := v.(string); ok {
63+
file = filename
64+
}
65+
}
66+
}
67+
}
68+
69+
row := markdownRow{
70+
Detector: r.DetectorType.String(),
71+
File: file,
72+
Line: line,
73+
Redacted: sanitize(r.Redacted),
74+
}
75+
76+
p.mu.Lock()
77+
defer p.mu.Unlock()
78+
79+
if r.Verified {
80+
p.verified = append(p.verified, row)
81+
} else {
82+
p.unverified = append(p.unverified, row)
83+
}
84+
return nil
85+
}
86+
87+
// Close renders the buffered findings to Markdown. Close should be invoked by
88+
// the output manager once scanning finishes.
89+
func (p *MarkdownPrinter) Close() error {
90+
p.mu.Lock()
91+
defer p.mu.Unlock()
92+
93+
doc := renderMarkdown(p.verified, p.unverified)
94+
if doc == "" {
95+
return nil
96+
}
97+
if _, err := fmt.Fprint(p.out, doc); err != nil {
98+
return fmt.Errorf("write markdown: %w", err)
99+
}
100+
return nil
101+
}
102+
103+
// renderMarkdown mirrors templates/trufflehog_report.py by emitting a title,
104+
// optional sections for verified/unverified findings, and per-section counts.
105+
func renderMarkdown(verified, unverified []markdownRow) string {
106+
if len(verified) == 0 && len(unverified) == 0 {
107+
return ""
108+
}
109+
110+
var buf bytes.Buffer
111+
buf.WriteString("# TruffleHog Findings\n\n")
112+
writeSection := func(title string, rows []markdownRow) {
113+
if len(rows) == 0 {
114+
return
115+
}
116+
sort.SliceStable(rows, func(i, j int) bool {
117+
if rows[i].Detector != rows[j].Detector {
118+
return rows[i].Detector < rows[j].Detector
119+
}
120+
if rows[i].File != rows[j].File {
121+
return rows[i].File < rows[j].File
122+
}
123+
return rows[i].Line < rows[j].Line
124+
})
125+
126+
fmt.Fprintf(&buf, "## %s (%d)\n", title, len(rows))
127+
buf.WriteString("| Detector | File | Line | Redacted |\n")
128+
buf.WriteString("| --- | --- | --- | --- |\n")
129+
for _, row := range rows {
130+
fmt.Fprintf(&buf, "| %s | %s | %s | %s |\n", row.Detector, row.File, row.Line, row.Redacted)
131+
}
132+
buf.WriteString("\n")
133+
}
134+
135+
writeSection("Verified Findings", append([]markdownRow(nil), verified...))
136+
writeSection("Unverified Findings", append([]markdownRow(nil), unverified...))
137+
138+
return strings.TrimRight(buf.String(), "\n") + "\n"
139+
}
140+
141+
var sanitizer = strings.NewReplacer("\r", " ", "\n", " ", "|", "\\|")
142+
143+
func sanitize(value string) string { return sanitizer.Replace(value) }

pkg/tui/pages/source_configure/trufflehog_configure.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ func GetTrufflehogConfiguration() truffleCmdModel {
3737
Placeholder: "false",
3838
}
3939

40+
markdownOutput := textinputs.InputConfig{
41+
Label: "Markdown output",
42+
Key: "markdown",
43+
Required: false,
44+
Help: "Output results to Markdown",
45+
Placeholder: "false",
46+
}
47+
4048
excludeDetectors := textinputs.InputConfig{
4149
Label: "Exclude detectors",
4250
Key: "exclude_detectors",
@@ -53,13 +61,17 @@ func GetTrufflehogConfiguration() truffleCmdModel {
5361
Placeholder: strconv.Itoa(runtime.NumCPU()),
5462
}
5563

56-
return truffleCmdModel{textinputs.New([]textinputs.InputConfig{jsonOutput, verification, verifiedResults, excludeDetectors, concurrency}).SetSkip(true)}
64+
return truffleCmdModel{textinputs.New([]textinputs.InputConfig{jsonOutput, markdownOutput, verification, verifiedResults, excludeDetectors, concurrency}).SetSkip(true)}
5765
}
5866

5967
func (m truffleCmdModel) Cmd() string {
6068
var command []string
6169
inputs := m.GetInputs()
6270

71+
if isTrue(inputs["markdown"].Value) {
72+
command = append(command, "--markdown")
73+
}
74+
6375
if isTrue(inputs["json"].Value) {
6476
command = append(command, "--json")
6577
}
@@ -86,7 +98,7 @@ func (m truffleCmdModel) Cmd() string {
8698

8799
func (m truffleCmdModel) Summary() string {
88100
summary := strings.Builder{}
89-
keys := []string{"no-verification", "only-verified", "json", "exclude_detectors", "concurrency"}
101+
keys := []string{"no-verification", "only-verified", "json", "markdown", "exclude_detectors", "concurrency"}
90102

91103
inputs := m.GetInputs()
92104
labels := m.GetLabels()

0 commit comments

Comments
 (0)