diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b3d73..ec31b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added response time logging and filtering - Added a CLI flag to specify TLS SNI value - Added full line colors + - Added `-json` to emit newline delimited JSON output - Added 500 Internal Server Error to list of status codes matched by default - Changed - Fixed an issue where output file was created regardless of `-or` diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1624978..da3ac0d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -25,6 +25,7 @@ * [jimen0](https://github.com/jimen0) * [joohoi](https://github.com/joohoi) * [jsgv](https://github.com/jsgv) +* [justinsteven](https://github.com/justinsteven) * [jvesiluoma](https://github.com/jvesiluoma) * [Kiblyn11](https://github.com/Kiblyn11) * [lc](https://github.com/lc) diff --git a/ffufrc.example b/ffufrc.example index fe54256..059a8b8 100644 --- a/ffufrc.example +++ b/ffufrc.example @@ -39,6 +39,7 @@ stoponerrors = false threads = 40 verbose = false + json = false [input] dirsearchcompat = false diff --git a/help.go b/help.go index 37b309a..15af7d5 100644 --- a/help.go +++ b/help.go @@ -61,7 +61,7 @@ func Usage() { Description: "", Flags: make([]UsageFlag, 0), Hidden: false, - ExpectedFlags: []string{"ac", "acc", "c", "config", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "s", "sa", "se", "sf", "t", "v", "V"}, + ExpectedFlags: []string{"ac", "acc", "c", "config", "json", "maxtime", "maxtime-job", "noninteractive", "p", "rate", "s", "sa", "se", "sf", "t", "v", "V"}, } u_compat := UsageSection{ Name: "COMPATIBILITY OPTIONS", diff --git a/main.go b/main.go index 34f42c6..48abe64 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,7 @@ func ParseFlags(opts *ffuf.ConfigOptions) *ffuf.ConfigOptions { flag.BoolVar(&opts.Output.OutputSkipEmptyFile, "or", opts.Output.OutputSkipEmptyFile, "Don't create the output file if we don't have results") flag.BoolVar(&opts.General.AutoCalibration, "ac", opts.General.AutoCalibration, "Automatically calibrate filtering options") flag.BoolVar(&opts.General.Colors, "c", opts.General.Colors, "Colorize output.") + flag.BoolVar(&opts.General.Json, "json", opts.General.Json, "JSON output, printing newline-delimited JSON records") flag.BoolVar(&opts.General.Noninteractive, "noninteractive", opts.General.Noninteractive, "Disable the interactive console functionality") flag.BoolVar(&opts.General.Quiet, "s", opts.General.Quiet, "Do not print additional information (silent mode)") flag.BoolVar(&opts.General.ShowVersion, "V", opts.General.ShowVersion, "Show version information.") diff --git a/pkg/ffuf/config.go b/pkg/ffuf/config.go index 3c38194..48fc86c 100644 --- a/pkg/ffuf/config.go +++ b/pkg/ffuf/config.go @@ -26,6 +26,7 @@ type Config struct { InputNum int `json:"cmd_inputnum"` InputProviders []InputProviderConfig `json:"inputproviders"` InputShell string `json:"inputshell"` + Json bool `json:"json"` Matchers map[string]FilterProvider `json:"matchers"` MaxTime int `json:"maxtime"` MaxTimeJob int `json:"maxtime_job"` @@ -79,6 +80,7 @@ func NewConfig(ctx context.Context, cancel context.CancelFunc) Config { conf.InputNum = 0 conf.InputShell = "" conf.InputProviders = make([]InputProviderConfig, 0) + conf.Json = false conf.Matchers = make(map[string]FilterProvider) conf.MaxTime = 0 conf.MaxTimeJob = 0 diff --git a/pkg/ffuf/optionsparser.go b/pkg/ffuf/optionsparser.go index 84b817e..83f834e 100644 --- a/pkg/ffuf/optionsparser.go +++ b/pkg/ffuf/optionsparser.go @@ -49,6 +49,7 @@ type GeneralOptions struct { Colors bool ConfigFile string `toml:"-"` Delay string + Json bool MaxTime int MaxTimeJob int Noninteractive bool @@ -113,6 +114,7 @@ func NewConfigOptions() *ConfigOptions { c.General.AutoCalibration = false c.General.Colors = false c.General.Delay = "" + c.General.Json = false c.General.MaxTime = 0 c.General.MaxTimeJob = 0 c.General.Noninteractive = false @@ -449,6 +451,7 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con conf.MaxTimeJob = parseOpts.General.MaxTimeJob conf.Noninteractive = parseOpts.General.Noninteractive conf.Verbose = parseOpts.General.Verbose + conf.Json = parseOpts.General.Json conf.Http2 = parseOpts.HTTP.Http2 // Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP @@ -483,6 +486,12 @@ func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel con errs.Add(fmt.Errorf(errmsg)) } } + + // Make verbose mutually exclusive with json + if parseOpts.General.Verbose && parseOpts.General.Json { + errs.Add(fmt.Errorf("Cannot have -json and -v")) + } + return &conf, errs.ErrorOrNil() } diff --git a/pkg/output/stdout.go b/pkg/output/stdout.go index 138e361..588decb 100644 --- a/pkg/output/stdout.go +++ b/pkg/output/stdout.go @@ -2,6 +2,7 @@ package output import ( "crypto/md5" + "encoding/json" "fmt" "io/ioutil" "os" @@ -359,15 +360,16 @@ func (s *Stdoutput) writeResultToFile(resp ffuf.Response) string { } func (s *Stdoutput) PrintResult(res ffuf.Result) { - if s.config.Quiet { + switch { + case s.config.Quiet: s.resultQuiet(res) - } else { - if len(res.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0 { - // Print a multi-line result (when using multiple input keywords and wordlists) - s.resultMultiline(res) - } else { - s.resultNormal(res) - } + case s.config.Json: + s.resultJson(res) + case len(res.Input) > 1 || s.config.Verbose || len(s.config.OutputDirectory) > 0: + // Print a multi-line result (when using multiple input keywords and wordlists) + s.resultMultiline(res) + default: + s.resultNormal(res) } } @@ -431,6 +433,16 @@ func (s *Stdoutput) resultNormal(res ffuf.Result) { fmt.Println(resnormal) } +func (s *Stdoutput) resultJson(res ffuf.Result) { + resBytes, err := json.Marshal(res) + if err != nil { + s.Error(err.Error()) + } else { + fmt.Fprint(os.Stderr, TERMINAL_CLEAR_LINE) + fmt.Println(string(resBytes)) + } +} + func (s *Stdoutput) colorize(status int64) string { if !s.config.Colors { return ""