diff --git a/README.md b/README.md index 4929cb7c..e60b5e20 100644 --- a/README.md +++ b/README.md @@ -246,11 +246,12 @@ OPTIMIZATIONS: -no-stdin disable stdin processing HEADLESS: - -headless enable templates that require headless browser support (root user on Linux will disable sandbox) - -page-timeout int seconds to wait for each page in headless mode (default 20) - -sb, -show-browser show the browser on the screen when running templates with headless mode - -sc, -system-chrome use local installed Chrome browser instead of nuclei installed - -lha, -list-headless-action list available headless actions + -headless enable templates that require headless browser support (root user on Linux will disable sandbox) + -page-timeout int seconds to wait for each page in headless mode (default 20) + -sb, -show-browser show the browser on the screen when running templates with headless mode + -ho, -headless-options string[] start headless chrome with additional options + -sc, -system-chrome use local installed Chrome browser instead of nuclei installed + -lha, -list-headless-action list available headless actions DEBUG: -debug show all requests and responses diff --git a/v2/cmd/integration-test/code.go b/v2/cmd/integration-test/code.go index 6835f86b..b2b5f94c 100644 --- a/v2/cmd/integration-test/code.go +++ b/v2/cmd/integration-test/code.go @@ -34,9 +34,9 @@ import ( "github.com/projectdiscovery/ratelimit" ) -var codeTestcases = map[string]testutils.TestCase{ - "code/test.yaml": &goIntegrationTest{}, - "code/test.json": &goIntegrationTest{}, +var codeTestcases = []TestCaseInfo{ + {Path: "code/test.yaml", TestCase: &goIntegrationTest{}}, + {Path: "code/test.json", TestCase: &goIntegrationTest{}}, } type goIntegrationTest struct{} diff --git a/v2/cmd/integration-test/custom-dir.go b/v2/cmd/integration-test/custom-dir.go index 83f2b1e9..774a367d 100644 --- a/v2/cmd/integration-test/custom-dir.go +++ b/v2/cmd/integration-test/custom-dir.go @@ -8,8 +8,8 @@ import ( type customConfigDirTest struct{} -var customConfigDirTestCases = map[string]testutils.TestCase{ - "dns/cname-fingerprint.yaml": &customConfigDirTest{}, +var customConfigDirTestCases = []TestCaseInfo{ + {Path: "dns/cname-fingerprint.yaml", TestCase: &customConfigDirTest{}}, } // Execute executes a test case and returns an error if occurred diff --git a/v2/cmd/integration-test/dns.go b/v2/cmd/integration-test/dns.go index 08fac28c..570f734a 100644 --- a/v2/cmd/integration-test/dns.go +++ b/v2/cmd/integration-test/dns.go @@ -4,14 +4,14 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var dnsTestCases = map[string]testutils.TestCase{ - "dns/basic.yaml": &dnsBasic{}, - "dns/ptr.yaml": &dnsPtr{}, - "dns/caa.yaml": &dnsCAA{}, - "dns/tlsa.yaml": &dnsTLSA{}, - "dns/variables.yaml": &dnsVariables{}, - "dns/payload.yaml": &dnsPayload{}, - "dns/dsl-matcher-variable.yaml": &dnsDSLMatcherVariable{}, +var dnsTestCases = []TestCaseInfo{ + {Path: "dns/basic.yaml", TestCase: &dnsBasic{}}, + {Path: "dns/ptr.yaml", TestCase: &dnsPtr{}}, + {Path: "dns/caa.yaml", TestCase: &dnsCAA{}}, + {Path: "dns/tlsa.yaml", TestCase: &dnsTLSA{}}, + {Path: "dns/variables.yaml", TestCase: &dnsVariables{}}, + {Path: "dns/payload.yaml", TestCase: &dnsPayload{}}, + {Path: "dns/dsl-matcher-variable.yaml", TestCase: &dnsDSLMatcherVariable{}}, } type dnsBasic struct{} diff --git a/v2/cmd/integration-test/file.go b/v2/cmd/integration-test/file.go index 079b3a37..c5351f30 100644 --- a/v2/cmd/integration-test/file.go +++ b/v2/cmd/integration-test/file.go @@ -4,11 +4,11 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var fileTestcases = map[string]testutils.TestCase{ - "file/matcher-with-or.yaml": &fileWithOrMatcher{}, - "file/matcher-with-and.yaml": &fileWithAndMatcher{}, - "file/matcher-with-nested-and.yaml": &fileWithAndMatcher{}, - "file/extract.yaml": &fileWithExtractor{}, +var fileTestcases = []TestCaseInfo{ + {Path: "file/matcher-with-or.yaml", TestCase: &fileWithOrMatcher{}}, + {Path: "file/matcher-with-and.yaml", TestCase: &fileWithAndMatcher{}}, + {Path: "file/matcher-with-nested-and.yaml", TestCase: &fileWithAndMatcher{}}, + {Path: "file/extract.yaml", TestCase: &fileWithExtractor{}}, } type fileWithOrMatcher struct{} diff --git a/v2/cmd/integration-test/fuzz.go b/v2/cmd/integration-test/fuzz.go index 2a9573d2..5dd048d3 100644 --- a/v2/cmd/integration-test/fuzz.go +++ b/v2/cmd/integration-test/fuzz.go @@ -12,11 +12,11 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var fuzzingTestCases = map[string]testutils.TestCase{ - "fuzz/fuzz-mode.yaml": &fuzzModeOverride{}, - "fuzz/fuzz-type.yaml": &fuzzTypeOverride{}, - "fuzz/fuzz-query.yaml": &httpFuzzQuery{}, - "fuzz/fuzz-headless.yaml": &HeadlessFuzzingQuery{}, +var fuzzingTestCases = []TestCaseInfo{ + {Path: "fuzz/fuzz-mode.yaml", TestCase: &fuzzModeOverride{}}, + {Path: "fuzz/fuzz-type.yaml", TestCase: &fuzzTypeOverride{}}, + {Path: "fuzz/fuzz-query.yaml", TestCase: &httpFuzzQuery{}}, + {Path: "fuzz/fuzz-headless.yaml", TestCase: &HeadlessFuzzingQuery{}}, } type httpFuzzQuery struct{} diff --git a/v2/cmd/integration-test/generic.go b/v2/cmd/integration-test/generic.go index b22ffd16..8b66257a 100644 --- a/v2/cmd/integration-test/generic.go +++ b/v2/cmd/integration-test/generic.go @@ -13,8 +13,8 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var genericTestcases = map[string]testutils.TestCase{ - "generic/auth/certificate/http-get.yaml": &clientCertificate{}, +var genericTestcases = []TestCaseInfo{ + {Path: "generic/auth/certificate/http-get.yaml", TestCase: &clientCertificate{}}, } var ( diff --git a/v2/cmd/integration-test/headless.go b/v2/cmd/integration-test/headless.go index e4c70543..185cdc87 100644 --- a/v2/cmd/integration-test/headless.go +++ b/v2/cmd/integration-test/headless.go @@ -10,14 +10,14 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var headlessTestcases = map[string]testutils.TestCase{ - "headless/headless-basic.yaml": &headlessBasic{}, - "headless/headless-header-action.yaml": &headlessHeaderActions{}, - "headless/headless-extract-values.yaml": &headlessExtractValues{}, - "headless/headless-payloads.yaml": &headlessPayloads{}, - "headless/variables.yaml": &headlessVariables{}, - "headless/file-upload.yaml": &headlessFileUpload{}, - "headless/headless-header-status-test.yaml": &headlessHeaderStatus{}, +var headlessTestcases = []TestCaseInfo{ + {Path: "headless/headless-basic.yaml", TestCase: &headlessBasic{}}, + {Path: "headless/headless-header-action.yaml", TestCase: &headlessHeaderActions{}}, + {Path: "headless/headless-extract-values.yaml", TestCase: &headlessExtractValues{}}, + {Path: "headless/headless-payloads.yaml", TestCase: &headlessPayloads{}}, + {Path: "headless/variables.yaml", TestCase: &headlessVariables{}}, + {Path: "headless/file-upload.yaml", TestCase: &headlessFileUpload{}}, + {Path: "headless/headless-header-status-test.yaml", TestCase: &headlessHeaderStatus{}}, } type headlessBasic struct{} diff --git a/v2/cmd/integration-test/http.go b/v2/cmd/integration-test/http.go index a1b67c57..75e80e18 100644 --- a/v2/cmd/integration-test/http.go +++ b/v2/cmd/integration-test/http.go @@ -26,60 +26,60 @@ import ( stringsutil "github.com/projectdiscovery/utils/strings" ) -var httpTestcases = map[string]testutils.TestCase{ +var httpTestcases = []TestCaseInfo{ // TODO: excluded due to parsing errors with console // "http/raw-unsafe-request.yaml": &httpRawUnsafeRequest{}, - "http/get-headers.yaml": &httpGetHeaders{}, - "http/get-query-string.yaml": &httpGetQueryString{}, - "http/get-redirects.yaml": &httpGetRedirects{}, - "http/get-host-redirects.yaml": &httpGetHostRedirects{}, - "http/disable-redirects.yaml": &httpDisableRedirects{}, - "http/get.yaml": &httpGet{}, - "http/post-body.yaml": &httpPostBody{}, - "http/post-json-body.yaml": &httpPostJSONBody{}, - "http/post-multipart-body.yaml": &httpPostMultipartBody{}, - "http/raw-cookie-reuse.yaml": &httpRawCookieReuse{}, - "http/raw-dynamic-extractor.yaml": &httpRawDynamicExtractor{}, - "http/raw-get-query.yaml": &httpRawGetQuery{}, - "http/raw-get.yaml": &httpRawGet{}, - "http/raw-with-params.yaml": &httpRawWithParams{}, - "http/raw-unsafe-with-params.yaml": &httpRawWithParams{}, // Not a typo, functionality is same as above - "http/raw-path-trailing-slash.yaml": &httpRawPathTrailingSlash{}, - "http/raw-payload.yaml": &httpRawPayload{}, - "http/raw-post-body.yaml": &httpRawPostBody{}, - "http/raw-unsafe-path.yaml": &httpRawUnsafePath{}, - "http/http-paths.yaml": &httpPaths{}, - "http/request-condition.yaml": &httpRequestCondition{}, - "http/request-condition-new.yaml": &httpRequestCondition{}, - "http/self-contained.yaml": &httpRequestSelfContained{}, - "http/self-contained-with-path.yaml": &httpRequestSelfContained{}, // Not a typo, functionality is same as above - "http/self-contained-with-params.yaml": &httpRequestSelfContainedWithParams{}, - "http/self-contained-file-input.yaml": &httpRequestSelfContainedFileInput{}, - "http/get-case-insensitive.yaml": &httpGetCaseInsensitive{}, - "http/get.yaml,http/get-case-insensitive.yaml": &httpGetCaseInsensitiveCluster{}, - "http/get-redirects-chain-headers.yaml": &httpGetRedirectsChainHeaders{}, - "http/dsl-matcher-variable.yaml": &httpDSLVariable{}, - "http/dsl-functions.yaml": &httpDSLFunctions{}, - "http/race-simple.yaml": &httpRaceSimple{}, - "http/race-multiple.yaml": &httpRaceMultiple{}, - "http/stop-at-first-match.yaml": &httpStopAtFirstMatch{}, - "http/stop-at-first-match-with-extractors.yaml": &httpStopAtFirstMatchWithExtractors{}, - "http/variables.yaml": &httpVariables{}, - "http/variable-dsl-function.yaml": &httpVariableDSLFunction{}, - "http/get-override-sni.yaml": &httpSniAnnotation{}, - "http/get-sni.yaml": &customCLISNI{}, - "http/redirect-match-url.yaml": &httpRedirectMatchURL{}, - "http/get-sni-unsafe.yaml": &customCLISNIUnsafe{}, - "http/annotation-timeout.yaml": &annotationTimeout{}, - "http/custom-attack-type.yaml": &customAttackType{}, - "http/get-all-ips.yaml": &scanAllIPS{}, - "http/get-without-scheme.yaml": &httpGetWithoutScheme{}, - "http/cl-body-without-header.yaml": &httpCLBodyWithoutHeader{}, - "http/cl-body-with-header.yaml": &httpCLBodyWithHeader{}, - "http/save-extractor-values-to-file.yaml": &httpSaveExtractorValuesToFile{}, - "http/cli-with-constants.yaml": &ConstantWithCliVar{}, - "http/matcher-status.yaml": &matcherStatusTest{}, - "http/disable-path-automerge.yaml": &httpDisablePathAutomerge{}, + {Path: "http/get-headers.yaml", TestCase: &httpGetHeaders{}}, + {Path: "http/get-query-string.yaml", TestCase: &httpGetQueryString{}}, + {Path: "http/get-redirects.yaml", TestCase: &httpGetRedirects{}}, + {Path: "http/get-host-redirects.yaml", TestCase: &httpGetHostRedirects{}}, + {Path: "http/disable-redirects.yaml", TestCase: &httpDisableRedirects{}}, + {Path: "http/get.yaml", TestCase: &httpGet{}}, + {Path: "http/post-body.yaml", TestCase: &httpPostBody{}}, + {Path: "http/post-json-body.yaml", TestCase: &httpPostJSONBody{}}, + {Path: "http/post-multipart-body.yaml", TestCase: &httpPostMultipartBody{}}, + {Path: "http/raw-cookie-reuse.yaml", TestCase: &httpRawCookieReuse{}}, + {Path: "http/raw-dynamic-extractor.yaml", TestCase: &httpRawDynamicExtractor{}}, + {Path: "http/raw-get-query.yaml", TestCase: &httpRawGetQuery{}}, + {Path: "http/raw-get.yaml", TestCase: &httpRawGet{}}, + {Path: "http/raw-with-params.yaml", TestCase: &httpRawWithParams{}}, + {Path: "http/raw-unsafe-with-params.yaml", TestCase: &httpRawWithParams{}}, // Not a typo, functionality is same as above + {Path: "http/raw-path-trailing-slash.yaml", TestCase: &httpRawPathTrailingSlash{}}, + {Path: "http/raw-payload.yaml", TestCase: &httpRawPayload{}}, + {Path: "http/raw-post-body.yaml", TestCase: &httpRawPostBody{}}, + {Path: "http/raw-unsafe-path.yaml", TestCase: &httpRawUnsafePath{}}, + {Path: "http/http-paths.yaml", TestCase: &httpPaths{}}, + {Path: "http/request-condition.yaml", TestCase: &httpRequestCondition{}}, + {Path: "http/request-condition-new.yaml", TestCase: &httpRequestCondition{}}, + {Path: "http/self-contained.yaml", TestCase: &httpRequestSelfContained{}}, + {Path: "http/self-contained-with-path.yaml", TestCase: &httpRequestSelfContained{}}, // Not a typo, functionality is same as above + {Path: "http/self-contained-with-params.yaml", TestCase: &httpRequestSelfContainedWithParams{}}, + {Path: "http/self-contained-file-input.yaml", TestCase: &httpRequestSelfContainedFileInput{}}, + {Path: "http/get-case-insensitive.yaml", TestCase: &httpGetCaseInsensitive{}}, + {Path: "http/get.yaml,http/get-case-insensitive.yaml", TestCase: &httpGetCaseInsensitiveCluster{}}, + {Path: "http/get-redirects-chain-headers.yaml", TestCase: &httpGetRedirectsChainHeaders{}}, + {Path: "http/dsl-matcher-variable.yaml", TestCase: &httpDSLVariable{}}, + {Path: "http/dsl-functions.yaml", TestCase: &httpDSLFunctions{}}, + {Path: "http/race-simple.yaml", TestCase: &httpRaceSimple{}}, + {Path: "http/race-multiple.yaml", TestCase: &httpRaceMultiple{}}, + {Path: "http/stop-at-first-match.yaml", TestCase: &httpStopAtFirstMatch{}}, + {Path: "http/stop-at-first-match-with-extractors.yaml", TestCase: &httpStopAtFirstMatchWithExtractors{}}, + {Path: "http/variables.yaml", TestCase: &httpVariables{}}, + {Path: "http/variable-dsl-function.yaml", TestCase: &httpVariableDSLFunction{}}, + {Path: "http/get-override-sni.yaml", TestCase: &httpSniAnnotation{}}, + {Path: "http/get-sni.yaml", TestCase: &customCLISNI{}}, + {Path: "http/redirect-match-url.yaml", TestCase: &httpRedirectMatchURL{}}, + {Path: "http/get-sni-unsafe.yaml", TestCase: &customCLISNIUnsafe{}}, + {Path: "http/annotation-timeout.yaml", TestCase: &annotationTimeout{}}, + {Path: "http/custom-attack-type.yaml", TestCase: &customAttackType{}}, + {Path: "http/get-all-ips.yaml", TestCase: &scanAllIPS{}}, + {Path: "http/get-without-scheme.yaml", TestCase: &httpGetWithoutScheme{}}, + {Path: "http/cl-body-without-header.yaml", TestCase: &httpCLBodyWithoutHeader{}}, + {Path: "http/cl-body-with-header.yaml", TestCase: &httpCLBodyWithHeader{}}, + {Path: "http/save-extractor-values-to-file.yaml", TestCase: &httpSaveExtractorValuesToFile{}}, + {Path: "http/cli-with-constants.yaml", TestCase: &ConstantWithCliVar{}}, + {Path: "http/matcher-status.yaml", TestCase: &matcherStatusTest{}}, + {Path: "http/disable-path-automerge.yaml", TestCase: &httpDisablePathAutomerge{}}, } type httpInteractshRequest struct{} diff --git a/v2/cmd/integration-test/integration-test.go b/v2/cmd/integration-test/integration-test.go index 381a6277..f878a76e 100644 --- a/v2/cmd/integration-test/integration-test.go +++ b/v2/cmd/integration-test/integration-test.go @@ -4,7 +4,7 @@ import ( "flag" "fmt" "os" - "sort" + "runtime" "strings" "github.com/logrusorgru/aurora" @@ -13,6 +13,12 @@ import ( sliceutil "github.com/projectdiscovery/utils/slice" ) +type TestCaseInfo struct { + Path string + TestCase testutils.TestCase + DisableOn func() bool +} + var ( debug = os.Getenv("DEBUG") == "true" githubAction = os.Getenv("GH_ACTION") == "true" @@ -21,7 +27,7 @@ var ( success = aurora.Green("[✓]").String() failed = aurora.Red("[✘]").String() - protocolTests = map[string]map[string]testutils.TestCase{ + protocolTests = map[string][]TestCaseInfo{ "http": httpTestcases, "interactsh": interactshTestCases, "network": networkTestcases, @@ -99,18 +105,18 @@ func executeWithRetry(testCase testutils.TestCase, templatePath string, retryCou } func debugTests() { - keys := getMapKeys(protocolTests[runProtocol]) - for _, tpath := range keys { - testcase := protocolTests[runProtocol][tpath] - if runTemplate != "" && !strings.Contains(tpath, runTemplate) { + testCaseInfos := protocolTests[runProtocol] + for _, testCaseInfo := range testCaseInfos { + if (runTemplate != "" && !strings.Contains(testCaseInfo.Path, runTemplate)) || + (testCaseInfo.DisableOn != nil && testCaseInfo.DisableOn()) { continue } if runProtocol == "interactsh" { - if _, err := executeWithRetry(testcase, tpath, interactshRetryCount); err != nil { + if _, err := executeWithRetry(testCaseInfo.TestCase, testCaseInfo.Path, interactshRetryCount); err != nil { fmt.Printf("\n%v", err.Error()) } } else { - if _, err := execute(testcase, tpath); err != nil { + if _, err := execute(testCaseInfo.TestCase, testCaseInfo.Path); err != nil { fmt.Printf("\n%v", err.Error()) } } @@ -120,21 +126,22 @@ func debugTests() { func runTests(customTemplatePaths []string) []string { var failedTestTemplatePaths []string - for proto, testCases := range protocolTests { + for proto, testCaseInfos := range protocolTests { if len(customTemplatePaths) == 0 { fmt.Printf("Running test cases for %q protocol\n", aurora.Blue(proto)) } - keys := getMapKeys(testCases) - - for _, templatePath := range keys { - testCase := testCases[templatePath] - if len(customTemplatePaths) == 0 || sliceutil.Contains(customTemplatePaths, templatePath) { + for _, testCaseInfo := range testCaseInfos { + if testCaseInfo.DisableOn != nil && testCaseInfo.DisableOn() { + fmt.Printf("skipping test case %v. disabled on %v.\n", aurora.Blue(testCaseInfo.Path), runtime.GOOS) + continue + } + if len(customTemplatePaths) == 0 || sliceutil.Contains(customTemplatePaths, testCaseInfo.Path) { var failedTemplatePath string var err error if proto == "interactsh" { - failedTemplatePath, err = executeWithRetry(testCase, templatePath, interactshRetryCount) + failedTemplatePath, err = executeWithRetry(testCaseInfo.TestCase, testCaseInfo.Path, interactshRetryCount) } else { - failedTemplatePath, err = execute(testCase, templatePath) + failedTemplatePath, err = execute(testCaseInfo.TestCase, testCaseInfo.Path) } if err != nil { failedTestTemplatePaths = append(failedTestTemplatePaths, failedTemplatePath) @@ -169,12 +176,3 @@ func normalizeSplit(str string) []string { return r == ',' }) } - -func getMapKeys[T any](testcases map[string]T) []string { - keys := make([]string, 0, len(testcases)) - for k := range testcases { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} diff --git a/v2/cmd/integration-test/interactsh.go b/v2/cmd/integration-test/interactsh.go index 035f844d..d163fef8 100644 --- a/v2/cmd/integration-test/interactsh.go +++ b/v2/cmd/integration-test/interactsh.go @@ -1,10 +1,10 @@ package main -import "github.com/projectdiscovery/nuclei/v2/pkg/testutils" +import osutils "github.com/projectdiscovery/utils/os" // All Interactsh related testcases -var interactshTestCases = map[string]testutils.TestCase{ - "http/interactsh.yaml": &httpInteractshRequest{}, - "http/interactsh-stop-at-first-match.yaml": &httpInteractshStopAtFirstMatchRequest{}, - "http/default-matcher-condition.yaml": &httpDefaultMatcherCondition{}, +var interactshTestCases = []TestCaseInfo{ + {Path: "http/interactsh.yaml", TestCase: &httpInteractshRequest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }}, + {Path: "http/interactsh-stop-at-first-match.yaml", TestCase: &httpInteractshStopAtFirstMatchRequest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }}, + {Path: "http/default-matcher-condition.yaml", TestCase: &httpDefaultMatcherCondition{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }}, } diff --git a/v2/cmd/integration-test/loader.go b/v2/cmd/integration-test/loader.go index 4f4c0605..6c0ccae6 100644 --- a/v2/cmd/integration-test/loader.go +++ b/v2/cmd/integration-test/loader.go @@ -12,13 +12,13 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var loaderTestcases = map[string]testutils.TestCase{ - "loader/template-list.yaml": &remoteTemplateList{}, - "loader/workflow-list.yaml": &remoteWorkflowList{}, - "loader/excluded-template.yaml": &excludedTemplate{}, - "loader/nonexistent-template-list.yaml": &nonExistentTemplateList{}, - "loader/nonexistent-workflow-list.yaml": &nonExistentWorkflowList{}, - "loader/template-list-not-allowed.yaml": &remoteTemplateListNotAllowed{}, +var loaderTestcases = []TestCaseInfo{ + {Path: "loader/template-list.yaml", TestCase: &remoteTemplateList{}}, + {Path: "loader/workflow-list.yaml", TestCase: &remoteWorkflowList{}}, + {Path: "loader/excluded-template.yaml", TestCase: &excludedTemplate{}}, + {Path: "loader/nonexistent-template-list.yaml", TestCase: &nonExistentTemplateList{}}, + {Path: "loader/nonexistent-workflow-list.yaml", TestCase: &nonExistentWorkflowList{}}, + {Path: "loader/template-list-not-allowed.yaml", TestCase: &remoteTemplateListNotAllowed{}}, } type remoteTemplateList struct{} diff --git a/v2/cmd/integration-test/network.go b/v2/cmd/integration-test/network.go index ff4d72a8..6335fd00 100644 --- a/v2/cmd/integration-test/network.go +++ b/v2/cmd/integration-test/network.go @@ -4,15 +4,16 @@ import ( "net" "github.com/projectdiscovery/nuclei/v2/pkg/testutils" + osutils "github.com/projectdiscovery/utils/os" ) -var networkTestcases = map[string]testutils.TestCase{ - "network/basic.yaml": &networkBasic{}, - "network/hex.yaml": &networkBasic{}, - "network/multi-step.yaml": &networkMultiStep{}, - "network/self-contained.yaml": &networkRequestSelContained{}, - "network/variables.yaml": &networkVariables{}, - "network/same-address.yaml": &networkBasic{}, +var networkTestcases = []TestCaseInfo{ + {Path: "network/basic.yaml", TestCase: &networkBasic{}, DisableOn: func() bool { return osutils.IsWindows() }}, + {Path: "network/hex.yaml", TestCase: &networkBasic{}, DisableOn: func() bool { return osutils.IsWindows() }}, + {Path: "network/multi-step.yaml", TestCase: &networkMultiStep{}}, + {Path: "network/self-contained.yaml", TestCase: &networkRequestSelContained{}}, + {Path: "network/variables.yaml", TestCase: &networkVariables{}}, + {Path: "network/same-address.yaml", TestCase: &networkBasic{}}, } const defaultStaticPort = 5431 diff --git a/v2/cmd/integration-test/offline-http.go b/v2/cmd/integration-test/offline-http.go index b563cbc0..8e442112 100644 --- a/v2/cmd/integration-test/offline-http.go +++ b/v2/cmd/integration-test/offline-http.go @@ -6,10 +6,10 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var offlineHttpTestcases = map[string]testutils.TestCase{ - "offlinehttp/rfc-req-resp.yaml": &RfcRequestResponse{}, - "offlinehttp/offline-allowed-paths.yaml": &RequestResponseWithAllowedPaths{}, - "offlinehttp/offline-raw.yaml": &RawRequestResponse{}, +var offlineHttpTestcases = []TestCaseInfo{ + {Path: "offlinehttp/rfc-req-resp.yaml", TestCase: &RfcRequestResponse{}}, + {Path: "offlinehttp/offline-allowed-paths.yaml", TestCase: &RequestResponseWithAllowedPaths{}}, + {Path: "offlinehttp/offline-raw.yaml", TestCase: &RawRequestResponse{}}, } type RfcRequestResponse struct{} diff --git a/v2/cmd/integration-test/ssl.go b/v2/cmd/integration-test/ssl.go index 7f337481..03b2e6bc 100644 --- a/v2/cmd/integration-test/ssl.go +++ b/v2/cmd/integration-test/ssl.go @@ -7,12 +7,12 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var sslTestcases = map[string]testutils.TestCase{ - "ssl/basic.yaml": &sslBasic{}, - "ssl/basic-ztls.yaml": &sslBasicZtls{}, - "ssl/custom-cipher.yaml": &sslCustomCipher{}, - "ssl/custom-version.yaml": &sslCustomVersion{}, - "ssl/ssl-with-vars.yaml": &sslWithVars{}, +var sslTestcases = []TestCaseInfo{ + {Path: "ssl/basic.yaml", TestCase: &sslBasic{}}, + {Path: "ssl/basic-ztls.yaml", TestCase: &sslBasicZtls{}}, + {Path: "ssl/custom-cipher.yaml", TestCase: &sslCustomCipher{}}, + {Path: "ssl/custom-version.yaml", TestCase: &sslCustomVersion{}}, + {Path: "ssl/ssl-with-vars.yaml", TestCase: &sslWithVars{}}, } type sslBasic struct{} diff --git a/v2/cmd/integration-test/template-dir.go b/v2/cmd/integration-test/template-dir.go index 8b318ad8..fe629fff 100644 --- a/v2/cmd/integration-test/template-dir.go +++ b/v2/cmd/integration-test/template-dir.go @@ -7,8 +7,8 @@ import ( errorutil "github.com/projectdiscovery/utils/errors" ) -var templatesDirTestCases = map[string]testutils.TestCase{ - "dns/cname-fingerprint.yaml": &templateDirWithTargetTest{}, +var templatesDirTestCases = []TestCaseInfo{ + {Path: "dns/cname-fingerprint.yaml", TestCase: &templateDirWithTargetTest{}}, } type templateDirWithTargetTest struct{} diff --git a/v2/cmd/integration-test/template-path.go b/v2/cmd/integration-test/template-path.go index 9633558d..97cab7aa 100644 --- a/v2/cmd/integration-test/template-path.go +++ b/v2/cmd/integration-test/template-path.go @@ -12,15 +12,15 @@ func getTemplatePath() string { return config.DefaultConfig.TemplatesDirectory } -var templatesPathTestCases = map[string]testutils.TestCase{ +var templatesPathTestCases = []TestCaseInfo{ //template folder path issue - "http/get.yaml": &folderPathTemplateTest{}, + {Path: "http/get.yaml", TestCase: &folderPathTemplateTest{}}, //cwd - "./dns/cname-fingerprint.yaml": &cwdTemplateTest{}, + {Path: "./dns/cname-fingerprint.yaml", TestCase: &cwdTemplateTest{}}, //relative path - "dns/cname-fingerprint.yaml": &relativePathTemplateTest{}, + {Path: "dns/cname-fingerprint.yaml", TestCase: &relativePathTemplateTest{}}, //absolute path - fmt.Sprintf("%v/dns/cname-fingerprint.yaml", getTemplatePath()): &absolutePathTemplateTest{}, + {Path: fmt.Sprintf("%v/dns/cname-fingerprint.yaml", getTemplatePath()), TestCase: &absolutePathTemplateTest{}}, } type cwdTemplateTest struct{} diff --git a/v2/cmd/integration-test/websocket.go b/v2/cmd/integration-test/websocket.go index 73de9e48..9d0873ac 100644 --- a/v2/cmd/integration-test/websocket.go +++ b/v2/cmd/integration-test/websocket.go @@ -9,11 +9,11 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var websocketTestCases = map[string]testutils.TestCase{ - "websocket/basic.yaml": &websocketBasic{}, - "websocket/cswsh.yaml": &websocketCswsh{}, - "websocket/no-cswsh.yaml": &websocketNoCswsh{}, - "websocket/path.yaml": &websocketWithPath{}, +var websocketTestCases = []TestCaseInfo{ + {Path: "websocket/basic.yaml", TestCase: &websocketBasic{}}, + {Path: "websocket/cswsh.yaml", TestCase: &websocketCswsh{}}, + {Path: "websocket/no-cswsh.yaml", TestCase: &websocketNoCswsh{}}, + {Path: "websocket/path.yaml", TestCase: &websocketWithPath{}}, } type websocketBasic struct{} diff --git a/v2/cmd/integration-test/whois.go b/v2/cmd/integration-test/whois.go index edb534e4..5caa846b 100644 --- a/v2/cmd/integration-test/whois.go +++ b/v2/cmd/integration-test/whois.go @@ -4,8 +4,8 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var whoisTestCases = map[string]testutils.TestCase{ - "whois/basic.yaml": &whoisBasic{}, +var whoisTestCases = []TestCaseInfo{ + {Path: "whois/basic.yaml", TestCase: &whoisBasic{}}, } type whoisBasic struct{} diff --git a/v2/cmd/integration-test/workflow.go b/v2/cmd/integration-test/workflow.go index fc22d8d8..4cfb95b1 100644 --- a/v2/cmd/integration-test/workflow.go +++ b/v2/cmd/integration-test/workflow.go @@ -11,14 +11,14 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/testutils" ) -var workflowTestcases = map[string]testutils.TestCase{ - "workflow/basic.yaml": &workflowBasic{}, - "workflow/condition-matched.yaml": &workflowConditionMatched{}, - "workflow/condition-unmatched.yaml": &workflowConditionUnmatch{}, - "workflow/matcher-name.yaml": &workflowMatcherName{}, - "workflow/http-value-share-workflow.yaml": &workflowHttpKeyValueShare{}, - "workflow/dns-value-share-workflow.yaml": &workflowDnsKeyValueShare{}, - "workflow/shared-cookie.yaml": &workflowSharedCookies{}, +var workflowTestcases = []TestCaseInfo{ + {Path: "workflow/basic.yaml", TestCase: &workflowBasic{}}, + {Path: "workflow/condition-matched.yaml", TestCase: &workflowConditionMatched{}}, + {Path: "workflow/condition-unmatched.yaml", TestCase: &workflowConditionUnmatch{}}, + {Path: "workflow/matcher-name.yaml", TestCase: &workflowMatcherName{}}, + {Path: "workflow/http-value-share-workflow.yaml", TestCase: &workflowHttpKeyValueShare{}}, + {Path: "workflow/dns-value-share-workflow.yaml", TestCase: &workflowDnsKeyValueShare{}}, + {Path: "workflow/shared-cookie.yaml", TestCase: &workflowSharedCookies{}}, } type workflowBasic struct{} diff --git a/v2/cmd/nuclei/main.go b/v2/cmd/nuclei/main.go index 67f15b2e..301326c8 100644 --- a/v2/cmd/nuclei/main.go +++ b/v2/cmd/nuclei/main.go @@ -272,6 +272,7 @@ on extensive configurability, massive extensibility and ease of use.`) flagSet.BoolVar(&options.Headless, "headless", false, "enable templates that require headless browser support (root user on Linux will disable sandbox)"), flagSet.IntVar(&options.PageTimeout, "page-timeout", 20, "seconds to wait for each page in headless mode"), flagSet.BoolVarP(&options.ShowBrowser, "show-browser", "sb", false, "show the browser on the screen when running templates with headless mode"), + flagSet.StringSliceVarP(&options.HeadlessOptionalArguments, "headless-options", "ho", nil, "start headless chrome with additional options", goflags.FileCommaSeparatedStringSliceOptions), flagSet.BoolVarP(&options.UseInstalledChrome, "system-chrome", "sc", false, "use local installed Chrome browser instead of nuclei installed"), flagSet.BoolVarP(&options.ShowActions, "list-headless-action", "lha", false, "list available headless actions"), ) diff --git a/v2/internal/runner/options.go b/v2/internal/runner/options.go index ebbe3e66..914f72a8 100644 --- a/v2/internal/runner/options.go +++ b/v2/internal/runner/options.go @@ -114,6 +114,10 @@ func validateOptions(options *types.Options) error { return errors.New("both verbose and silent mode specified") } + if (options.HeadlessOptionalArguments != nil || options.ShowBrowser || options.UseInstalledChrome) && !options.Headless { + return errors.New("headless mode (-headless) is required if -ho, -sb, -sc or -lha are set") + } + if options.FollowHostRedirects && options.FollowRedirects { return errors.New("both follow host redirects and follow redirects specified") } diff --git a/v2/internal/runner/options_test.go b/v2/internal/runner/options_test.go new file mode 100644 index 00000000..72a9adb3 --- /dev/null +++ b/v2/internal/runner/options_test.go @@ -0,0 +1,61 @@ +package runner + +import ( + "strings" + "testing" + + "github.com/projectdiscovery/goflags" + "github.com/projectdiscovery/nuclei/v2/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestParseHeadlessOptionalArguments(t *testing.T) { + tests := []struct { + name string + input string + want map[string]string + }{ + { + name: "single value", + input: "a=b", + want: map[string]string{"a": "b"}, + }, + { + name: "empty string", + input: "", + want: map[string]string{}, + }, + { + name: "empty key", + input: "=b", + want: map[string]string{}, + }, + { + name: "empty value", + input: "a=", + want: map[string]string{}, + }, + { + name: "double input", + input: "a=b,c=d", + want: map[string]string{"a": "b", "c": "d"}, + }, + { + name: "duplicated input", + input: "a=b,a=b", + want: map[string]string{"a": "b"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strsl := goflags.StringSlice{} + for _, v := range strings.Split(tt.input, ",") { + //nolint + strsl.Set(v) + } + opt := types.Options{HeadlessOptionalArguments: strsl} + got := opt.ParseHeadlessOptionalArguments() + require.Equal(t, tt.want, got) + }) + } +} diff --git a/v2/pkg/protocols/headless/engine/engine.go b/v2/pkg/protocols/headless/engine/engine.go index 85e7af7b..de43211d 100644 --- a/v2/pkg/protocols/headless/engine/engine.go +++ b/v2/pkg/protocols/headless/engine/engine.go @@ -8,6 +8,7 @@ import ( "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/launcher/flags" "github.com/pkg/errors" "github.com/projectdiscovery/nuclei/v2/pkg/types" @@ -75,6 +76,11 @@ func New(options *types.Options) (*Browser, error) { if types.ProxyURL != "" { chromeLauncher = chromeLauncher.Proxy(types.ProxyURL) } + + for k, v := range options.ParseHeadlessOptionalArguments() { + chromeLauncher.Set(flags.Flag(k), v) + } + launcherURL, err := chromeLauncher.Launch() if err != nil { return nil, err diff --git a/v2/pkg/types/types.go b/v2/pkg/types/types.go index 48347a60..c3842bbb 100644 --- a/v2/pkg/types/types.go +++ b/v2/pkg/types/types.go @@ -2,6 +2,7 @@ package types import ( "io" + "strings" "time" "github.com/projectdiscovery/goflags" @@ -197,6 +198,8 @@ type Options struct { Headless bool // ShowBrowser specifies whether the show the browser in headless mode ShowBrowser bool + // HeadlessOptionalArguments specifies optional arguments to pass to Chrome + HeadlessOptionalArguments goflags.StringSlice // NoTables disables pretty printing of cloud results in tables NoTables bool // DisableClustering disables clustering of templates @@ -441,3 +444,17 @@ func (options *Options) HasCloudOptions() bool { func (options *Options) ShouldUseHostError() bool { return options.MaxHostError > 0 && !options.NoHostErrors } + +func (options *Options) ParseHeadlessOptionalArguments() map[string]string { + optionalArguments := make(map[string]string) + for _, v := range options.HeadlessOptionalArguments { + if argParts := strings.SplitN(v, "=", 2); len(argParts) >= 2 { + key := strings.TrimSpace(argParts[0]) + value := strings.TrimSpace(argParts[1]) + if key != "" && value != "" { + optionalArguments[key] = value + } + } + } + return optionalArguments +}