Response time logging and filtering (#433)
* Added response time reporting and filtering * Update to use the http config context * Added changelog and contributor info * Round time output in stdout to nearest millisecond * Change stdout duration rounding to use Milliseconds() * Go back to Round() for timing output * Changed stdout to display millisecond durations Co-authored-by: Joona Hoikkala <joohoi@users.noreply.github.com>
This commit is contained in:
parent
b56de007d4
commit
965f282c0b
@ -1,7 +1,9 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
- master
|
- master
|
||||||
- New
|
- New
|
||||||
|
- Added response time logging and filtering
|
||||||
- Added a CLI flag to specify TLS SNI value
|
- Added a CLI flag to specify TLS SNI value
|
||||||
|
|
||||||
- Changed
|
- Changed
|
||||||
- Fixed an issue where output file was created regardless of `-or`
|
- Fixed an issue where output file was created regardless of `-or`
|
||||||
- Fixed an issue where output (often a lot of it) would be printed after entering interactive mode
|
- Fixed an issue where output (often a lot of it) would be printed after entering interactive mode
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
* [Damian89](https://github.com/Damian89)
|
* [Damian89](https://github.com/Damian89)
|
||||||
* [Daviey](https://github.com/Daviey)
|
* [Daviey](https://github.com/Daviey)
|
||||||
* [delic](https://github.com/delic)
|
* [delic](https://github.com/delic)
|
||||||
|
* [denandz](https://github.com/denandz)
|
||||||
* [erbbysam](https://github.com/erbbysam)
|
* [erbbysam](https://github.com/erbbysam)
|
||||||
* [eur0pa](https://github.com/eur0pa)
|
* [eur0pa](https://github.com/eur0pa)
|
||||||
* [fabiobauer](https://github.com/fabiobauer)
|
* [fabiobauer](https://github.com/fabiobauer)
|
||||||
|
|||||||
@ -199,6 +199,7 @@ MATCHER OPTIONS:
|
|||||||
-ml Match amount of lines in response
|
-ml Match amount of lines in response
|
||||||
-mr Match regexp
|
-mr Match regexp
|
||||||
-ms Match HTTP response size
|
-ms Match HTTP response size
|
||||||
|
-mt Match how many milliseconds to the first response byte, either greater or less than. EG: >100 or <100
|
||||||
-mw Match amount of words in response
|
-mw Match amount of words in response
|
||||||
|
|
||||||
FILTER OPTIONS:
|
FILTER OPTIONS:
|
||||||
@ -206,6 +207,7 @@ FILTER OPTIONS:
|
|||||||
-fl Filter by amount of lines in response. Comma separated list of line counts and ranges
|
-fl Filter by amount of lines in response. Comma separated list of line counts and ranges
|
||||||
-fr Filter regexp
|
-fr Filter regexp
|
||||||
-fs Filter HTTP response size. Comma separated list of sizes and ranges
|
-fs Filter HTTP response size. Comma separated list of sizes and ranges
|
||||||
|
-ft Filter by number of milliseconds to the first response byte, either greater or less than. EG: >100 or <100
|
||||||
-fw Filter by amount of words in response. Comma separated list of word counts and ranges
|
-fw Filter by amount of words in response. Comma separated list of word counts and ranges
|
||||||
|
|
||||||
INPUT OPTIONS:
|
INPUT OPTIONS:
|
||||||
|
|||||||
@ -69,6 +69,7 @@
|
|||||||
regexp = ""
|
regexp = ""
|
||||||
size = ""
|
size = ""
|
||||||
status = ""
|
status = ""
|
||||||
|
time = ""
|
||||||
words = ""
|
words = ""
|
||||||
|
|
||||||
[matcher]
|
[matcher]
|
||||||
@ -76,4 +77,5 @@
|
|||||||
regexp = ""
|
regexp = ""
|
||||||
size = ""
|
size = ""
|
||||||
status = "200,204,301,302,307,401,403,405"
|
status = "200,204,301,302,307,401,403,405"
|
||||||
|
time = ""
|
||||||
words = ""
|
words = ""
|
||||||
|
|||||||
4
help.go
4
help.go
@ -75,14 +75,14 @@ func Usage() {
|
|||||||
Description: "Matchers for the response filtering.",
|
Description: "Matchers for the response filtering.",
|
||||||
Flags: make([]UsageFlag, 0),
|
Flags: make([]UsageFlag, 0),
|
||||||
Hidden: false,
|
Hidden: false,
|
||||||
ExpectedFlags: []string{"mc", "ml", "mr", "ms", "mw"},
|
ExpectedFlags: []string{"mc", "ml", "mr", "ms", "mt", "mw"},
|
||||||
}
|
}
|
||||||
u_filter := UsageSection{
|
u_filter := UsageSection{
|
||||||
Name: "FILTER OPTIONS",
|
Name: "FILTER OPTIONS",
|
||||||
Description: "Filters for the response filtering.",
|
Description: "Filters for the response filtering.",
|
||||||
Flags: make([]UsageFlag, 0),
|
Flags: make([]UsageFlag, 0),
|
||||||
Hidden: false,
|
Hidden: false,
|
||||||
ExpectedFlags: []string{"fc", "fl", "fr", "fs", "fw"},
|
ExpectedFlags: []string{"fc", "fl", "fr", "fs", "ft", "fw"},
|
||||||
}
|
}
|
||||||
u_input := UsageSection{
|
u_input := UsageSection{
|
||||||
Name: "INPUT OPTIONS",
|
Name: "INPUT OPTIONS",
|
||||||
|
|||||||
2
main.go
2
main.go
@ -86,6 +86,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
|
|||||||
flag.StringVar(&opts.Filter.Regexp, "fr", opts.Filter.Regexp, "Filter regexp")
|
flag.StringVar(&opts.Filter.Regexp, "fr", opts.Filter.Regexp, "Filter regexp")
|
||||||
flag.StringVar(&opts.Filter.Size, "fs", opts.Filter.Size, "Filter HTTP response size. Comma separated list of sizes and ranges")
|
flag.StringVar(&opts.Filter.Size, "fs", opts.Filter.Size, "Filter HTTP response size. Comma separated list of sizes and ranges")
|
||||||
flag.StringVar(&opts.Filter.Status, "fc", opts.Filter.Status, "Filter HTTP status codes from response. Comma separated list of codes and ranges")
|
flag.StringVar(&opts.Filter.Status, "fc", opts.Filter.Status, "Filter HTTP status codes from response. Comma separated list of codes and ranges")
|
||||||
|
flag.StringVar(&opts.Filter.Time, "ft", opts.Filter.Time, "Filter by number of milliseconds to the first response byte, either greater or less than. EG: >100 or <100")
|
||||||
flag.StringVar(&opts.Filter.Words, "fw", opts.Filter.Words, "Filter by amount of words in response. Comma separated list of word counts and ranges")
|
flag.StringVar(&opts.Filter.Words, "fw", opts.Filter.Words, "Filter by amount of words in response. Comma separated list of word counts and ranges")
|
||||||
flag.StringVar(&opts.General.Delay, "p", opts.General.Delay, "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"")
|
flag.StringVar(&opts.General.Delay, "p", opts.General.Delay, "Seconds of `delay` between requests, or a range of random delay. For example \"0.1\" or \"0.1-2.0\"")
|
||||||
flag.StringVar(&opts.HTTP.Data, "d", opts.HTTP.Data, "POST data")
|
flag.StringVar(&opts.HTTP.Data, "d", opts.HTTP.Data, "POST data")
|
||||||
@ -107,6 +108,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions {
|
|||||||
flag.StringVar(&opts.Matcher.Regexp, "mr", opts.Matcher.Regexp, "Match regexp")
|
flag.StringVar(&opts.Matcher.Regexp, "mr", opts.Matcher.Regexp, "Match regexp")
|
||||||
flag.StringVar(&opts.Matcher.Size, "ms", opts.Matcher.Size, "Match HTTP response size")
|
flag.StringVar(&opts.Matcher.Size, "ms", opts.Matcher.Size, "Match HTTP response size")
|
||||||
flag.StringVar(&opts.Matcher.Status, "mc", opts.Matcher.Status, "Match HTTP status codes, or \"all\" for everything.")
|
flag.StringVar(&opts.Matcher.Status, "mc", opts.Matcher.Status, "Match HTTP status codes, or \"all\" for everything.")
|
||||||
|
flag.StringVar(&opts.Matcher.Time, "mt", opts.Matcher.Time, "Match how many milliseconds to the first response byte, either greater or less than. EG: >100 or <100")
|
||||||
flag.StringVar(&opts.Matcher.Words, "mw", opts.Matcher.Words, "Match amount of words in response")
|
flag.StringVar(&opts.Matcher.Words, "mw", opts.Matcher.Words, "Match amount of words in response")
|
||||||
flag.StringVar(&opts.Output.DebugLog, "debug-log", opts.Output.DebugLog, "Write all of the internal logging to the specified file.")
|
flag.StringVar(&opts.Output.DebugLog, "debug-log", opts.Output.DebugLog, "Write all of the internal logging to the specified file.")
|
||||||
flag.StringVar(&opts.Output.OutputDirectory, "od", opts.Output.OutputDirectory, "Directory path to store matched results to.")
|
flag.StringVar(&opts.Output.OutputDirectory, "od", opts.Output.OutputDirectory, "Directory path to store matched results to.")
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package ffuf
|
package ffuf
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
//FilterProvider is a generic interface for both Matchers and Filters
|
//FilterProvider is a generic interface for both Matchers and Filters
|
||||||
type FilterProvider interface {
|
type FilterProvider interface {
|
||||||
Filter(response *Response) (bool, error)
|
Filter(response *Response) (bool, error)
|
||||||
@ -62,6 +64,7 @@ type Result struct {
|
|||||||
ContentType string `json:"content-type"`
|
ContentType string `json:"content-type"`
|
||||||
RedirectLocation string `json:"redirectlocation"`
|
RedirectLocation string `json:"redirectlocation"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
|
Duration time.Duration `json:"duration"`
|
||||||
ResultFile string `json:"resultfile"`
|
ResultFile string `json:"resultfile"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
HTMLColor string `json:"-"`
|
HTMLColor string `json:"-"`
|
||||||
|
|||||||
@ -87,6 +87,7 @@ type FilterOptions struct {
|
|||||||
Regexp string
|
Regexp string
|
||||||
Size string
|
Size string
|
||||||
Status string
|
Status string
|
||||||
|
Time string
|
||||||
Words string
|
Words string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +96,7 @@ type MatcherOptions struct {
|
|||||||
Regexp string
|
Regexp string
|
||||||
Size string
|
Size string
|
||||||
Status string
|
Status string
|
||||||
|
Time string
|
||||||
Words string
|
Words string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +107,7 @@ func NewConfigOptions() *ConfigOptions {
|
|||||||
c.Filter.Regexp = ""
|
c.Filter.Regexp = ""
|
||||||
c.Filter.Size = ""
|
c.Filter.Size = ""
|
||||||
c.Filter.Status = ""
|
c.Filter.Status = ""
|
||||||
|
c.Filter.Time = ""
|
||||||
c.Filter.Words = ""
|
c.Filter.Words = ""
|
||||||
c.General.AutoCalibration = false
|
c.General.AutoCalibration = false
|
||||||
c.General.Colors = false
|
c.General.Colors = false
|
||||||
@ -143,6 +146,7 @@ func NewConfigOptions() *ConfigOptions {
|
|||||||
c.Matcher.Regexp = ""
|
c.Matcher.Regexp = ""
|
||||||
c.Matcher.Size = ""
|
c.Matcher.Size = ""
|
||||||
c.Matcher.Status = "200,204,301,302,307,401,403,405"
|
c.Matcher.Status = "200,204,301,302,307,401,403,405"
|
||||||
|
c.Matcher.Time = ""
|
||||||
c.Matcher.Words = ""
|
c.Matcher.Words = ""
|
||||||
c.Output.DebugLog = ""
|
c.Output.DebugLog = ""
|
||||||
c.Output.OutputDirectory = ""
|
c.Output.OutputDirectory = ""
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package ffuf
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response struct holds the meaningful data returned from request and is meant for passing to filters
|
// Response struct holds the meaningful data returned from request and is meant for passing to filters
|
||||||
@ -18,6 +19,7 @@ type Response struct {
|
|||||||
Request *Request
|
Request *Request
|
||||||
Raw string
|
Raw string
|
||||||
ResultFile string
|
ResultFile string
|
||||||
|
Time time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRedirectLocation returns the redirect location for a 3xx redirect HTTP response
|
// GetRedirectLocation returns the redirect location for a 3xx redirect HTTP response
|
||||||
|
|||||||
@ -25,6 +25,9 @@ func NewFilterByName(name string, value string) (ffuf.FilterProvider, error) {
|
|||||||
if name == "regexp" {
|
if name == "regexp" {
|
||||||
return NewRegexpFilter(value)
|
return NewRegexpFilter(value)
|
||||||
}
|
}
|
||||||
|
if name == "time" {
|
||||||
|
return NewTimeFilter(value)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("Could not create filter with name %s", name)
|
return nil, fmt.Errorf("Could not create filter with name %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +146,9 @@ func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error {
|
|||||||
if f.Name == "mr" {
|
if f.Name == "mr" {
|
||||||
matcherSet = true
|
matcherSet = true
|
||||||
}
|
}
|
||||||
|
if f.Name == "mt" {
|
||||||
|
matcherSet = true
|
||||||
|
}
|
||||||
if f.Name == "mw" {
|
if f.Name == "mw" {
|
||||||
matcherSet = true
|
matcherSet = true
|
||||||
warningIgnoreBody = true
|
warningIgnoreBody = true
|
||||||
@ -182,6 +188,11 @@ func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error {
|
|||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if parseOpts.Filter.Time != "" {
|
||||||
|
if err := AddFilter(conf, "time", parseOpts.Filter.Time); err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if parseOpts.Matcher.Size != "" {
|
if parseOpts.Matcher.Size != "" {
|
||||||
if err := AddMatcher(conf, "size", parseOpts.Matcher.Size); err != nil {
|
if err := AddMatcher(conf, "size", parseOpts.Matcher.Size); err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
@ -202,6 +213,11 @@ func SetupFilters(parseOpts *ffuf.ConfigOptions, conf *ffuf.Config) error {
|
|||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if parseOpts.Matcher.Time != "" {
|
||||||
|
if err := AddFilter(conf, "time", parseOpts.Matcher.Time); err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if conf.IgnoreBody && warningIgnoreBody {
|
if conf.IgnoreBody && warningIgnoreBody {
|
||||||
fmt.Printf("*** Warning: possible undesired combination of -ignore-body and the response options: fl,fs,fw,ml,ms and mw.\n")
|
fmt.Printf("*** Warning: possible undesired combination of -ignore-body and the response options: fl,fs,fw,ml,ms and mw.\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,11 @@ func TestNewFilterByName(t *testing.T) {
|
|||||||
if _, ok := ref.(*RegexpFilter); !ok {
|
if _, ok := ref.(*RegexpFilter); !ok {
|
||||||
t.Errorf("Was expecting regexpfilter")
|
t.Errorf("Was expecting regexpfilter")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tf, _ := NewFilterByName("time", "200")
|
||||||
|
if _, ok := tf.(*TimeFilter); !ok {
|
||||||
|
t.Errorf("Was expecting timefilter")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewFilterByNameError(t *testing.T) {
|
func TestNewFilterByNameError(t *testing.T) {
|
||||||
|
|||||||
66
pkg/filter/time.go
Executable file
66
pkg/filter/time.go
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeFilter struct {
|
||||||
|
ms int64 // milliseconds since first response byte
|
||||||
|
gt bool // filter if response time is greater than
|
||||||
|
lt bool // filter if response time is less than
|
||||||
|
valueRaw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeFilter(value string) (ffuf.FilterProvider, error) {
|
||||||
|
var milliseconds int64
|
||||||
|
gt, lt := false, false
|
||||||
|
|
||||||
|
gt = strings.HasPrefix(value, ">")
|
||||||
|
lt = strings.HasPrefix(value, "<")
|
||||||
|
|
||||||
|
if (!lt && !gt) || (lt && gt) {
|
||||||
|
return &TimeFilter{}, fmt.Errorf("Time filter or matcher (-ft / -mt): invalid value: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
milliseconds, err := strconv.ParseInt(value[1:], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return &TimeFilter{}, fmt.Errorf("Time filter or matcher (-ft / -mt): invalid value: %s", value)
|
||||||
|
}
|
||||||
|
return &TimeFilter{ms: milliseconds, gt: gt, lt: lt, valueRaw: value}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TimeFilter) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}{
|
||||||
|
Value: f.valueRaw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TimeFilter) Filter(response *ffuf.Response) (bool, error) {
|
||||||
|
if f.gt {
|
||||||
|
if response.Time.Milliseconds() > f.ms {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if f.lt {
|
||||||
|
if response.Time.Milliseconds() < f.ms {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TimeFilter) Repr() string {
|
||||||
|
return f.valueRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TimeFilter) ReprVerbose() string {
|
||||||
|
return fmt.Sprintf("Response time: %s", f.Repr())
|
||||||
|
}
|
||||||
54
pkg/filter/time_test.go
Executable file
54
pkg/filter/time_test.go
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewTimeFilter(t *testing.T) {
|
||||||
|
fp, _ := NewTimeFilter(">100")
|
||||||
|
|
||||||
|
f := fp.(*TimeFilter)
|
||||||
|
|
||||||
|
if !f.gt || f.lt {
|
||||||
|
t.Errorf("Time filter was expected to have greater-than")
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.ms != 100 {
|
||||||
|
t.Errorf("Time filter was expected to have ms == 100")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTimeFilterError(t *testing.T) {
|
||||||
|
_, err := NewTimeFilter("100>")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Was expecting an error from errenous input data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeFiltering(t *testing.T) {
|
||||||
|
f, _ := NewTimeFilter(">100")
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
input int64
|
||||||
|
output bool
|
||||||
|
}{
|
||||||
|
{1342, true},
|
||||||
|
{2000, true},
|
||||||
|
{35000, true},
|
||||||
|
{1458700, true},
|
||||||
|
{99, false},
|
||||||
|
{2, false},
|
||||||
|
} {
|
||||||
|
resp := ffuf.Response{
|
||||||
|
Data: []byte("dahhhhhtaaaaa"),
|
||||||
|
Time: time.Duration(test.input * int64(time.Millisecond)),
|
||||||
|
}
|
||||||
|
filterReturn, _ := f.Filter(&resp)
|
||||||
|
if filterReturn != test.output {
|
||||||
|
t.Errorf("Filter test %d: Was expecing filter return value of %t but got %t", i, test.output, filterReturn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,12 @@ package interactive
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ffuf/ffuf/pkg/ffuf"
|
|
||||||
"github.com/ffuf/ffuf/pkg/filter"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||||
|
"github.com/ffuf/ffuf/pkg/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
type interactive struct {
|
type interactive struct {
|
||||||
@ -110,6 +111,15 @@ func (i *interactive) handleInput(in []byte) {
|
|||||||
i.updateFilter("size", args[1])
|
i.updateFilter("size", args[1])
|
||||||
i.Job.Output.Info("New response size filter value set")
|
i.Job.Output.Info("New response size filter value set")
|
||||||
}
|
}
|
||||||
|
case "ft":
|
||||||
|
if len(args) < 2 {
|
||||||
|
i.Job.Output.Error("Please define a value for response time filter, or \"none\" for removing it")
|
||||||
|
} else if len(args) > 2 {
|
||||||
|
i.Job.Output.Error("Too many arguments for \"ft\"")
|
||||||
|
} else {
|
||||||
|
i.updateFilter("time", args[1])
|
||||||
|
i.Job.Output.Info("New response time filter value set")
|
||||||
|
}
|
||||||
case "queueshow":
|
case "queueshow":
|
||||||
i.printQueue()
|
i.printQueue()
|
||||||
case "queuedel":
|
case "queuedel":
|
||||||
@ -205,7 +215,7 @@ func (i *interactive) printPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *interactive) printHelp() {
|
func (i *interactive) printHelp() {
|
||||||
var fc, fl, fs, fw string
|
var fc, fl, fs, ft, fw string
|
||||||
for name, filter := range i.Job.Config.Filters {
|
for name, filter := range i.Job.Config.Filters {
|
||||||
switch name {
|
switch name {
|
||||||
case "status":
|
case "status":
|
||||||
@ -216,6 +226,8 @@ func (i *interactive) printHelp() {
|
|||||||
fw = "(active: " + filter.Repr() + ")"
|
fw = "(active: " + filter.Repr() + ")"
|
||||||
case "size":
|
case "size":
|
||||||
fs = "(active: " + filter.Repr() + ")"
|
fs = "(active: " + filter.Repr() + ")"
|
||||||
|
case "time":
|
||||||
|
ft = "(active: " + filter.Repr() + ")"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
help := `
|
help := `
|
||||||
@ -224,6 +236,7 @@ available commands:
|
|||||||
fl [value] - (re)configure line count filter %s
|
fl [value] - (re)configure line count filter %s
|
||||||
fw [value] - (re)configure word count filter %s
|
fw [value] - (re)configure word count filter %s
|
||||||
fs [value] - (re)configure size filter %s
|
fs [value] - (re)configure size filter %s
|
||||||
|
ft [value] - (re)configure time filter %s
|
||||||
queueshow - show recursive job queue
|
queueshow - show recursive job queue
|
||||||
queuedel [number] - delete a recursion job in the queue
|
queuedel [number] - delete a recursion job in the queue
|
||||||
queueskip - advance to the next queued recursion job
|
queueskip - advance to the next queued recursion job
|
||||||
@ -233,5 +246,5 @@ available commands:
|
|||||||
savejson [filename] - save current matches to a file
|
savejson [filename] - save current matches to a file
|
||||||
help - you are looking at it
|
help - you are looking at it
|
||||||
`
|
`
|
||||||
i.Job.Output.Raw(fmt.Sprintf(help, fc, fl, fw, fs))
|
i.Job.Output.Raw(fmt.Sprintf(help, fc, fl, fw, fs, ft))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/ffuf/ffuf/pkg/ffuf"
|
"github.com/ffuf/ffuf/pkg/ffuf"
|
||||||
)
|
)
|
||||||
|
|
||||||
var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines", "content_type", "resultfile"}
|
var staticheaders = []string{"url", "redirectlocation", "position", "status_code", "content_length", "content_words", "content_lines", "content_type", "duration", "resultfile"}
|
||||||
|
|
||||||
func writeCSV(filename string, config *ffuf.Config, res []ffuf.Result, encode bool) error {
|
func writeCSV(filename string, config *ffuf.Config, res []ffuf.Result, encode bool) error {
|
||||||
header := make([]string, 0)
|
header := make([]string, 0)
|
||||||
@ -64,6 +64,7 @@ func toCSV(r ffuf.Result) []string {
|
|||||||
res = append(res, strconv.FormatInt(r.ContentWords, 10))
|
res = append(res, strconv.FormatInt(r.ContentWords, 10))
|
||||||
res = append(res, strconv.FormatInt(r.ContentLines, 10))
|
res = append(res, strconv.FormatInt(r.ContentLines, 10))
|
||||||
res = append(res, r.ContentType)
|
res = append(res, r.ContentType)
|
||||||
|
res = append(res, r.Duration.String())
|
||||||
res = append(res, r.ResultFile)
|
res = append(res, r.ResultFile)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,7 @@ const (
|
|||||||
<th>Words</th>
|
<th>Words</th>
|
||||||
<th>Lines</th>
|
<th>Lines</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
|
<th>Duration</th>
|
||||||
<th>Resultfile</th>
|
<th>Resultfile</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -99,6 +100,7 @@ const (
|
|||||||
<td>{{ $result.ContentWords }}</td>
|
<td>{{ $result.ContentWords }}</td>
|
||||||
<td>{{ $result.ContentLines }}</td>
|
<td>{{ $result.ContentLines }}</td>
|
||||||
<td>{{ $result.ContentType }}</td>
|
<td>{{ $result.ContentType }}</td>
|
||||||
|
<td>{{ $result.Duration }}</td>
|
||||||
<td>{{ $result.ResultFile }}</td>
|
<td>{{ $result.ResultFile }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type JsonResult struct {
|
|||||||
ContentLines int64 `json:"lines"`
|
ContentLines int64 `json:"lines"`
|
||||||
ContentType string `json:"content-type"`
|
ContentType string `json:"content-type"`
|
||||||
RedirectLocation string `json:"redirectlocation"`
|
RedirectLocation string `json:"redirectlocation"`
|
||||||
|
Duration time.Duration `json:"duration"`
|
||||||
ResultFile string `json:"resultfile"`
|
ResultFile string `json:"resultfile"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
@ -72,6 +73,7 @@ func writeJSON(filename string, config *ffuf.Config, res []ffuf.Result) error {
|
|||||||
ContentLines: r.ContentLines,
|
ContentLines: r.ContentLines,
|
||||||
ContentType: r.ContentType,
|
ContentType: r.ContentType,
|
||||||
RedirectLocation: r.RedirectLocation,
|
RedirectLocation: r.RedirectLocation,
|
||||||
|
Duration: r.Duration,
|
||||||
ResultFile: r.ResultFile,
|
ResultFile: r.ResultFile,
|
||||||
Url: r.Url,
|
Url: r.Url,
|
||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
|
|||||||
@ -14,9 +14,9 @@ const (
|
|||||||
Command line : ` + "`{{.CommandLine}}`" + `
|
Command line : ` + "`{{.CommandLine}}`" + `
|
||||||
Time: ` + "{{ .Time }}" + `
|
Time: ` + "{{ .Time }}" + `
|
||||||
|
|
||||||
{{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | Content Type | ResultFile |
|
{{ range .Keys }}| {{ . }} {{ end }}| URL | Redirectlocation | Position | Status Code | Content Length | Content Words | Content Lines | Content Type | Duration | ResultFile |
|
||||||
{{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- | :----------- |
|
{{ range .Keys }}| :- {{ end }}| :-- | :--------------- | :---- | :------- | :---------- | :------------- | :------------ | :--------- | :----------- |
|
||||||
{{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{ .ContentType }} | {{ .ResultFile }} |
|
{{range .Results}}{{ range $keyword, $value := .Input }}| {{ $value | printf "%s" }} {{ end }}| {{ .Url }} | {{ .RedirectLocation }} | {{ .Position }} | {{ .StatusCode }} | {{ .ContentLength }} | {{ .ContentWords }} | {{ .ContentLines }} | {{ .ContentType }} | {{ .Duration}} | {{ .ResultFile }} |
|
||||||
{{end}}` // The template format is not pretty but follows the markdown guide
|
{{end}}` // The template format is not pretty but follows the markdown guide
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -324,6 +324,7 @@ func (s *Stdoutput) Result(resp ffuf.Response) {
|
|||||||
ContentType: resp.ContentType,
|
ContentType: resp.ContentType,
|
||||||
RedirectLocation: resp.GetRedirectLocation(false),
|
RedirectLocation: resp.GetRedirectLocation(false),
|
||||||
Url: resp.Request.Url,
|
Url: resp.Request.Url,
|
||||||
|
Duration: resp.Time,
|
||||||
ResultFile: resp.ResultFile,
|
ResultFile: resp.ResultFile,
|
||||||
Host: resp.Request.Host,
|
Host: resp.Request.Host,
|
||||||
}
|
}
|
||||||
@ -401,7 +402,7 @@ func (s *Stdoutput) resultQuiet(res ffuf.Result) {
|
|||||||
func (s *Stdoutput) resultMultiline(res ffuf.Result) {
|
func (s *Stdoutput) resultMultiline(res ffuf.Result) {
|
||||||
var res_hdr, res_str string
|
var res_hdr, res_str string
|
||||||
res_str = "%s%s * %s: %s\n"
|
res_str = "%s%s * %s: %s\n"
|
||||||
res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, res.StatusCode, res.ContentLength, res.ContentWords, res.ContentLines)
|
res_hdr = fmt.Sprintf("%s[Status: %d, Size: %d, Words: %d, Lines: %d, Duration: %dms]", TERMINAL_CLEAR_LINE, res.StatusCode, res.ContentLength, res.ContentWords, res.ContentLines, res.Duration.Milliseconds())
|
||||||
res_hdr = s.colorize(res_hdr, res.StatusCode)
|
res_hdr = s.colorize(res_hdr, res.StatusCode)
|
||||||
reslines := ""
|
reslines := ""
|
||||||
if s.config.Verbose {
|
if s.config.Verbose {
|
||||||
@ -427,7 +428,7 @@ func (s *Stdoutput) resultMultiline(res ffuf.Result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stdoutput) resultNormal(res ffuf.Result) {
|
func (s *Stdoutput) resultNormal(res ffuf.Result) {
|
||||||
resnormal := fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(res), s.colorize(fmt.Sprintf("%d", res.StatusCode), res.StatusCode), res.ContentLength, res.ContentWords, res.ContentLines)
|
resnormal := fmt.Sprintf("%s%-23s [Status: %s, Size: %d, Words: %d, Lines: %d, Duration: %dms]", TERMINAL_CLEAR_LINE, s.prepareInputsOneLine(res), s.colorize(fmt.Sprintf("%d", res.StatusCode), res.StatusCode), res.ContentLength, res.ContentWords, res.ContentLines, res.Duration.Milliseconds())
|
||||||
fmt.Println(resnormal)
|
fmt.Println(resnormal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -97,7 +98,21 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
|
|||||||
var err error
|
var err error
|
||||||
var rawreq []byte
|
var rawreq []byte
|
||||||
data := bytes.NewReader(req.Data)
|
data := bytes.NewReader(req.Data)
|
||||||
|
|
||||||
|
var start time.Time
|
||||||
|
var firstByteTime time.Duration
|
||||||
|
|
||||||
|
trace := &httptrace.ClientTrace{
|
||||||
|
WroteRequest: func(wri httptrace.WroteRequestInfo) {
|
||||||
|
start = time.Now() // begin the timer after the request is fully written
|
||||||
|
},
|
||||||
|
GotFirstResponseByte: func() {
|
||||||
|
firstByteTime = time.Since(start) // record when the first byte of the response was received
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
httpreq, err = http.NewRequestWithContext(r.config.Context, req.Method, req.Url, data)
|
httpreq, err = http.NewRequestWithContext(r.config.Context, req.Method, req.Url, data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ffuf.Response{}, err
|
return ffuf.Response{}, err
|
||||||
}
|
}
|
||||||
@ -113,7 +128,7 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Host = httpreq.Host
|
req.Host = httpreq.Host
|
||||||
httpreq = httpreq.WithContext(r.config.Context)
|
httpreq = httpreq.WithContext(httptrace.WithClientTrace(r.config.Context, trace))
|
||||||
for k, v := range req.Headers {
|
for k, v := range req.Headers {
|
||||||
httpreq.Header.Set(k, v)
|
httpreq.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
@ -155,6 +170,7 @@ func (r *SimpleRunner) Execute(req *ffuf.Request) (ffuf.Response, error) {
|
|||||||
linesSize := len(strings.Split(string(resp.Data), "\n"))
|
linesSize := len(strings.Split(string(resp.Data), "\n"))
|
||||||
resp.ContentWords = int64(wordsSize)
|
resp.ContentWords = int64(wordsSize)
|
||||||
resp.ContentLines = int64(linesSize)
|
resp.ContentLines = int64(linesSize)
|
||||||
|
resp.Time = firstByteTime
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user