diff --git a/api/curl.go b/api/curl.go index da70274..031e79d 100644 --- a/api/curl.go +++ b/api/curl.go @@ -33,9 +33,14 @@ func (h *Handler) ScanURL(ctx *gin.Context) { opts.Cookies = ctx.Request.Cookies() client := request.NewClient(opts) - s, err := scenario.NewURLScan(form.Method, form.URL, form.Data, client, &scan.ScanOptions{ - IncludeScans: form.Opts.Scans, - ExcludeScans: form.Opts.ExcludeScans, + s, err := scenario.NewURLScan(form.Method, form.URL, form.Data, client, nil, &scan.ScanOptions{ + IncludeScans: form.Opts.Scans, + ExcludeScans: form.Opts.ExcludeScans, + MinIssueSeverity: form.Opts.MinSeverity, + IncludeCWEs: form.Opts.IncludeCWEs, + ExcludeCWEs: form.Opts.ExcludeCWEs, + IncludeOWASPs: form.Opts.IncludeOWASPs, + ExcludeOWASPs: form.Opts.ExcludeOWASPs, }) if err != nil { span.RecordError(err) diff --git a/api/graphql.go b/api/graphql.go index 9da0829..cb1112b 100644 --- a/api/graphql.go +++ b/api/graphql.go @@ -32,9 +32,14 @@ func (h *Handler) ScanGraphQL(ctx *gin.Context) { opts.Cookies = ctx.Request.Cookies() client := request.NewClient(opts) - s, err := scenario.NewGraphQLScan(form.Endpoint, client, &scan.ScanOptions{ - IncludeScans: form.Opts.Scans, - ExcludeScans: form.Opts.ExcludeScans, + s, err := scenario.NewGraphQLScan(form.Endpoint, client, nil, &scan.ScanOptions{ + IncludeScans: form.Opts.Scans, + ExcludeScans: form.Opts.ExcludeScans, + MinIssueSeverity: form.Opts.MinSeverity, + IncludeCWEs: form.Opts.IncludeCWEs, + ExcludeCWEs: form.Opts.ExcludeCWEs, + IncludeOWASPs: form.Opts.IncludeOWASPs, + ExcludeOWASPs: form.Opts.ExcludeOWASPs, }) if err != nil { span.RecordError(err) diff --git a/api/openapi.go b/api/openapi.go index c500d3c..6a906d8 100644 --- a/api/openapi.go +++ b/api/openapi.go @@ -59,9 +59,14 @@ func (h *Handler) ScanOpenAPI(ctx *gin.Context) { } } securitySchemesValues := openapi.NewSecuritySchemeValues(values) - s, err := scenario.NewOpenAPIScan(doc, securitySchemesValues, client, &scan.ScanOptions{ - IncludeScans: form.Opts.Scans, - ExcludeScans: form.Opts.ExcludeScans, + s, err := scenario.NewOpenAPIScan(doc, securitySchemesValues, client, nil, &scan.ScanOptions{ + IncludeScans: form.Opts.Scans, + ExcludeScans: form.Opts.ExcludeScans, + MinIssueSeverity: form.Opts.MinSeverity, + IncludeCWEs: form.Opts.IncludeCWEs, + ExcludeCWEs: form.Opts.ExcludeCWEs, + IncludeOWASPs: form.Opts.IncludeOWASPs, + ExcludeOWASPs: form.Opts.ExcludeOWASPs, }) if err != nil { span.RecordError(err) diff --git a/api/request.go b/api/request.go index 98b1c15..d951c4c 100644 --- a/api/request.go +++ b/api/request.go @@ -10,8 +10,13 @@ type ScanOptions struct { RateLimit int `json:"rateLimit"` ProxyURL string `json:"proxy"` - Scans []string `json:"scans"` - ExcludeScans []string `json:"excludeScans"` + Scans []string `json:"scans"` + ExcludeScans []string `json:"excludeScans"` + MinSeverity float64 `json:"minSeverity"` + IncludeCWEs []string `json:"includeCWEs"` + ExcludeCWEs []string `json:"excludeCWEs"` + IncludeOWASPs []string `json:"includeOWASPs"` + ExcludeOWASPs []string `json:"excludeOWASPs"` } func parseScanOptions(opts *ScanOptions) request.NewClientOptions { diff --git a/cmd/scan/curl.go b/cmd/scan/curl.go index 0b13a2f..d45a21d 100644 --- a/cmd/scan/curl.go +++ b/cmd/scan/curl.go @@ -41,9 +41,14 @@ func NewCURLScanCmd() (scanCmd *cobra.Command) { } request.SetDefaultClient(client) - s, err := scenario.NewURLScan(curlMethod, curlUrl, curlData, client, &scan.ScanOptions{ - IncludeScans: internalCmd.GetIncludeScans(), - ExcludeScans: internalCmd.GetExcludeScans(), + s, err := scenario.NewURLScan(curlMethod, curlUrl, curlData, client, nil, &scan.ScanOptions{ + IncludeScans: internalCmd.GetIncludeScans(), + ExcludeScans: internalCmd.GetExcludeScans(), + MinIssueSeverity: internalCmd.GetScanMinIssueSeverity(), + IncludeCWEs: internalCmd.GetScanIncludeCWEs(), + ExcludeCWEs: internalCmd.GetScanExcludeCWEs(), + IncludeOWASPs: internalCmd.GetScanIncludeOWASPs(), + ExcludeOWASPs: internalCmd.GetScanExcludeOWASPs(), }) if err != nil { span.RecordError(err) diff --git a/cmd/scan/graphql.go b/cmd/scan/graphql.go index 9a68444..347539c 100644 --- a/cmd/scan/graphql.go +++ b/cmd/scan/graphql.go @@ -35,9 +35,14 @@ func NewGraphQLScanCmd() (scanCmd *cobra.Command) { } request.SetDefaultClient(client) - s, err := scenario.NewGraphQLScan(graphqlEndpoint, client, &scan.ScanOptions{ - IncludeScans: internalCmd.GetIncludeScans(), - ExcludeScans: internalCmd.GetExcludeScans(), + s, err := scenario.NewGraphQLScan(graphqlEndpoint, client, nil, &scan.ScanOptions{ + IncludeScans: internalCmd.GetIncludeScans(), + ExcludeScans: internalCmd.GetExcludeScans(), + MinIssueSeverity: internalCmd.GetScanMinIssueSeverity(), + IncludeCWEs: internalCmd.GetScanIncludeCWEs(), + ExcludeCWEs: internalCmd.GetScanExcludeCWEs(), + IncludeOWASPs: internalCmd.GetScanIncludeOWASPs(), + ExcludeOWASPs: internalCmd.GetScanExcludeOWASPs(), }) if err != nil { span.RecordError(err) diff --git a/cmd/scan/openapi.go b/cmd/scan/openapi.go index bfc15e7..0be158f 100644 --- a/cmd/scan/openapi.go +++ b/cmd/scan/openapi.go @@ -78,9 +78,14 @@ func NewOpenAPIScanCmd() (scanCmd *cobra.Command) { } request.SetDefaultClient(client) - s, err := scenario.NewOpenAPIScan(doc, securitySchemesValues, client, &scan.ScanOptions{ - IncludeScans: internalCmd.GetIncludeScans(), - ExcludeScans: internalCmd.GetExcludeScans(), + s, err := scenario.NewOpenAPIScan(doc, securitySchemesValues, client, nil, &scan.ScanOptions{ + IncludeScans: internalCmd.GetIncludeScans(), + ExcludeScans: internalCmd.GetExcludeScans(), + MinIssueSeverity: internalCmd.GetScanMinIssueSeverity(), + IncludeCWEs: internalCmd.GetScanIncludeCWEs(), + ExcludeCWEs: internalCmd.GetScanExcludeCWEs(), + IncludeOWASPs: internalCmd.GetScanIncludeOWASPs(), + ExcludeOWASPs: internalCmd.GetScanExcludeOWASPs(), }) if err != nil { span.RecordError(err) diff --git a/internal/cmd/args.go b/internal/cmd/args.go index 08e9f47..3b572b8 100644 --- a/internal/cmd/args.go +++ b/internal/cmd/args.go @@ -25,6 +25,12 @@ var ( noProgress bool severityThreshold float64 + scanMinIssueSeverity float64 + scanIncludeCWEs []string + scanExcludeCWEs []string + scanIncludeOWASPs []string + scanExcludeOWASPs []string + placeholderString string placeholderBool bool ) @@ -48,6 +54,12 @@ func AddCommonArgs(cmd *cobra.Command) { cmd.Flags().BoolVarP(&noProgress, "no-progress", "", false, "Disable progress output") cmd.Flags().Float64VarP(&severityThreshold, "severity-threshold", "", 1, "Threshold to trigger stderr output if at least one vulnerability CVSS is higher") + + cmd.Flags().Float64VarP(&scanMinIssueSeverity, "scan-min-severity", "", 0, "Minimum severity score (CVSS) to report an issue") + cmd.Flags().StringArrayVarP(&scanIncludeCWEs, "scan-include-cwe", "", scanIncludeCWEs, "Include specific CWEs (e.g., CWE-200, CWE-22)") + cmd.Flags().StringArrayVarP(&scanExcludeCWEs, "scan-exclude-cwe", "", scanExcludeCWEs, "Exclude specific CWEs (e.g., CWE-200, CWE-22)") + cmd.Flags().StringArrayVarP(&scanIncludeOWASPs, "scan-include-owasp", "", scanIncludeOWASPs, "Include specific OWASP") + cmd.Flags().StringArrayVarP(&scanExcludeOWASPs, "scan-exclude-owasp", "", scanExcludeOWASPs, "Exclude specific OWASP") } func AddPlaceholderArgs(cmd *cobra.Command) { @@ -126,6 +138,26 @@ func GetSeverityThreshold() float64 { return severityThreshold } +func GetScanMinIssueSeverity() float64 { + return scanMinIssueSeverity +} + +func GetScanIncludeCWEs() []string { + return scanIncludeCWEs +} + +func GetScanExcludeCWEs() []string { + return scanExcludeCWEs +} + +func GetScanIncludeOWASPs() []string { + return scanIncludeOWASPs +} + +func GetScanExcludeOWASPs() []string { + return scanExcludeOWASPs +} + func basicAuth(user string) string { credentials := strings.Split(user, ":") if len(credentials) != 2 || credentials[0] == "" { @@ -148,4 +180,9 @@ func ClearValues() { reportURL = "" noProgress = false severityThreshold = 1 + scanMinIssueSeverity = 0 + scanIncludeCWEs = []string{} + scanExcludeCWEs = []string{} + scanIncludeOWASPs = []string{} + scanExcludeOWASPs = []string{} } diff --git a/internal/cmd/args_test.go b/internal/cmd/args_test.go index bef3da9..2daa153 100644 --- a/internal/cmd/args_test.go +++ b/internal/cmd/args_test.go @@ -13,52 +13,67 @@ func TestAddCommonArgs(t *testing.T) { name string args []string expected struct { - rateLimit string - proxy string - headers []string - cookies []string - authUser string - includeScans []string - excludeScans []string - outputFormat string - outputTransport string - outputPath string - outputURL string - noProgress bool - severityThreshold float64 + rateLimit string + proxy string + headers []string + cookies []string + authUser string + includeScans []string + excludeScans []string + outputFormat string + outputTransport string + outputPath string + outputURL string + noProgress bool + severityThreshold float64 + scanMinIssueSeverity float64 + scanIncludeCWEs []string + scanExcludeCWEs []string + scanIncludeOWASPs []string + scanExcludeOWASPs []string } }{ { name: "default values", args: []string{}, expected: struct { - rateLimit string - proxy string - headers []string - cookies []string - authUser string - includeScans []string - excludeScans []string - outputFormat string - outputTransport string - outputPath string - outputURL string - noProgress bool - severityThreshold float64 + rateLimit string + proxy string + headers []string + cookies []string + authUser string + includeScans []string + excludeScans []string + outputFormat string + outputTransport string + outputPath string + outputURL string + noProgress bool + severityThreshold float64 + scanMinIssueSeverity float64 + scanIncludeCWEs []string + scanExcludeCWEs []string + scanIncludeOWASPs []string + scanExcludeOWASPs []string }{ - rateLimit: "10/s", - proxy: "", - headers: []string{}, - cookies: []string{}, - authUser: "", - includeScans: nil, - excludeScans: nil, - outputFormat: "table", - outputTransport: "file", - outputPath: "", - outputURL: "", - noProgress: false, - severityThreshold: 1, + rateLimit: "10/s", + proxy: "", + headers: []string{}, + cookies: []string{}, + authUser: "", + includeScans: nil, + excludeScans: nil, + outputFormat: "table", + outputTransport: "file", + outputPath: "", + outputURL: "", + noProgress: false, + severityThreshold: 1, + scanMinIssueSeverity: 0, + scanIncludeCWEs: []string{}, + scanExcludeCWEs: []string{}, + scanIncludeOWASPs: []string{}, + scanExcludeOWASPs: []string{}, }, }, { @@ -69,33 +84,43 @@ func TestAddCommonArgs(t *testing.T) { "--scans=scan2", }, expected: struct { - rateLimit string - proxy string - headers []string - cookies []string - authUser string - includeScans []string - excludeScans []string - outputFormat string - outputTransport string - outputPath string - outputURL string - noProgress bool - severityThreshold float64 + rateLimit string + proxy string + headers []string + cookies []string + authUser string + includeScans []string + excludeScans []string + outputFormat string + outputTransport string + outputPath string + outputURL string + noProgress bool + severityThreshold float64 + scanMinIssueSeverity float64 + scanIncludeCWEs []string + scanExcludeCWEs []string + scanIncludeOWASPs []string + scanExcludeOWASPs []string }{ - rateLimit: "10/s", - proxy: "", - headers: []string{"Authorization: Basic dXNlcjpwYXNzd29yZA=="}, - cookies: []string{}, - authUser: "user:password", - includeScans: []string{"scan1", "scan2"}, - excludeScans: nil, - outputFormat: "table", - outputTransport: "file", - outputPath: "", - outputURL: "", - noProgress: false, - severityThreshold: 1, + rateLimit: "10/s", + proxy: "", + headers: []string{"Authorization: Basic dXNlcjpwYXNzd29yZA=="}, + cookies: []string{}, + authUser: "user:password", + includeScans: []string{"scan1", "scan2"}, + excludeScans: nil, + outputFormat: "table", + outputTransport: "file", + outputPath: "", + outputURL: "", + noProgress: false, + severityThreshold: 1, + scanMinIssueSeverity: 0, + scanIncludeCWEs: []string{}, + scanExcludeCWEs: []string{}, + scanIncludeOWASPs: []string{}, + scanExcludeOWASPs: []string{}, }, }, { @@ -106,33 +131,43 @@ func TestAddCommonArgs(t *testing.T) { "--scans=scan2", }, expected: struct { - rateLimit string - proxy string - headers []string - cookies []string - authUser string - includeScans []string - excludeScans []string - outputFormat string - outputTransport string - outputPath string - outputURL string - noProgress bool - severityThreshold float64 + rateLimit string + proxy string + headers []string + cookies []string + authUser string + includeScans []string + excludeScans []string + outputFormat string + outputTransport string + outputPath string + outputURL string + noProgress bool + severityThreshold float64 + scanMinIssueSeverity float64 + scanIncludeCWEs []string + scanExcludeCWEs []string + scanIncludeOWASPs []string + scanExcludeOWASPs []string }{ - rateLimit: "10/s", - proxy: "", - headers: []string{}, - cookies: []string{}, - authUser: "user", - includeScans: []string{"scan1", "scan2"}, - excludeScans: nil, - outputFormat: "table", - outputTransport: "file", - outputPath: "", - outputURL: "", - noProgress: false, - severityThreshold: 1, + rateLimit: "10/s", + proxy: "", + headers: []string{}, + cookies: []string{}, + authUser: "user", + includeScans: []string{"scan1", "scan2"}, + excludeScans: nil, + outputFormat: "table", + outputTransport: "file", + outputPath: "", + outputURL: "", + noProgress: false, + severityThreshold: 1, + scanMinIssueSeverity: 0, + scanIncludeCWEs: []string{}, + scanExcludeCWEs: []string{}, + scanIncludeOWASPs: []string{}, + scanExcludeOWASPs: []string{}, }, }, { @@ -150,35 +185,54 @@ func TestAddCommonArgs(t *testing.T) { "--report-url=http://example.com/output", "--no-progress", "--severity-threshold=5", + + "--scan-min-severity=3", + "--scan-include-cwe=CWE-123", + "--scan-include-cwe=CWE-456", + "--scan-exclude-cwe=CWE-789", + "--scan-include-owasp=OWASP-A1", + "--scan-include-owasp=OWASP-A2", + "--scan-exclude-owasp=OWASP-B1", + "--scan-exclude-owasp=OWASP-B2", }, expected: struct { - rateLimit string - proxy string - headers []string - cookies []string - authUser string - includeScans []string - excludeScans []string - outputFormat string - outputTransport string - outputPath string - outputURL string - noProgress bool - severityThreshold float64 + rateLimit string + proxy string + headers []string + cookies []string + authUser string + includeScans []string + excludeScans []string + outputFormat string + outputTransport string + outputPath string + outputURL string + noProgress bool + severityThreshold float64 + scanMinIssueSeverity float64 + scanIncludeCWEs []string + scanExcludeCWEs []string + scanIncludeOWASPs []string + scanExcludeOWASPs []string }{ - rateLimit: "5/m", - proxy: "http://proxy.example.com", - headers: []string{"Authorization: Bearer token"}, - cookies: []string{"sessionid=12345"}, - authUser: "", - includeScans: []string{"scan1", "scan2"}, - excludeScans: nil, - outputFormat: "json", - outputTransport: "http", - outputPath: "/tmp/output", - outputURL: "http://example.com/output", - noProgress: true, - severityThreshold: 5, + rateLimit: "5/m", + proxy: "http://proxy.example.com", + headers: []string{"Authorization: Bearer token"}, + cookies: []string{"sessionid=12345"}, + authUser: "", + includeScans: []string{"scan1", "scan2"}, + excludeScans: nil, + outputFormat: "json", + outputTransport: "http", + outputPath: "/tmp/output", + outputURL: "http://example.com/output", + noProgress: true, + severityThreshold: 5, + scanMinIssueSeverity: 3, + scanIncludeCWEs: []string{"CWE-123", "CWE-456"}, + scanExcludeCWEs: []string{"CWE-789"}, + scanIncludeOWASPs: []string{"OWASP-A1", "OWASP-A2"}, + scanExcludeOWASPs: []string{"OWASP-B1", "OWASP-B2"}, }, }, } @@ -202,6 +256,11 @@ func TestAddCommonArgs(t *testing.T) { assert.Equal(t, tt.expected.outputTransport, cmd.GetReportTransport()) assert.Equal(t, tt.expected.noProgress, cmd.GetNoProgress()) assert.Equal(t, tt.expected.severityThreshold, cmd.GetSeverityThreshold()) + assert.Equal(t, tt.expected.scanMinIssueSeverity, cmd.GetScanMinIssueSeverity()) + assert.Equal(t, tt.expected.scanIncludeCWEs, cmd.GetScanIncludeCWEs()) + assert.Equal(t, tt.expected.scanExcludeCWEs, cmd.GetScanExcludeCWEs()) + assert.Equal(t, tt.expected.scanIncludeOWASPs, cmd.GetScanIncludeOWASPs()) + assert.Equal(t, tt.expected.scanExcludeOWASPs, cmd.GetScanExcludeOWASPs()) }) } } diff --git a/report/options_report.go b/report/options_report.go deleted file mode 100644 index 5773b28..0000000 --- a/report/options_report.go +++ /dev/null @@ -1,7 +0,0 @@ -package report - -type OptionsReport struct{} // TODO - -func NewOptionsReport() OptionsReport { - return OptionsReport{} -} diff --git a/report/report_options.go b/report/report_options.go new file mode 100644 index 0000000..61ac9bc --- /dev/null +++ b/report/report_options.go @@ -0,0 +1,16 @@ +package report + +type ReportOptions struct { + ScansIncluded []string `json:"scansIncluded,omitempty" yaml:"scansIncluded,omitempty"` + ScansExcluded []string `json:"scansExcluded,omitempty" yaml:"scansExcluded,omitempty"` + + MinIssueSeverity float64 `json:"minSeverity,omitempty" yaml:"minSeverity,omitempty"` + CWEsIncluded []string `json:"CWEsIncluded,omitempty" yaml:"CWEsIncluded,omitempty"` + CWEsExcluded []string `json:"CWEsExcluded,omitempty" yaml:"CWEsExcluded,omitempty"` + OWASPsIncluded []string `json:"OWASPsIncluded,omitempty" yaml:"OWASPsIncluded,omitempty"` + OWASPsExcluded []string `json:"OWASPsExcluded,omitempty" yaml:"OWASPsExcluded,omitempty"` +} + +func NewEmptyOptionsReport() *ReportOptions { + return &ReportOptions{} +} diff --git a/report/report_options_test.go b/report/report_options_test.go new file mode 100644 index 0000000..b5822d8 --- /dev/null +++ b/report/report_options_test.go @@ -0,0 +1,15 @@ +package report_test + +import ( + "testing" + + "github.com/cerberauth/vulnapi/report" + "github.com/stretchr/testify/assert" +) + +func TestNewEmptyOptionsReport(t *testing.T) { + optionsReport := report.NewEmptyOptionsReport() + + assert.NotNil(t, optionsReport) + assert.Nil(t, optionsReport.ScansIncluded) +} diff --git a/report/reporter.go b/report/reporter.go index 3d96ef3..93fcf2e 100644 --- a/report/reporter.go +++ b/report/reporter.go @@ -13,7 +13,7 @@ const reporterSchema = "https://schemas.cerberauth.com/vulnapi/draft/2024-10/rep type Reporter struct { Schema string `json:"$schema" yaml:"$schema"` - Options OptionsReport `json:"options" yaml:"options"` + Options *ReportOptions `json:"options" yaml:"options"` Curl *CurlReport `json:"curl,omitempty" yaml:"curl,omitempty"` OpenAPI *OpenAPIReport `json:"openapi,omitempty" yaml:"openapi,omitempty"` GraphQL *GraphQLReport `json:"graphql,omitempty" yaml:"graphql,omitempty"` @@ -24,7 +24,7 @@ func NewReporter() *Reporter { return &Reporter{ Schema: reporterSchema, - Options: NewOptionsReport(), + Options: NewEmptyOptionsReport(), ScanReports: []*ScanReport{}, } } @@ -33,7 +33,7 @@ func NewReporterWithCurl(method string, url string, data interface{}, header htt return &Reporter{ Schema: reporterSchema, - Options: NewOptionsReport(), + Options: NewEmptyOptionsReport(), Curl: NewCurlReport(method, url, data, header, cookies, securitySchemes), ScanReports: []*ScanReport{}, } @@ -43,7 +43,7 @@ func NewReporterWithOpenAPIDoc(openapi *openapi3.T, operations operation.Operati return &Reporter{ Schema: reporterSchema, - Options: NewOptionsReport(), + Options: NewEmptyOptionsReport(), OpenAPI: NewOpenAPIReport(openapi, operations), ScanReports: []*ScanReport{}, } @@ -53,7 +53,7 @@ func NewReporterWithGraphQL(url string, securitySchemes []*auth.SecurityScheme) return &Reporter{ Schema: reporterSchema, - Options: NewOptionsReport(), + Options: NewEmptyOptionsReport(), GraphQL: NewGraphQLReport(url, securitySchemes), ScanReports: []*ScanReport{}, } @@ -126,6 +126,10 @@ func (rr *Reporter) GetIssueReports() []*IssueReport { return reports } +func (rr *Reporter) GetOptions() *ReportOptions { + return rr.Options +} + func (rr *Reporter) GetFailedIssueReports() []*IssueReport { var reports []*IssueReport for _, r := range rr.GetScanReports() { diff --git a/report/reporter_test.go b/report/reporter_test.go index 9f7610b..e932076 100644 --- a/report/reporter_test.go +++ b/report/reporter_test.go @@ -149,6 +149,12 @@ func TestReporter_HasVulnerability_WhenFailedReport(t *testing.T) { assert.True(t, reporter.HasIssue()) } +func TestGetOptions(t *testing.T) { + reporter := report.NewReporter() + options := reporter.GetOptions() + assert.NotNil(t, options) +} + func TestReporters_HasHighRiskOrHigherSeverityVulnerability_WhenLowRiskReport(t *testing.T) { reporter := report.NewReporter() operation := operation.MustNewOperation(http.MethodPost, "http://localhost:8080/", nil, nil) diff --git a/scan/operation_scan.go b/scan/operation_scan.go index 656cdfb..ac3e43c 100644 --- a/scan/operation_scan.go +++ b/scan/operation_scan.go @@ -11,6 +11,8 @@ type OperationScanHandlerFunc func(operation *operation.Operation, securitySchem type OperationScanHandler struct { ID string Handler OperationScanHandlerFunc + + PotentialIssues []report.Issue } type OperationScan struct { @@ -18,9 +20,11 @@ type OperationScan struct { ScanHandler *OperationScanHandler } -func NewOperationScanHandler(id string, handler OperationScanHandlerFunc) *OperationScanHandler { +func NewOperationScanHandler(id string, handler OperationScanHandlerFunc, potentialIssues []report.Issue) *OperationScanHandler { return &OperationScanHandler{ ID: id, Handler: handler, + + PotentialIssues: potentialIssues, } } diff --git a/scan/operation_scan_test.go b/scan/operation_scan_test.go index 5cc7087..44e7e34 100644 --- a/scan/operation_scan_test.go +++ b/scan/operation_scan_test.go @@ -16,9 +16,10 @@ func TestNewOperationScanHandler(t *testing.T) { } handlerID := "test-handler" - handler := scan.NewOperationScanHandler(handlerID, handlerFunc) + handler := scan.NewOperationScanHandler(handlerID, handlerFunc, []report.Issue{}) assert.NotNil(t, handler) assert.Equal(t, handlerID, handler.ID) assert.NotNil(t, handler.Handler) + assert.Empty(t, handler.PotentialIssues) } diff --git a/scan/scan.go b/scan/scan.go index 3dc8031..9a5ae9e 100644 --- a/scan/scan.go +++ b/scan/scan.go @@ -15,11 +15,17 @@ import ( type ScanOptions struct { IncludeScans []string ExcludeScans []string - Reporter *report.Reporter + + MinIssueSeverity float64 + IncludeCWEs []string + ExcludeCWEs []string + IncludeOWASPs []string + ExcludeOWASPs []string } type Scan struct { *ScanOptions + Reporter *report.Reporter Operations operation.Operations OperationsScans []OperationScan @@ -27,7 +33,7 @@ type Scan struct { var tracer = otel.Tracer("scan") -func NewScan(operations operation.Operations, opts *ScanOptions) (*Scan, error) { +func NewScan(operations operation.Operations, reporter *report.Reporter, opts *ScanOptions) (*Scan, error) { if len(operations) == 0 { return nil, fmt.Errorf("a scan must have at least one operation") } @@ -36,12 +42,13 @@ func NewScan(operations operation.Operations, opts *ScanOptions) (*Scan, error) opts = &ScanOptions{} } - if opts.Reporter == nil { - opts.Reporter = report.NewReporter() + if reporter == nil { + reporter = report.NewReporter() } return &Scan{ ScanOptions: opts, + Reporter: reporter, Operations: operations, OperationsScans: []OperationScan{}, @@ -53,7 +60,7 @@ func (s *Scan) GetOperationsScans() []OperationScan { } func (s *Scan) AddOperationScanHandler(handler *OperationScanHandler) *Scan { - if !s.shouldAddScan(handler.ID) { + if !s.shouldAddScan(handler) { return s } @@ -67,7 +74,7 @@ func (s *Scan) AddOperationScanHandler(handler *OperationScanHandler) *Scan { } func (s *Scan) AddScanHandler(handler *OperationScanHandler) *Scan { - if !s.shouldAddScan(handler.ID) { + if !s.shouldAddScan(handler) { return s } @@ -121,20 +128,57 @@ func (s *Scan) Execute(ctx context.Context, scanCallback func(operationScan *Ope operationSpan.End() } + s.Reporter.Options.ScansIncluded = s.IncludeScans + s.Reporter.Options.ScansExcluded = s.ExcludeScans + s.Reporter.Options.MinIssueSeverity = s.MinIssueSeverity + s.Reporter.Options.CWEsIncluded = s.IncludeCWEs + s.Reporter.Options.CWEsExcluded = s.ExcludeCWEs + s.Reporter.Options.OWASPsIncluded = s.IncludeOWASPs + s.Reporter.Options.OWASPsExcluded = s.ExcludeOWASPs + return s.Reporter, errors, nil } -func (s *Scan) shouldAddScan(scanID string) bool { +func (s *Scan) shouldAddScan(handler *OperationScanHandler) bool { // Check if the scan should be excluded - if len(s.ExcludeScans) > 0 && contains(s.ExcludeScans, scanID) { + if len(s.ExcludeScans) > 0 && contains(s.ExcludeScans, handler.ID) { return false } // Check if the scan should be included - if len(s.IncludeScans) > 0 && !contains(s.IncludeScans, scanID) { + if len(s.IncludeScans) > 0 && !contains(s.IncludeScans, handler.ID) { return false } + for _, issue := range handler.PotentialIssues { + // Check if the scan's potential issues match the min severity + if issue.CVSS.Score < s.MinIssueSeverity { + return false + } + + if issue.Classifications == nil { + continue + } + + // Check if the scan's potential issues match CWE classification + if len(s.IncludeCWEs) > 0 && !contains(s.IncludeCWEs, string(issue.Classifications.CWE)) { + return false + } + + if len(s.ExcludeCWEs) > 0 && contains(s.ExcludeCWEs, string(issue.Classifications.CWE)) { + return false + } + + // Check if the scan's potential issues match OWASP classification + if len(s.IncludeOWASPs) > 0 && !contains(s.IncludeOWASPs, string(issue.Classifications.OWASP)) { + return false + } + + if len(s.ExcludeOWASPs) > 0 && !contains(s.ExcludeOWASPs, string(issue.Classifications.OWASP)) { + return false + } + } + return true } diff --git a/scan/scan_test.go b/scan/scan_test.go index 39565cc..0dae04b 100644 --- a/scan/scan_test.go +++ b/scan/scan_test.go @@ -14,7 +14,7 @@ import ( ) func TestNewScanWithNoOperations(t *testing.T) { - _, err := scan.NewScan(operation.Operations{}, nil) + _, err := scan.NewScan(operation.Operations{}, nil, nil) require.Error(t, err) } @@ -23,15 +23,14 @@ func TestNewScan(t *testing.T) { op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) operations := operation.Operations{op} expected := scan.Scan{ - ScanOptions: &scan.ScanOptions{ - Reporter: report.NewReporter(), - }, + ScanOptions: &scan.ScanOptions{}, + Reporter: report.NewReporter(), Operations: operations, OperationsScans: []scan.OperationScan{}, } - s, err := scan.NewScan(operations, nil) + s, err := scan.NewScan(operations, nil, nil) require.NoError(t, err) assert.Equal(t, expected.Operations, s.Operations) @@ -42,17 +41,17 @@ func TestNewScan(t *testing.T) { func TestNewScanWithOptions(t *testing.T) { op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) operations := operation.Operations{op} - opts := &scan.ScanOptions{ - Reporter: report.NewReporter(), - } + opts := &scan.ScanOptions{} + reporter := report.NewReporter() expected := scan.Scan{ ScanOptions: opts, + Reporter: reporter, Operations: operations, OperationsScans: []scan.OperationScan{}, } - s, err := scan.NewScan(operations, opts) + s, err := scan.NewScan(operations, reporter, opts) require.NoError(t, err) assert.Equal(t, expected.Operations, s.Operations) @@ -63,7 +62,7 @@ func TestNewScanWithOptions(t *testing.T) { func TestScanGetOperationsScansWhenEmpty(t *testing.T) { op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) operations := operation.Operations{op} - s, _ := scan.NewScan(operations, nil) + s, _ := scan.NewScan(operations, nil, nil) operationsScans := s.GetOperationsScans() @@ -73,10 +72,10 @@ func TestScanGetOperationsScansWhenEmpty(t *testing.T) { func TestScanGetOperationsScans(t *testing.T) { op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) operations := operation.Operations{op} - s, _ := scan.NewScan(operations, nil) + s, _ := scan.NewScan(operations, nil, nil) s.AddOperationScanHandler(scan.NewOperationScanHandler("test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { return nil, nil - })) + }, []report.Issue{})) operationsScans := s.GetOperationsScans() @@ -86,7 +85,7 @@ func TestScanGetOperationsScans(t *testing.T) { func TestScanExecuteWithNoHandlers(t *testing.T) { op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) operations := operation.Operations{op} - s, _ := scan.NewScan(operations, nil) + s, _ := scan.NewScan(operations, nil, nil) reporter, errors, err := s.Execute(context.TODO(), nil) @@ -98,10 +97,10 @@ func TestScanExecuteWithNoHandlers(t *testing.T) { func TestScanExecuteWithHandler(t *testing.T) { op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) operations := operation.Operations{op} - s, _ := scan.NewScan(operations, nil) + s, _ := scan.NewScan(operations, nil, nil) handler := scan.NewOperationScanHandler("test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { return &report.ScanReport{ID: "test-report"}, nil - }) + }, []report.Issue{}) s.AddOperationScanHandler(handler) reporter, errors, err := s.Execute(context.TODO(), nil) @@ -112,132 +111,245 @@ func TestScanExecuteWithHandler(t *testing.T) { assert.Equal(t, "test-report", reporter.GetScanReports()[0].ID) } -func TestScanExecuteWithIncludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - IncludeScans: []string{"test-handler"}, - }) - handler := scan.NewOperationScanHandler("test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) - - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 1, len(reporter.GetScanReports())) - assert.Equal(t, "test-report", reporter.GetScanReports()[0].ID) -} - -func TestScanExecuteWithEmptyStringIncludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - IncludeScans: []string{""}, - }) - handler := scan.NewOperationScanHandler("test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) - - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 1, len(reporter.GetScanReports())) - assert.Equal(t, "test-report", reporter.GetScanReports()[0].ID) -} - -func TestScanExecuteWithMatchStringIncludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - IncludeScans: []string{"category.*"}, - }) - handler := scan.NewOperationScanHandler("category.test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) - - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 1, len(reporter.GetScanReports())) - assert.Equal(t, "test-report", reporter.GetScanReports()[0].ID) -} - -func TestScanExecuteWithWrongMatchStringIncludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - IncludeScans: []string{"wrong-category.*"}, - }) - handler := scan.NewOperationScanHandler("category.test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) - - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 0, len(reporter.GetScanReports())) -} - -func TestScanExecuteWithExcludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - ExcludeScans: []string{"test-handler"}, - }) - handler := scan.NewOperationScanHandler("test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) - - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 0, len(reporter.GetScanReports())) -} - -func TestScanExecuteWithMatchStringExcludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - ExcludeScans: []string{"category.*"}, - }) - handler := scan.NewOperationScanHandler("category.test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) - - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 0, len(reporter.GetScanReports())) -} - -func TestScanExecuteWithWrongMatchStringExcludeScans(t *testing.T) { - op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) - operations := operation.Operations{op} - s, _ := scan.NewScan(operations, &scan.ScanOptions{ - ExcludeScans: []string{"wrong-category.*"}, - }) - handler := scan.NewOperationScanHandler("category.test-handler", func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { - return &report.ScanReport{ID: "test-report"}, nil - }) - s.AddOperationScanHandler(handler) - - reporter, errors, err := s.Execute(context.TODO(), nil) +func TestScanExecuteWithMultipleHandlersAndOptions(t *testing.T) { + tests := []struct { + name string + scanId string + potentialIssues []report.Issue + opts *scan.ScanOptions + expected struct { + scanReportsLength int + } + }{ + { + name: "WithIncludeScansOptions", + scanId: "test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + IncludeScans: []string{"test-handler"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithEmptyStringIncludeScansOptions", + scanId: "test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + IncludeScans: []string{"test-handler"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithMatch string IncludeScans options", + scanId: "category.test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + IncludeScans: []string{"category.*"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithWrongMatchStringIncludeScansOptions", + scanId: "category.test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + IncludeScans: []string{"wrong-category.*"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 0, + }, + }, + { + name: "WithExcludeScansOptions", + scanId: "test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + ExcludeScans: []string{"test-handler"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 0, + }, + }, + { + name: "WithMatchStringExcludeScansOptions", + scanId: "category.test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + ExcludeScans: []string{"category.*"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 0, + }, + }, + { + name: "WithWrongMatchStringExcludeScansOptions", + scanId: "category.test-handler", + potentialIssues: []report.Issue{}, + opts: &scan.ScanOptions{ + ExcludeScans: []string{"wrong-category.*"}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithMinIssueSeverityOptionsAndPotentialIssuesBelowMinSeverity", + scanId: "test-handler", + potentialIssues: []report.Issue{ + { + CVSS: report.CVSS{ + Score: 6.0, + }, + }, + }, + opts: &scan.ScanOptions{ + MinIssueSeverity: 7.0, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 0, + }, + }, + { + name: "WithMinIssueSeverityOptionsAndPotentialIssuesAboveMinSeverity", + scanId: "test-handler", + potentialIssues: []report.Issue{ + { + CVSS: report.CVSS{ + Score: 9.0, + }, + }, + }, + opts: &scan.ScanOptions{ + MinIssueSeverity: 7.0, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithIncludeCWEsOptions", + scanId: "test-handler", + potentialIssues: []report.Issue{ + { + Classifications: &report.Classifications{ + CWE: report.CWE_1275_Sensitive_Cookie_With_Improper_SameSite, + }, + }, + }, + opts: &scan.ScanOptions{ + IncludeCWEs: []string{string(report.CWE_1275_Sensitive_Cookie_With_Improper_SameSite)}, + ExcludeCWEs: []string{}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithExcludeCWEsOptions", + scanId: "test-handler", + potentialIssues: []report.Issue{ + { + Classifications: &report.Classifications{ + CWE: report.CWE_1275_Sensitive_Cookie_With_Improper_SameSite, + }, + }, + }, + opts: &scan.ScanOptions{ + IncludeCWEs: []string{}, + ExcludeCWEs: []string{string(report.CWE_1275_Sensitive_Cookie_With_Improper_SameSite)}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 0, + }, + }, + { + name: "WithIncludeOWASPsOptions", + scanId: "test-handler", + potentialIssues: []report.Issue{ + { + Classifications: &report.Classifications{ + OWASP: report.OWASP_2023_BOLA, + }, + }, + }, + opts: &scan.ScanOptions{ + IncludeOWASPs: []string{string(report.OWASP_2023_BOLA)}, + ExcludeOWASPs: []string{}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + { + name: "WithIncludeCWEsOptionsAndNilIssueClassifications", + scanId: "test-handler", + potentialIssues: []report.Issue{ + { + CVSS: report.CVSS{ + Score: 9.0, + }, + }, + }, + opts: &scan.ScanOptions{ + IncludeCWEs: []string{string(report.CWE_16_Configuration)}, + }, + expected: struct { + scanReportsLength int + }{ + scanReportsLength: 1, + }, + }, + } - require.NoError(t, err) - assert.Empty(t, errors) - assert.Equal(t, 1, len(reporter.GetScanReports())) - assert.Equal(t, "test-report", reporter.GetScanReports()[0].ID) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, nil) + operations := operation.Operations{op} + s, _ := scan.NewScan(operations, nil, tt.opts) + scanReportID := "test-report" + handler := scan.NewOperationScanHandler(tt.scanId, func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { + return &report.ScanReport{ID: scanReportID}, nil + }, tt.potentialIssues) + s.AddOperationScanHandler(handler) + + reporter, errors, err := s.Execute(context.TODO(), nil) + + require.NoError(t, err) + assert.Empty(t, errors) + assert.Equal(t, tt.expected.scanReportsLength, len(reporter.GetScanReports())) + if tt.expected.scanReportsLength > 0 && len(reporter.GetScanReports()) > 0 { + assert.Equal(t, scanReportID, reporter.GetScanReports()[0].ID) + } + }) + } } diff --git a/scenario/discover_api.go b/scenario/discover_api.go index 7cb05c9..0cc8bfb 100644 --- a/scenario/discover_api.go +++ b/scenario/discover_api.go @@ -3,6 +3,7 @@ package scenario import ( "github.com/cerberauth/vulnapi/internal/operation" "github.com/cerberauth/vulnapi/internal/request" + "github.com/cerberauth/vulnapi/report" "github.com/cerberauth/vulnapi/scan" discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" @@ -28,17 +29,17 @@ func NewDiscoverAPIScan(method string, url string, client *request.Client, opts } operations := operation.Operations{op} - urlScan, err := scan.NewScan(operations, opts) + urlScan, err := scan.NewScan(operations, nil, opts) if err != nil { return nil, err } - urlScan.AddScanHandler(scan.NewOperationScanHandler(fingerprint.DiscoverFingerPrintScanID, fingerprint.ScanHandler)) - urlScan.AddScanHandler(scan.NewOperationScanHandler(discoverableopenapi.DiscoverableOpenAPIScanID, discoverableopenapi.ScanHandler)) - urlScan.AddScanHandler(scan.NewOperationScanHandler(discoverablegraphql.DiscoverableGraphQLPathScanID, discoverablegraphql.ScanHandler)) - urlScan.AddScanHandler(scan.NewOperationScanHandler(exposedfiles.DiscoverableFilesScanID, exposedfiles.ScanHandler)) - urlScan.AddScanHandler(scan.NewOperationScanHandler(wellknown.DiscoverableWellKnownScanID, wellknown.ScanHandler)) - urlScan.AddScanHandler(scan.NewOperationScanHandler(healthcheck.DiscoverableHealthCheckScanID, healthcheck.ScanHandler)) + urlScan.AddScanHandler(scan.NewOperationScanHandler(fingerprint.DiscoverFingerPrintScanID, fingerprint.ScanHandler, []report.Issue{})) + urlScan.AddScanHandler(scan.NewOperationScanHandler(discoverableopenapi.DiscoverableOpenAPIScanID, discoverableopenapi.ScanHandler, []report.Issue{})) + urlScan.AddScanHandler(scan.NewOperationScanHandler(discoverablegraphql.DiscoverableGraphQLPathScanID, discoverablegraphql.ScanHandler, []report.Issue{})) + urlScan.AddScanHandler(scan.NewOperationScanHandler(exposedfiles.DiscoverableFilesScanID, exposedfiles.ScanHandler, []report.Issue{})) + urlScan.AddScanHandler(scan.NewOperationScanHandler(wellknown.DiscoverableWellKnownScanID, wellknown.ScanHandler, []report.Issue{})) + urlScan.AddScanHandler(scan.NewOperationScanHandler(healthcheck.DiscoverableHealthCheckScanID, healthcheck.ScanHandler, []report.Issue{})) return urlScan, nil } diff --git a/scenario/discover_domain.go b/scenario/discover_domain.go index 4f5616a..aef888e 100644 --- a/scenario/discover_domain.go +++ b/scenario/discover_domain.go @@ -8,6 +8,7 @@ import ( "github.com/cerberauth/vulnapi/internal/operation" "github.com/cerberauth/vulnapi/internal/request" + "github.com/cerberauth/vulnapi/report" "github.com/cerberauth/vulnapi/scan" discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" @@ -130,14 +131,14 @@ func NewDiscoverDomainsScan(rootDomain string, client *request.Client, opts *sca domainsScan := []*scan.Scan{} for _, domain := range domains { if op, err := testFqdnReachable(domain, client); op != nil && err == nil { - domainScan, err := scan.NewScan(operation.Operations{op}, opts) + domainScan, err := scan.NewScan(operation.Operations{op}, nil, opts) if err != nil { return nil, err } - domainScan.AddScanHandler(scan.NewOperationScanHandler(fingerprint.DiscoverFingerPrintScanID, fingerprint.ScanHandler)) - domainScan.AddScanHandler(scan.NewOperationScanHandler(discoverableopenapi.DiscoverableOpenAPIScanID, discoverableopenapi.ScanHandler)) - domainScan.AddScanHandler(scan.NewOperationScanHandler(discoverablegraphql.DiscoverableGraphQLPathScanID, discoverablegraphql.ScanHandler)) + domainScan.AddScanHandler(scan.NewOperationScanHandler(fingerprint.DiscoverFingerPrintScanID, fingerprint.ScanHandler, []report.Issue{})) + domainScan.AddScanHandler(scan.NewOperationScanHandler(discoverableopenapi.DiscoverableOpenAPIScanID, discoverableopenapi.ScanHandler, []report.Issue{})) + domainScan.AddScanHandler(scan.NewOperationScanHandler(discoverablegraphql.DiscoverableGraphQLPathScanID, discoverablegraphql.ScanHandler, []report.Issue{})) domainsScan = append(domainsScan, domainScan) } } diff --git a/scenario/graphql.go b/scenario/graphql.go index 4512e2a..13dd832 100644 --- a/scenario/graphql.go +++ b/scenario/graphql.go @@ -11,7 +11,7 @@ import ( introspectionenabled "github.com/cerberauth/vulnapi/scan/graphql/introspection_enabled" ) -func NewGraphQLScan(url string, client *request.Client, opts *scan.ScanOptions) (*scan.Scan, error) { +func NewGraphQLScan(url string, client *request.Client, reporter *report.Reporter, opts *scan.ScanOptions) (*scan.Scan, error) { if client == nil { client = request.GetDefaultClient() } @@ -44,18 +44,18 @@ func NewGraphQLScan(url string, client *request.Client, opts *scan.ScanOptions) opts = &scan.ScanOptions{} } - if opts.Reporter == nil { - opts.Reporter = report.NewReporterWithGraphQL(url, securitySchemes) + if reporter == nil { + reporter = report.NewReporterWithGraphQL(url, securitySchemes) } operations := operation.Operations{op} - graphqlScan, err := scan.NewScan(operations, opts) + graphqlScan, err := scan.NewScan(operations, reporter, opts) if err != nil { return nil, err } WithAllCommonScans(graphqlScan) - graphqlScan.AddScanHandler(scan.NewOperationScanHandler(introspectionenabled.GraphqlIntrospectionScanID, introspectionenabled.ScanHandler)) + graphqlScan.AddScanHandler(scan.NewOperationScanHandler(introspectionenabled.GraphqlIntrospectionScanID, introspectionenabled.ScanHandler, []report.Issue{})) return graphqlScan, nil } diff --git a/scenario/graphql_test.go b/scenario/graphql_test.go index 352c9e8..2c6e1b6 100644 --- a/scenario/graphql_test.go +++ b/scenario/graphql_test.go @@ -19,7 +19,7 @@ func TestNewGraphQLScan(t *testing.T) { })) defer server.Close() - s, err := scenario.NewGraphQLScan(server.URL, nil, nil) + s, err := scenario.NewGraphQLScan(server.URL, nil, nil, nil) require.NoError(t, err) assert.Equal(t, server.URL, s.Operations[0].URL.String()) @@ -34,7 +34,7 @@ func TestNewGraphQLScanWithoutURLProto(t *testing.T) { defer server.Close() url := strings.TrimPrefix(server.URL, "http://") - s, err := scenario.NewGraphQLScan(url, nil, nil) + s, err := scenario.NewGraphQLScan(url, nil, nil, nil) require.NoError(t, err) assert.Equal(t, "https://"+url, s.Operations[0].URL.String()) @@ -43,7 +43,7 @@ func TestNewGraphQLScanWithoutURLProto(t *testing.T) { } func TestNewGraphQLScanWhenNotReachable(t *testing.T) { - _, err := scenario.NewGraphQLScan("http://localhost:8009", nil, nil) + _, err := scenario.NewGraphQLScan("http://localhost:8009", nil, nil, nil) require.Error(t, err) assert.Contains(t, err.Error(), ":8009: connect: connection refused") @@ -62,7 +62,7 @@ func TestNewGraphQLScanWithUpperCaseAuthorizationHeader(t *testing.T) { Header: header, }) - s, err := scenario.NewGraphQLScan(server.URL, client, nil) + s, err := scenario.NewGraphQLScan(server.URL, client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes) @@ -83,7 +83,7 @@ func TestNewGraphQLScanWithUpperCaseAuthorizationAndLowerCaseBearerHeader(t *tes Header: header, }) - s, err := scenario.NewGraphQLScan(server.URL, client, nil) + s, err := scenario.NewGraphQLScan(server.URL, client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes) @@ -102,7 +102,7 @@ func TestNewGraphQLScanWithLowerCaseAuthorizationHeader(t *testing.T) { Header: header, }) - s, err := scenario.NewGraphQLScan(server.URL, client, nil) + s, err := scenario.NewGraphQLScan(server.URL, client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes) diff --git a/scenario/openapi.go b/scenario/openapi.go index ac74873..2ef1b56 100644 --- a/scenario/openapi.go +++ b/scenario/openapi.go @@ -7,7 +7,7 @@ import ( "github.com/cerberauth/vulnapi/scan" ) -func NewOpenAPIScan(openapi *openapi.OpenAPI, securitySchemesValues *openapi.SecuritySchemeValues, client *request.Client, opts *scan.ScanOptions) (*scan.Scan, error) { +func NewOpenAPIScan(openapi *openapi.OpenAPI, securitySchemesValues *openapi.SecuritySchemeValues, client *request.Client, reporter *report.Reporter, opts *scan.ScanOptions) (*scan.Scan, error) { if client == nil { client = request.GetDefaultClient() } @@ -36,11 +36,11 @@ func NewOpenAPIScan(openapi *openapi.OpenAPI, securitySchemesValues *openapi.Sec opts = &scan.ScanOptions{} } - if opts.Reporter == nil { - opts.Reporter = report.NewReporterWithOpenAPIDoc(openapi.Doc, operations) + if reporter == nil { + reporter = report.NewReporterWithOpenAPIDoc(openapi.Doc, operations) } - openapiScan, err := scan.NewScan(operations, opts) + openapiScan, err := scan.NewScan(operations, reporter, opts) if err != nil { return nil, err } diff --git a/scenario/openapi_test.go b/scenario/openapi_test.go index df5364d..20c0e15 100644 --- a/scenario/openapi_test.go +++ b/scenario/openapi_test.go @@ -49,7 +49,7 @@ func TestNewOpenAPIScanWithHttpBearer(t *testing.T) { "bearer_auth": &token, }) - s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil) + s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil, nil) require.NoError(t, err) assert.Equal(t, 1, len(s.Operations)) @@ -66,7 +66,7 @@ func TestNewOpenAPIScanWithJWTHttpBearer(t *testing.T) { "bearer_auth": &token, }) - s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil) + s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil, nil) require.NoError(t, err) assert.Equal(t, 1, len(s.Operations)) @@ -85,7 +85,7 @@ func TestNewOpenAPIScanWithMultipleOperations(t *testing.T) { "bearer_auth": &token, }) - s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil) + s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil, nil) require.NoError(t, err) assert.Equal(t, 2, len(s.Operations)) @@ -104,7 +104,7 @@ func TestNewOpenAPIScanWithoutParamsExample(t *testing.T) { "bearer_auth": &token, }) - s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil) + s, err := scenario.NewOpenAPIScan(doc, securitySchemeValues, nil, nil, nil) require.NoError(t, err) assert.Equal(t, 2, len(s.Operations)) diff --git a/scenario/scans.go b/scenario/scans.go index 42117ea..eeb4d82 100644 --- a/scenario/scans.go +++ b/scenario/scans.go @@ -1,6 +1,7 @@ package scenario import ( + "github.com/cerberauth/vulnapi/report" "github.com/cerberauth/vulnapi/scan" authenticationbypass "github.com/cerberauth/vulnapi/scan/broken_authentication/authentication_bypass" algnone "github.com/cerberauth/vulnapi/scan/broken_authentication/jwt/alg_none" @@ -18,21 +19,21 @@ import ( ) func WithAllCommonScans(s *scan.Scan) *scan.Scan { - s.AddScanHandler(scan.NewOperationScanHandler(fingerprint.DiscoverFingerPrintScanID, fingerprint.ScanHandler)) + s.AddScanHandler(scan.NewOperationScanHandler(fingerprint.DiscoverFingerPrintScanID, fingerprint.ScanHandler, []report.Issue{})) - s.AddOperationScanHandler(scan.NewOperationScanHandler(acceptunauthenticated.NoAuthOperationScanID, acceptunauthenticated.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(authenticationbypass.AcceptsUnauthenticatedOperationScanID, authenticationbypass.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(algnone.AlgNoneJwtScanID, algnone.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(blanksecret.BlankSecretVulnerabilityScanID, blanksecret.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(notverified.NotVerifiedJwtScanID, notverified.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(nullsignature.NullSignatureScanID, nullsignature.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(weaksecret.WeakSecretVulnerabilityScanID, weaksecret.ScanHandler)) + s.AddOperationScanHandler(scan.NewOperationScanHandler(acceptunauthenticated.NoAuthOperationScanID, acceptunauthenticated.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(authenticationbypass.AcceptsUnauthenticatedOperationScanID, authenticationbypass.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(algnone.AlgNoneJwtScanID, algnone.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(blanksecret.BlankSecretVulnerabilityScanID, blanksecret.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(notverified.NotVerifiedJwtScanID, notverified.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(nullsignature.NullSignatureScanID, nullsignature.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(weaksecret.WeakSecretVulnerabilityScanID, weaksecret.ScanHandler, []report.Issue{})) - s.AddOperationScanHandler(scan.NewOperationScanHandler(httpcookies.HTTPCookiesScanID, httpcookies.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(httpheaders.HTTPHeadersScanID, httpheaders.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(httpmethodoverride.HTTPMethodOverrideScanID, httpmethodoverride.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(httptrace.HTTPTraceScanID, httptrace.ScanHandler)) - s.AddOperationScanHandler(scan.NewOperationScanHandler(httptrack.HTTPTrackScanID, httptrack.ScanHandler)) + s.AddOperationScanHandler(scan.NewOperationScanHandler(httpcookies.HTTPCookiesScanID, httpcookies.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(httpheaders.HTTPHeadersScanID, httpheaders.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(httpmethodoverride.HTTPMethodOverrideScanID, httpmethodoverride.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(httptrace.HTTPTraceScanID, httptrace.ScanHandler, []report.Issue{})) + s.AddOperationScanHandler(scan.NewOperationScanHandler(httptrack.HTTPTrackScanID, httptrack.ScanHandler, []report.Issue{})) return s } diff --git a/scenario/url.go b/scenario/url.go index ee06825..c7f878e 100644 --- a/scenario/url.go +++ b/scenario/url.go @@ -10,7 +10,7 @@ import ( "github.com/cerberauth/vulnapi/scan" ) -func NewURLScan(method string, url string, data string, client *request.Client, opts *scan.ScanOptions) (*scan.Scan, error) { +func NewURLScan(method string, url string, data string, client *request.Client, reporter *report.Reporter, opts *scan.ScanOptions) (*scan.Scan, error) { if client == nil { client = request.GetDefaultClient() } @@ -45,16 +45,16 @@ func NewURLScan(method string, url string, data string, client *request.Client, opts = &scan.ScanOptions{} } - if opts.Reporter == nil { + if reporter == nil { var reportData interface{} = nil if data != "" { reportData = data } - opts.Reporter = report.NewReporterWithCurl(method, url, reportData, client.Header, client.Cookies, securitySchemes) + reporter = report.NewReporterWithCurl(method, url, reportData, client.Header, client.Cookies, securitySchemes) } operations := operation.Operations{op} - urlScan, err := scan.NewScan(operations, opts) + urlScan, err := scan.NewScan(operations, reporter, opts) if err != nil { return nil, err } diff --git a/scenario/url_test.go b/scenario/url_test.go index 0220f9a..c97ae13 100644 --- a/scenario/url_test.go +++ b/scenario/url_test.go @@ -18,7 +18,7 @@ func TestNewURLScan(t *testing.T) { })) defer server.Close() - s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", nil, nil) + s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", nil, nil, nil) require.NoError(t, err) assert.Equal(t, server.URL, s.Operations[0].URL.String()) @@ -39,7 +39,7 @@ func TestNewURLScanWithUpperCaseAuthorizationHeader(t *testing.T) { Header: header, }) - s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil) + s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes) @@ -60,7 +60,7 @@ func TestNewURLScanWithUpperCaseAuthorizationAndLowerCaseBearerHeader(t *testing Header: header, }) - s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil) + s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes) @@ -79,7 +79,7 @@ func TestNewURLScanWithLowerCaseAuthorizationHeader(t *testing.T) { Header: header, }) - s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil) + s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes) @@ -120,7 +120,7 @@ func TestNewURLScanWithAPIKeyInHeader(t *testing.T) { Header: header, }) - s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil) + s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAPIKeySecurityScheme(tt.name, auth.InHeader, &apiKey)}, s.Operations[0].SecuritySchemes) @@ -143,7 +143,7 @@ func TestNewURLScanWithHTTPBasic(t *testing.T) { Header: header, }) - s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil) + s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil, nil) require.NoError(t, err) assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBasicSecurityScheme("default", credentials)}, s.Operations[0].SecuritySchemes)