Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions runner/md_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package runner

import (
"fmt"
"reflect"
"strings"
)

func (r Result) MarkdownHeader() string { //nolint
var headers []string

t := reflect.TypeOf(r)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("md")
if tag == "" || tag == "-" {
continue
}
headers = append(headers, tag)
}

var b strings.Builder
b.WriteString("|")
for _, h := range headers {
fmt.Fprintf(&b, " %s |", h)
}
b.WriteString("\n")

b.WriteString("|")
for range headers {
b.WriteString("---|")
}
b.WriteString("\n")

return b.String()
}

func (r Result) MarkdownRow(scanopts *ScanOptions) string { //nolint
var values []string

v := reflect.ValueOf(r)
t := v.Type()

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("md")
if tag == "" || tag == "-" {
continue
}

fieldValue := v.Field(i)
values = append(values, formatMarkdownValue(fieldValue))
}

var b strings.Builder
b.WriteString("|")
for _, val := range values {
fmt.Fprintf(&b, " %s |", val)
}
b.WriteString("\n")

return b.String()
}

func formatMarkdownValue(v reflect.Value) string {
switch v.Kind() {
case reflect.String:
return escapeMarkdown(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprintf("%d", v.Int())
case reflect.Bool:
return fmt.Sprintf("%t", v.Bool())
case reflect.Slice:
if v.Len() == 0 {
return ""
}
var items []string
for i := 0; i < v.Len(); i++ {
items = append(items, fmt.Sprintf("%v", v.Index(i).Interface()))
}
return escapeMarkdown(strings.Join(items, ", "))
default:
if v.CanInterface() {
return escapeMarkdown(fmt.Sprintf("%v", v.Interface()))
}
return ""
}
}

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
58 changes: 48 additions & 10 deletions runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,8 @@ func (r *Runner) RunEnumeration() {
}
}()

var plainFile, jsonFile, csvFile, indexFile, indexScreenshotFile *os.File
var plainFile, jsonFile, csvFile, mdFile, indexFile, indexScreenshotFile *os.File
markdownHeaderWritten := false // guard to prevent writing the header multiple times

if r.options.Output != "" && r.options.OutputAll {
plainFile = openOrCreateFile(r.options.Resume, r.options.Output)
Expand All @@ -830,11 +831,15 @@ func (r *Runner) RunEnumeration() {
defer func() {
_ = csvFile.Close()
}()
mdFile = openOrCreateFile(r.options.Resume, r.options.Output+".md")
defer func() {
_ = mdFile.Close()
}()
}

jsonOrCsv := (r.options.JSONOutput || r.options.CSVOutput)
jsonAndCsv := (r.options.JSONOutput && r.options.CSVOutput)
if r.options.Output != "" && plainFile == nil && !jsonOrCsv {
jsonOrCsvOrMD := (r.options.JSONOutput || r.options.CSVOutput || r.options.MarkDownOutput)
jsonAndCsvAndMD := (r.options.JSONOutput && r.options.CSVOutput && r.options.MarkDownOutput)
if r.options.Output != "" && plainFile == nil && !jsonOrCsvOrMD {
plainFile = openOrCreateFile(r.options.Resume, r.options.Output)
defer func() {
_ = plainFile.Close()
Expand All @@ -843,7 +848,7 @@ func (r *Runner) RunEnumeration() {

if r.options.Output != "" && r.options.JSONOutput && jsonFile == nil {
ext := ""
if jsonAndCsv {
if jsonAndCsvAndMD {
ext = ".json"
}
jsonFile = openOrCreateFile(r.options.Resume, r.options.Output+ext)
Expand All @@ -854,7 +859,7 @@ func (r *Runner) RunEnumeration() {

if r.options.Output != "" && r.options.CSVOutput && csvFile == nil {
ext := ""
if jsonAndCsv {
if jsonAndCsvAndMD {
ext = ".csv"
}
csvFile = openOrCreateFile(r.options.Resume, r.options.Output+ext)
Expand All @@ -863,6 +868,17 @@ func (r *Runner) RunEnumeration() {
}()
}

if r.options.Output != "" && r.options.MarkDownOutput && mdFile == nil {
ext := ""
if jsonAndCsvAndMD {
ext = ".md"
}
mdFile = openOrCreateFile(r.options.Resume, r.options.Output+ext)
defer func() {
_ = mdFile.Close()
}()
}

if r.options.CSVOutput {
outEncoding := strings.ToLower(r.options.CSVOutputEncoding)
switch outEncoding {
Expand All @@ -877,7 +893,7 @@ func (r *Runner) RunEnumeration() {
gologger.Fatal().Msgf("unknown csv output encoding: %s\n", r.options.CSVOutputEncoding)
}
headers := Result{}.CSVHeader()
if !r.options.OutputAll && !jsonAndCsv {
if !r.options.OutputAll && !jsonAndCsvAndMD {
gologger.Silent().Msgf("%s\n", headers)
}

Expand Down Expand Up @@ -1092,7 +1108,7 @@ func (r *Runner) RunEnumeration() {
}
}

if !r.options.DisableStdout && (!jsonOrCsv || jsonAndCsv || r.options.OutputAll) {
if !r.options.DisableStdout && (!jsonOrCsvOrMD || jsonAndCsvAndMD || r.options.OutputAll) {
gologger.Silent().Msgf("%s\n", resp.str)
}

Expand Down Expand Up @@ -1205,7 +1221,7 @@ func (r *Runner) RunEnumeration() {
if r.options.JSONOutput {
row := resp.JSON(&r.scanopts)

if !r.options.OutputAll && !jsonAndCsv {
if !r.options.OutputAll && !jsonAndCsvAndMD {
gologger.Silent().Msgf("%s\n", row)
}

Expand All @@ -1218,7 +1234,7 @@ func (r *Runner) RunEnumeration() {
if r.options.CSVOutput {
row := resp.CSVRow(&r.scanopts)

if !r.options.OutputAll && !jsonAndCsv {
if !r.options.OutputAll && !jsonAndCsvAndMD {
gologger.Silent().Msgf("%s\n", row)
}

Expand All @@ -1228,6 +1244,28 @@ func (r *Runner) RunEnumeration() {
}
}

if r.options.MarkDownOutput || r.options.OutputAll {
if !markdownHeaderWritten {
header := resp.MarkdownHeader()
if !r.options.OutputAll {
gologger.Silent().Msgf("%s", header)
}
if mdFile != nil {
_, _ = mdFile.WriteString(header)
}
markdownHeaderWritten = true
}

row := resp.MarkdownRow(&r.scanopts)

if !r.options.OutputAll {
gologger.Silent().Msgf("%s", row)
}
if mdFile != nil {
_, _ = mdFile.WriteString(row)
}
}

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