Skip to content

Commit f53b945

Browse files
committed
Add Markdown output support
- Introduce `MarkDownOutput` flag and CLI option (`-markdown` / `-md`). - Implement `Result.MarkdownOutput` to generate a formatted Markdown table with optional title and CDN columns, and a response body preview. - Add markdown escaping helper. - Update runner to create `.md` output file, handle markdown generation per result, and write to file or stdout. - Adjust option parsing and related logic to include markdown handling.
1 parent 7f9403e commit f53b945

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

runner/md_output.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package runner
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
)
8+
9+
func (r Result) MarkdownOutput(scanopts *ScanOptions) string {
10+
var b strings.Builder
11+
12+
// Table Header
13+
b.WriteString("| URL | Status | Method | IP | Size | Words | Lines |")
14+
if r.Title != "" {
15+
b.WriteString(" Title |")
16+
}
17+
if r.CDNName != "" {
18+
b.WriteString(" CDN |")
19+
}
20+
b.WriteString("\n")
21+
22+
// Table Separator
23+
b.WriteString("|---|---|---|---|---|---|---|")
24+
if r.Title != "" {
25+
b.WriteString("---|")
26+
}
27+
if r.CDNName != "" {
28+
b.WriteString("---|")
29+
}
30+
b.WriteString("\n")
31+
32+
// Table Data Row
33+
fmt.Fprintf(&b, "| %s | `%d %s` | `%s` | `%s` | %d | %d | %d |",
34+
r.URL,
35+
r.StatusCode, http.StatusText(r.StatusCode),
36+
r.Method,
37+
r.HostIP,
38+
r.ContentLength,
39+
r.Words,
40+
r.Lines)
41+
42+
if r.Title != "" {
43+
fmt.Fprintf(&b, " %s |", escapeMarkdown(r.Title))
44+
}
45+
if r.CDNName != "" {
46+
fmt.Fprintf(&b, " `%s` |", r.CDNName)
47+
}
48+
b.WriteString("\n\n")
49+
50+
// Response Body Code Block
51+
if r.BodyPreview != "" {
52+
b.WriteString("**Response Body Preview:**\n")
53+
b.WriteString("```text\n")
54+
b.WriteString(r.BodyPreview)
55+
b.WriteString("\n```\n")
56+
}
57+
58+
return b.String()
59+
}
60+
61+
func escapeMarkdown(s string) string {
62+
replacer := strings.NewReplacer(
63+
"|", "\\|",
64+
"\n", " ",
65+
)
66+
return strings.TrimSpace(replacer.Replace(s))
67+
}

runner/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ type Options struct {
222222
RespectHSTS bool
223223
StoreResponse bool
224224
JSONOutput bool
225+
MarkDownOutput bool
225226
CSVOutput bool
226227
CSVOutputEncoding string
227228
PdcpAuth string
@@ -478,6 +479,7 @@ func ParseOptions() *Options {
478479
flagSet.BoolVar(&options.CSVOutput, "csv", false, "store output in csv format"),
479480
flagSet.StringVarP(&options.CSVOutputEncoding, "csv-output-encoding", "csvo", "", "define output encoding"),
480481
flagSet.BoolVarP(&options.JSONOutput, "json", "j", false, "store output in JSONL(ines) format"),
482+
flagSet.BoolVarP(&options.MarkDownOutput, "markdown", "md", false, "store output in Markdown table format"),
481483
flagSet.BoolVarP(&options.ResponseHeadersInStdout, "include-response-header", "irh", false, "include http response (headers) in JSON output (-json only)"),
482484
flagSet.BoolVarP(&options.ResponseInStdout, "include-response", "irr", false, "include http request/response (headers + body) in JSON output (-json only)"),
483485
flagSet.BoolVarP(&options.Base64ResponseInStdout, "include-response-base64", "irrb", false, "include base64 encoded http request/response in JSON output (-json only)"),

runner/runner.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ func (r *Runner) RunEnumeration() {
815815
}
816816
}()
817817

818-
var plainFile, jsonFile, csvFile, indexFile, indexScreenshotFile *os.File
818+
var plainFile, jsonFile, csvFile, mdFile, indexFile, indexScreenshotFile *os.File
819819

820820
if r.options.Output != "" && r.options.OutputAll {
821821
plainFile = openOrCreateFile(r.options.Resume, r.options.Output)
@@ -832,7 +832,17 @@ func (r *Runner) RunEnumeration() {
832832
}()
833833
}
834834

835-
jsonOrCsv := (r.options.JSONOutput || r.options.CSVOutput)
835+
if r.options.Output != "" && r.options.MarkDownOutput {
836+
mdFile = openOrCreateFile(
837+
r.options.Resume,
838+
r.options.Output+".md",
839+
)
840+
defer func() {
841+
_ = mdFile.Close()
842+
}()
843+
}
844+
845+
jsonOrCsv := (r.options.JSONOutput || r.options.CSVOutput || r.options.MarkDownOutput)
836846
jsonAndCsv := (r.options.JSONOutput && r.options.CSVOutput)
837847
if r.options.Output != "" && plainFile == nil && !jsonOrCsv {
838848
plainFile = openOrCreateFile(r.options.Resume, r.options.Output)
@@ -1228,6 +1238,16 @@ func (r *Runner) RunEnumeration() {
12281238
}
12291239
}
12301240

1241+
if r.options.MarkDownOutput {
1242+
row := resp.MarkdownOutput(&r.scanopts)
1243+
if !r.options.OutputAll {
1244+
gologger.Silent().Msgf("%s\n", row)
1245+
}
1246+
if mdFile != nil {
1247+
mdFile.WriteString(row + "\n")
1248+
}
1249+
}
1250+
12311251
for _, nextStep := range nextSteps {
12321252
nextStep <- resp
12331253
}

0 commit comments

Comments
 (0)