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:
parent
8d057ea177
commit
826ebbc21c
12
README.md
12
README.md
@ -15,11 +15,11 @@ Heavily inspired by the great projects [gobuster](https://github.com/OJ/gobuster
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Fast!
|
- Fast!
|
||||||
- Allows fuzzing of HTTP header values, HTTP method, POST data, and different parts of URL, including GET parameter names and values
|
- 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.
|
- 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
|
- Modularized architecture that allows integration with existing toolchains with reasonable effort
|
||||||
- Easy-to-add filters and matchers (they are interoperable)
|
- Easy-to-add filters and matchers (they are interoperable)
|
||||||
|
|
||||||
## Example cases
|
## Example cases
|
||||||
|
|
||||||
@ -193,6 +193,8 @@ The only dependency of ffuf is Go 1.11. No dependencies outside of Go standard l
|
|||||||
- Changed
|
- Changed
|
||||||
- New CLI flag: -i, dummy flag that does nothing. for compatibility with copy as curl.
|
- 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 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
|
- 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.
|
- The internal logging information to be discarded, and can be written to a file with the new `-debug-log` flag.
|
||||||
|
|
||||||
|
|||||||
4
main.go
4
main.go
@ -85,7 +85,7 @@ func main() {
|
|||||||
flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL")
|
flag.StringVar(&opts.proxyURL, "x", "", "HTTP Proxy URL")
|
||||||
flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
|
flag.StringVar(&conf.Method, "X", "GET", "HTTP method to use")
|
||||||
flag.StringVar(&conf.OutputFile, "o", "", "Write output to file")
|
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.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.Quiet, "s", false, "Do not print additional information (silent mode)")
|
||||||
flag.BoolVar(&conf.StopOn403, "sf", false, "Stop when > 95% of responses return 403 Forbidden")
|
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
|
//Check the output file format option
|
||||||
if conf.OutputFile != "" {
|
if conf.OutputFile != "" {
|
||||||
//No need to check / error out if output file isn't defined
|
//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
|
found := false
|
||||||
for _, f := range outputFormats {
|
for _, f := range outputFormats {
|
||||||
if f == parseOpts.outputFormat {
|
if f == parseOpts.outputFormat {
|
||||||
|
|||||||
161
pkg/output/file_html.go
Normal file
161
pkg/output/file_html.go
Normal 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
51
pkg/output/file_md.go
Normal 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
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ type Result struct {
|
|||||||
StatusCode int64 `json:"status"`
|
StatusCode int64 `json:"status"`
|
||||||
ContentLength int64 `json:"length"`
|
ContentLength int64 `json:"length"`
|
||||||
ContentWords int64 `json:"words"`
|
ContentWords int64 `json:"words"`
|
||||||
|
HTMLColor string `json:"html_color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStdoutput(conf *ffuf.Config) *Stdoutput {
|
func NewStdoutput(conf *ffuf.Config) *Stdoutput {
|
||||||
@ -108,6 +109,10 @@ func (s *Stdoutput) Finalize() error {
|
|||||||
if s.config.OutputFile != "" {
|
if s.config.OutputFile != "" {
|
||||||
if s.config.OutputFormat == "json" {
|
if s.config.OutputFormat == "json" {
|
||||||
err = writeJSON(s.config, s.Results)
|
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" {
|
} else if s.config.OutputFormat == "csv" {
|
||||||
err = writeCSV(s.config, s.Results, false)
|
err = writeCSV(s.config, s.Results, false)
|
||||||
} else if s.config.OutputFormat == "ecsv" {
|
} else if s.config.OutputFormat == "ecsv" {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user