Skip to content
67 changes: 67 additions & 0 deletions runner/md_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package runner

import (
"fmt"
"net/http"
"strings"
)

func (r Result) MarkdownOutput(scanopts *ScanOptions) string {
var b strings.Builder

// Table Header
b.WriteString("| URL | Status | Method | IP | Size | Words | Lines |")
if r.Title != "" {
b.WriteString(" Title |")
}
if r.CDNName != "" {
b.WriteString(" CDN |")
}
b.WriteString("\n")

// Table Separator
b.WriteString("|---|---|---|---|---|---|---|")
if r.Title != "" {
b.WriteString("---|")
}
if r.CDNName != "" {
b.WriteString("---|")
}
b.WriteString("\n")

// Table Data Row
fmt.Fprintf(&b, "| %s | `%d %s` | `%s` | `%s` | %d | %d | %d |",
r.URL,
r.StatusCode, http.StatusText(r.StatusCode),
r.Method,
r.HostIP,
r.ContentLength,
r.Words,
r.Lines)

if r.Title != "" {
fmt.Fprintf(&b, " %s |", escapeMarkdown(r.Title))
}
if r.CDNName != "" {
fmt.Fprintf(&b, " `%s` |", r.CDNName)
}
b.WriteString("\n\n")

// Response Body Code Block
if r.BodyPreview != "" {
b.WriteString("**Response Body Preview:**\n")
b.WriteString("```text\n")
b.WriteString(r.BodyPreview)
b.WriteString("\n```\n")
}

return b.String()
}

func escapeMarkdown(s string) string {
replacer := strings.NewReplacer(
"|", "\\|",
"\n", " ",
)
return strings.TrimSpace(replacer.Replace(s))
}
2 changes: 2 additions & 0 deletions runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ type Options struct {
RespectHSTS bool
StoreResponse bool
JSONOutput bool
MarkDownOutput bool
CSVOutput bool
CSVOutputEncoding string
PdcpAuth string
Expand Down Expand Up @@ -478,6 +479,7 @@ func ParseOptions() *Options {
flagSet.BoolVar(&options.CSVOutput, "csv", false, "store output in csv format"),
flagSet.StringVarP(&options.CSVOutputEncoding, "csv-output-encoding", "csvo", "", "define output encoding"),
flagSet.BoolVarP(&options.JSONOutput, "json", "j", false, "store output in JSONL(ines) format"),
flagSet.BoolVarP(&options.MarkDownOutput, "markdown", "md", false, "store output in Markdown table format"),
flagSet.BoolVarP(&options.ResponseHeadersInStdout, "include-response-header", "irh", false, "include http response (headers) in JSON output (-json only)"),
flagSet.BoolVarP(&options.ResponseInStdout, "include-response", "irr", false, "include http request/response (headers + body) in JSON output (-json only)"),
flagSet.BoolVarP(&options.Base64ResponseInStdout, "include-response-base64", "irrb", false, "include base64 encoded http request/response in JSON output (-json only)"),
Expand Down
24 changes: 22 additions & 2 deletions runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ func (r *Runner) RunEnumeration() {
}
}()

var plainFile, jsonFile, csvFile, indexFile, indexScreenshotFile *os.File
var plainFile, jsonFile, csvFile, mdFile, indexFile, indexScreenshotFile *os.File

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

jsonOrCsv := (r.options.JSONOutput || r.options.CSVOutput)
if r.options.Output != "" && r.options.MarkDownOutput {
mdFile = openOrCreateFile(
r.options.Resume,
r.options.Output+".md",
)
defer func() {
_ = mdFile.Close()
}()
}

jsonOrCsv := (r.options.JSONOutput || r.options.CSVOutput || r.options.MarkDownOutput)
jsonAndCsv := (r.options.JSONOutput && r.options.CSVOutput)
if r.options.Output != "" && plainFile == nil && !jsonOrCsv {
plainFile = openOrCreateFile(r.options.Resume, r.options.Output)
Expand Down Expand Up @@ -1228,6 +1238,16 @@ func (r *Runner) RunEnumeration() {
}
}

if r.options.MarkDownOutput {
row := resp.MarkdownOutput(&r.scanopts)
if !r.options.OutputAll {
gologger.Silent().Msgf("%s\n", row)
}
if mdFile != nil {
mdFile.WriteString(row + "\n")
}
}

for _, nextStep := range nextSteps {
nextStep <- resp
}
Expand Down