Skip to content

Commit 338000b

Browse files
authored
Add support for exporting report in multiple formats (#256)
* Bump Go version * Update Dockerfile * Add support for exporting report in multiple formats
1 parent b889bcd commit 338000b

File tree

9 files changed

+184
-59
lines changed

9 files changed

+184
-59
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# syntax=docker/dockerfile:1
22

33
# Build Stage ==================================================================
4-
FROM golang:1.22-alpine AS build
4+
FROM golang:1.23-alpine AS build
55

66
RUN apk --no-cache add git
77

cmd/gotestwaf/flags.go

+49-10
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package main
22

33
import (
44
"fmt"
5+
"maps"
56
"os"
67
"path/filepath"
78
"regexp"
9+
"slices"
810
"strings"
911

1012
"github.com/hashicorp/go-multierror"
@@ -16,9 +18,36 @@ import (
1618

1719
"github.com/wallarm/gotestwaf/internal/config"
1820
"github.com/wallarm/gotestwaf/internal/helpers"
21+
"github.com/wallarm/gotestwaf/internal/report"
1922
"github.com/wallarm/gotestwaf/internal/version"
2023
)
2124

25+
const (
26+
textLogFormat = "text"
27+
jsonLogFormat = "json"
28+
)
29+
30+
var (
31+
logFormatsSet = map[string]any{
32+
textLogFormat: nil,
33+
jsonLogFormat: nil,
34+
}
35+
logFormats = slices.Collect(maps.Keys(logFormatsSet))
36+
)
37+
38+
const (
39+
chromeClient = "chrome"
40+
gohttpClient = "gohttp"
41+
)
42+
43+
var (
44+
httpClientsSet = map[string]any{
45+
chromeClient: nil,
46+
gohttpClient: nil,
47+
}
48+
httpClients = slices.Collect(maps.Keys(httpClientsSet))
49+
)
50+
2251
const (
2352
maxReportFilenameLength = 249 // 255 (max length) - 5 (".html") - 1 (to be sure)
2453

@@ -28,19 +57,16 @@ const (
2857
defaultConfigPath = "config.yaml"
2958

3059
wafName = "generic"
60+
)
3161

32-
textLogFormat = "text"
33-
jsonLogFormat = "json"
34-
35-
cliDescription = `GoTestWAF is a tool for API and OWASP attack simulation that supports a
62+
const cliDescription = `GoTestWAF is a tool for API and OWASP attack simulation that supports a
3663
wide range of API protocols including REST, GraphQL, gRPC, SOAP, XMLRPC, and others.
3764
Homepage: https://github.com/wallarm/gotestwaf
3865
3966
Usage: %s [OPTIONS] --url <URL>
4067
4168
Options:
4269
`
43-
)
4470

4571
var (
4672
configPath string
@@ -69,7 +95,7 @@ func parseFlags() (args []string, err error) {
6995
flag.StringVar(&configPath, "configPath", defaultConfigPath, "Path to the config file")
7096
flag.BoolVar(&quiet, "quiet", false, "If true, disable verbose logging")
7197
logLvl := flag.String("logLevel", "info", "Logging level: panic, fatal, error, warn, info, debug, trace")
72-
flag.StringVar(&logFormat, "logFormat", textLogFormat, "Set logging format: text, json")
98+
flag.StringVar(&logFormat, "logFormat", textLogFormat, "Set logging format: "+strings.Join(logFormats, ", "))
7399
showVersion := flag.Bool("version", false, "Show GoTestWAF version and exit")
74100

75101
// Target settings
@@ -84,7 +110,7 @@ func parseFlags() (args []string, err error) {
84110
flag.String("testSet", "", "If set then only this test set's cases will be run")
85111

86112
// HTTP client settings
87-
httpClient := flag.String("httpClient", "gohttp", "Which HTTP client use to send requests: chrome, gohttp")
113+
httpClient := flag.String("httpClient", gohttpClient, "Which HTTP client use to send requests: "+strings.Join(httpClients, ", "))
88114
flag.Bool("tlsVerify", false, "If true, the received TLS certificate will be verified")
89115
flag.String("proxy", "", "Proxy URL to use")
90116
flag.String("addHeader", "", "An HTTP header to add to requests")
@@ -121,7 +147,7 @@ func parseFlags() (args []string, err error) {
121147
flag.Bool("includePayloads", false, "If true, payloads will be included in HTML/PDF report")
122148
flag.String("reportPath", reportPath, "A directory to store reports")
123149
reportName := flag.String("reportName", defaultReportName, "Report file name. Supports `time' package template format")
124-
flag.String("reportFormat", "pdf", "Export report to one of the following formats: none, pdf, html, json")
150+
reportFormat := flag.StringSlice("reportFormat", []string{report.PdfFormat}, "Export report in the following formats: "+strings.Join(report.ReportFormats, ", "))
125151
noEmailReport := flag.Bool("noEmailReport", false, "Save report locally")
126152
email := flag.String("email", "", "E-mail to which the report will be sent")
127153

@@ -166,8 +192,16 @@ func parseFlags() (args []string, err error) {
166192
}
167193
logLevel = logrusLogLvl
168194

169-
if logFormat != textLogFormat && logFormat != jsonLogFormat {
170-
return nil, fmt.Errorf("unknown logging format: %s", logFormat)
195+
if err = validateLogFormat(logFormat); err != nil {
196+
return nil, err
197+
}
198+
199+
if err = validateHttpClient(*httpClient); err != nil {
200+
return nil, err
201+
}
202+
203+
if err = report.ValidateReportFormat(*reportFormat); err != nil {
204+
return nil, err
171205
}
172206

173207
validURL, err := validateURL(*urlParam, httpProto)
@@ -261,6 +295,11 @@ func normalizeArgs() ([]string, error) {
261295

262296
arg = fmt.Sprintf("--%s=%s", f.Name, value)
263297

298+
case "stringSlice":
299+
// remove square brackets: [pdf,json] -> pdf,json
300+
value = strings.Trim(f.Value.String(), "[]")
301+
arg = fmt.Sprintf("--%s=%s", f.Name, value)
302+
264303
case "bool":
265304
arg = fmt.Sprintf("--%s", f.Name)
266305

cmd/gotestwaf/helpers.go

+16
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,19 @@ func checkOrCraftProtocolURL(rawURL string, validHttpURL string, protocol string
6565

6666
return validURL, nil
6767
}
68+
69+
func validateHttpClient(httpClient string) error {
70+
if _, ok := httpClientsSet[httpClient]; !ok {
71+
return fmt.Errorf("invalid HTTP client: %s", httpClient)
72+
}
73+
74+
return nil
75+
}
76+
77+
func validateLogFormat(logFormat string) error {
78+
if _, ok := logFormatsSet[logFormat]; !ok {
79+
return fmt.Errorf("invalid log format: %s", logFormat)
80+
}
81+
82+
return nil
83+
}

cmd/gotestwaf/main.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,12 @@ func run(ctx context.Context, cfg *config.Config, logger *logrus.Logger) error {
192192
return err
193193
}
194194

195-
if cfg.ReportFormat == report.NoneFormat {
195+
if report.IsNoneReportFormat(cfg.ReportFormat) {
196196
return nil
197197
}
198198

199199
includePayloads := cfg.IncludePayloads
200-
if cfg.ReportFormat == report.HtmlFormat || cfg.ReportFormat == report.PdfFormat {
200+
if report.IsPdfOrHtmlReportFormat(cfg.ReportFormat) {
201201
askForPayloads := true
202202

203203
// If the cfg.IncludePayloads is already explicitly set by the user OR
@@ -219,7 +219,7 @@ func run(ctx context.Context, cfg *config.Config, logger *logrus.Logger) error {
219219
}
220220
}
221221

222-
reportFile, err = report.ExportFullReport(
222+
reportFiles, err := report.ExportFullReport(
223223
ctx, stat, reportFile,
224224
reportTime, cfg.WAFName, cfg.URL, cfg.OpenAPIFile, cfg.Args,
225225
cfg.IgnoreUnresolved, includePayloads, cfg.ReportFormat,
@@ -228,7 +228,10 @@ func run(ctx context.Context, cfg *config.Config, logger *logrus.Logger) error {
228228
return errors.Wrap(err, "couldn't export full report")
229229
}
230230

231-
logger.WithField("filename", reportFile).Infof("Export full report")
231+
for _, file := range reportFiles {
232+
reportExt := strings.ToUpper(strings.Trim(filepath.Ext(file), "."))
233+
logger.WithField("filename", file).Infof("Export %s full report", reportExt)
234+
}
232235

233236
payloadFiles := filepath.Join(cfg.ReportPath, reportName+".csv")
234237
err = db.ExportPayloads(payloadFiles)

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/wallarm/gotestwaf
22

3-
go 1.22
3+
go 1.23
44

55
require (
66
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476

go.sum

-8
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,9 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
3939
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4040
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
4141
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
42-
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
43-
github.com/chromedp/cdproto v0.0.0-20240501202034-ef67d660e9fd h1:5/HXKq8EaAWVmnl6Hnyl4SVq7FF5990DBW6AuTrWtVw=
44-
github.com/chromedp/cdproto v0.0.0-20240501202034-ef67d660e9fd/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
4542
github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
4643
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
4744
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
48-
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
49-
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
5045
github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKab1E=
5146
github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE=
5247
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
@@ -99,7 +94,6 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
9994
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
10095
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
10196
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
102-
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
10397
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
10498
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
10599
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -412,8 +406,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
412406
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
413407
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
414408
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
415-
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
416-
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
417409
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
418410
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
419411
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

internal/config/config.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ type Config struct {
4747
BlockConnReset bool `mapstructure:"blockConnReset"`
4848

4949
// Report settings
50-
WAFName string `mapstructure:"wafName"`
51-
IncludePayloads bool `mapstructure:"includePayloads"`
52-
ReportPath string `mapstructure:"reportPath"`
53-
ReportName string `mapstructure:"reportName"`
54-
ReportFormat string `mapstructure:"reportFormat"`
55-
NoEmailReport bool `mapstructure:"noEmailReport"`
56-
Email string `mapstructure:"email"`
50+
WAFName string `mapstructure:"wafName"`
51+
IncludePayloads bool `mapstructure:"includePayloads"`
52+
ReportPath string `mapstructure:"reportPath"`
53+
ReportName string `mapstructure:"reportName"`
54+
ReportFormat []string `mapstructure:"reportFormat"`
55+
NoEmailReport bool `mapstructure:"noEmailReport"`
56+
Email string `mapstructure:"email"`
5757

5858
// config.yaml
5959
HTTPHeaders map[string]string `mapstructure:"headers"`

0 commit comments

Comments
 (0)