Added HTML and Markdown output support (#63)

* Added HTML and Markdown output support

* Add HTML color code in HTML template

* Added changelog entry

* Fixed copy paste mistake

* Changed the html report to be grepable :)

* Grepable output fixed
This commit is contained in:
SakiiR 2019-11-08 15:18:27 +01:00 committed by Joona Hoikkala
parent 8d057ea177
commit 826ebbc21c
5 changed files with 226 additions and 7 deletions

View file

@ -15,11 +15,11 @@ Heavily inspired by the great projects [gobuster](https://github.com/OJ/gobuster
## Features
- Fast!
- Allows fuzzing of HTTP header values, HTTP method, POST data, and different parts of URL, including GET parameter names and values
- Silent mode (`-s`) for clean output that's easy to use in pipes to other processes.
- Modularized architecture that allows integration with existing toolchains with reasonable effort
- Easy-to-add filters and matchers (they are interoperable)
- Fast!
- Allows fuzzing of HTTP header values, POST data, and different parts of URL, including GET parameter names and values
- Silent mode (`-s`) for clean output that's easy to use in pipes to other processes.
- Modularized architecture that allows integration with existing toolchains with reasonable effort
- Easy-to-add filters and matchers (they are interoperable)
## Example cases
@ -193,6 +193,8 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l
- Changed
- New CLI flag: -i, dummy flag that does nothing. for compatibility with copy as curl.
- New CLI flag: -b/--cookie, cookie data for compatibility with copy as curl.
- New Output format are available: HTML and Markdown table.
- New CLI flag: -l, shows target location of redirect responses
- Filtering and matching by status code, response size or word count now allow using ranges in addition to single values
- The internal logging information to be discarded, and can be written to a file with the new `-debug-log` flag.

View file

@ -85,7 +85,7 @@ func main() {
flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL")
flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
flag.StringVar(&conf.OutputFile, "o", "", "Write output to file")
flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json, csv, ecsv")
flag.StringVar(&opts.outputFormat, "of", "json", "Output file format. Available formats: json, html, md, csv, ecsv")
flag.BoolVar(&conf.ShowRedirectLocation, "l", false, "Show target location of redirect responses")
flag.BoolVar(&conf.Quiet, "s", false, "Do not print additional information (silent mode)")
flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden")
@ -290,7 +290,7 @@ func prepareConfig(parseOpts *cliOptions, conf *ffuf.Config) error {
//Check the output file format option
if conf.OutputFile != "" {
//No need to check / error out if output file isn't defined
outputFormats := []string{"json", "csv", "ecsv"}
outputFormats := []string{"json", "html", "md", "csv", "ecsv"}
found := false
for _, f := range outputFormats {
if f == parseOpts.outputFormat {

161
pkg/output/file_html.go Normal file
View file

@ -0,0 +1,161 @@
package output
import (
"html/template"
"os"
"time"
"github.com/ffuf/ffuf/pkg/ffuf"
)
type htmlFileOutput struct {
CommandLine string
Time string
Results []Result
}
const (
htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0"
/>
<title>FFUF Report - </title>
<!-- CSS -->
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"
/>
</head>
<body>
<nav>
<div class="nav-wrapper">
<a href="#" class="brand-logo">FFUF</a>
<ul id="nav-mobile" class="right hide-on-med-and-down">
</ul>
</div>
</nav>
<main class="section no-pad-bot" id="index-banner">
<div class="container">
<br /><br />
<h1 class="header center ">FFUF Report</h1>
<div class="row center">
<pre>{{ .CommandLine }}</pre>
<pre>{{ .Time }}</pre>
<table>
<thead>
<div style="display:none">
|result_raw|StatusCode|Input|Position|ContentLength|ContentWords|
</div>
<tr>
<th>Status</th>
<th>Input</th>
<th>Position</th>
<th>Length</th>
<th>Words</th>
</tr>
</thead>
<tbody>
{{range .Results}}
<div style="display:none">
|result_raw|{{ .StatusCode }}|{{ .Input }}|{{ .Position }}|{{ .ContentLength }}|{{ .ContentWords }}|
</div>
<tr class="result-{{ .StatusCode }}" style="background-color: {{.HTMLColor}};"><td><font color="black" class="status-code">{{ .StatusCode }}</font></td><td>{{ .Input }}</td><td>{{ .Position }}</td><td>{{ .ContentLength }}</td><td>{{ .ContentWords }}</td></tr>
{{end}}
</tbody>
</table>
</div>
<br /><br />
</div>
</main>
<!--JavaScript at end of body for optimized loading-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<style>
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
main {
flex: 1 0 auto;
}
</style>
</body>
</html>
`
)
// colorizeResults returns a new slice with HTMLColor attribute
func colorizeResults(results []Result) []Result {
newResults := make([]Result, 0)
for _, r := range results {
result := r
result.HTMLColor = "black"
s := result.StatusCode
if s >= 200 && s <= 299 {
result.HTMLColor = "#adea9e"
}
if s >= 300 && s <= 399 {
result.HTMLColor = "#bbbbe6"
}
if s >= 400 && s <= 499 {
result.HTMLColor = "#d2cb7e"
}
if s >= 500 && s <= 599 {
result.HTMLColor = "#de8dc1"
}
newResults = append(newResults, result)
}
return newResults
}
func writeHTML(config *ffuf.Config, results []Result) error {
results = colorizeResults(results)
ti := time.Now()
outHTML := htmlFileOutput{
CommandLine: config.CommandLine,
Time: ti.Format(time.RFC3339),
Results: results,
}
f, err := os.Create(config.OutputFile)
if err != nil {
return err
}
defer f.Close()
templateName := "output.html"
t := template.New(templateName).Delims("{{", "}}")
t.Parse(htmlTemplate)
t.Execute(f, outHTML)
return nil
}

51
pkg/output/file_md.go Normal file
View file

@ -0,0 +1,51 @@
package output
import (
"html/template"
"os"
"time"
"github.com/ffuf/ffuf/pkg/ffuf"
)
type markdownFileOutput struct {
CommandLine string
Time string
Results []Result
}
const (
markdownTemplate = `# FFUF Report
Command line : ` + "`{{.CommandLine}}`" + `
Time: ` + "{{ .Time }}" + `
| Input | Position | Status Code | Content Length | Content Words |
| :---- | :------- | :---------- | :------------- | :------------ |
{{range .Results}}| {{ .Input }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} |
{{end}}
` // The template format is not pretty but follows the markdown guide
)
func writeMarkdown(config *ffuf.Config, res []Result) error {
ti := time.Now()
outHTML := htmlFileOutput{
CommandLine: config.CommandLine,
Time: ti.Format(time.RFC3339),
Results: res,
}
f, err := os.Create(config.OutputFile)
if err != nil {
return err
}
defer f.Close()
templateName := "output.md"
t := template.New(templateName).Delims("{{", "}}")
t.Parse(markdownTemplate)
t.Execute(f, outHTML)
return nil
}

View file

@ -32,6 +32,7 @@ type Result struct {
StatusCode int64 `json:"status"`
ContentLength int64 `json:"length"`
ContentWords int64 `json:"words"`
HTMLColor string `json:"html_color"`
}
func NewStdoutput(conf *ffuf.Config) *Stdoutput {
@ -108,6 +109,10 @@ func (s *Stdoutput) Finalize() error {
if s.config.OutputFile != "" {
if s.config.OutputFormat == "json" {
err = writeJSON(s.config, s.Results)
} else if s.config.OutputFormat == "html" {
err = writeHTML(s.config, s.Results)
} else if s.config.OutputFormat == "md" {
err = writeMarkdown(s.config, s.Results)
} else if s.config.OutputFormat == "csv" {
err = writeCSV(s.config, s.Results, false)
} else if s.config.OutputFormat == "ecsv" {