diff --git a/.gitignore b/.gitignore index 57d2daa008..038e71a289 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ pkg/protocols/headless/engine/.cache **/*-cache /fuzzplayground integration_tests/fuzzplayground - +/docgen diff --git a/Makefile b/Makefile index f6950b31a7..4b3130d02b 100644 --- a/Makefile +++ b/Makefile @@ -47,4 +47,8 @@ fuzzplayground: memogen: $(GOBUILD) $(GOFLAGS) -ldflags '$(LDFLAGS)' -o "memogen" cmd/memogen/memogen.go ./memogen -src pkg/js/libs -tpl cmd/memogen/function.tpl +genschema: + rm -f nuclei-jsonschema.json docgen 2>/dev/null + $(GOBUILD) $(GOFLAGS) -ldflags '$(LDFLAGS)' -o "docgen" cmd/docgen/docgen.go + ./docgen /dev/null nuclei-jsonschema.json diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index c589b98b0e..475a4ddfa0 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -32,7 +32,9 @@ func main() { } // Generate jsonschema - r := &jsonschema.Reflector{} + r := &jsonschema.Reflector{ + BaseSchemaID: jsonschema.ID("https://nuclei.projectdiscovery.io/"), + } jsonschemaData := r.Reflect(&templates.Template{}) var buf bytes.Buffer @@ -44,8 +46,37 @@ func main() { for _, match := range pathRegex.FindAllStringSubmatch(schema, -1) { schema = strings.ReplaceAll(schema, match[0], match[1]) } + var m map[string]interface{} + err = json.Unmarshal([]byte(schema), &m) + if err != nil { + log.Fatalf("Could not unmarshal jsonschema: %s\n", err) + } + + // patch the schema to enable markdown Descriptions in monaco and vscode + updateDescriptionKeyName("", m) + + schemax, err := json.MarshalIndent(m, "", " ") + if err != nil { + log.Fatalf("Could not marshal jsonschema: %s\n", err) + } + schema = string(schemax) + err = os.WriteFile(os.Args[2], []byte(schema), 0644) if err != nil { log.Fatalf("Could not write jsonschema: %s\n", err) } } + +// will recursively find and replace/rename PropName in description +func updateDescriptionKeyName(parent string, m map[string]interface{}) { + for k, v := range m { + if k == "description" && parent != "properties" { + delete(m, k) + m["markdownDescription"] = v + } + // if v is of type object then recursively call this function + if vMap, ok := v.(map[string]interface{}); ok { + updateDescriptionKeyName(k, vMap) + } + } +} diff --git a/nuclei-jsonschema.json b/nuclei-jsonschema.json index f39d5c89f4..2b554152ab 100644 --- a/nuclei-jsonschema.json +++ b/nuclei-jsonschema.json @@ -1,184 +1,184 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://template", - "$ref": "#/$defs/Template", "$defs": { "AttackTypeHolder": { - "properties": { - "Value": { - "type": "integer" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "Value" - ] + "enum": [ + "batteringram", + "pitchfork", + "clusterbomb" + ], + "markdownDescription": "Attack is the type of payload combinations to perform", + "title": "attack is the payload combination", + "type": "string" }, "Classification": { + "additionalProperties": false, "properties": { - "cve-id": { - "$ref": "#/$defs/StringOrSlice", - "title": "cve ids for the template", - "description": "CVE IDs for the template" + "cpe": { + "examples": [ + "cpe:/a:vendor:product:version" + ], + "markdownDescription": "CPE for the template", + "title": "cpe for the template", + "type": "string" }, - "cwe-id": { + "cve-id": { "$ref": "#/$defs/StringOrSlice", - "title": "cwe ids for the template", - "description": "CWE IDs for the template" + "markdownDescription": "CVE IDs for the template", + "title": "cve ids for the template" }, "cvss-metrics": { - "type": "string", - "title": "cvss metrics for the template", - "description": "CVSS Metrics for the template", "examples": [ "3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" - ] + ], + "markdownDescription": "CVSS Metrics for the template", + "title": "cvss metrics for the template", + "type": "string" }, "cvss-score": { - "type": "number", - "title": "cvss score for the template", - "description": "CVSS Score for the template", "examples": [ 9.8 - ] + ], + "markdownDescription": "CVSS Score for the template", + "title": "cvss score for the template", + "type": "number" }, - "epss-score": { - "type": "number", - "title": "epss score for the template", - "description": "EPSS Score for the template", - "examples": [ - 0.42509 - ] + "cwe-id": { + "$ref": "#/$defs/StringOrSlice", + "markdownDescription": "CWE IDs for the template", + "title": "cwe ids for the template" }, "epss-percentile": { - "type": "number", - "title": "epss percentile for the template", - "description": "EPSS Percentile for the template", "examples": [ 0.42509 - ] + ], + "markdownDescription": "EPSS Percentile for the template", + "title": "epss percentile for the template", + "type": "number" }, - "cpe": { - "type": "string", - "title": "cpe for the template", - "description": "CPE for the template", + "epss-score": { "examples": [ - "cpe:/a:vendor:product:version" - ] + 0.42509 + ], + "markdownDescription": "EPSS Score for the template", + "title": "epss score for the template", + "type": "number" } }, - "additionalProperties": false, "type": "object" }, "Extractor": { + "additionalProperties": false, "properties": { - "name": { - "type": "string", - "title": "name of the extractor", - "description": "Name of the extractor" + "attribute": { + "markdownDescription": "Optional attribute to extract from response XPath", + "title": "optional attribute to extract from xpath", + "type": "string" }, - "type": { - "$ref": "#/$defs/ExtractorTypeHolder" + "case-insensitive": { + "markdownDescription": "use case insensitive extract", + "title": "use case insensitive extract", + "type": "boolean" }, - "regex": { + "dsl": { "items": { "type": "string" }, - "type": "array", - "title": "regex to extract from part", - "description": "Regex to extract from part" + "markdownDescription": "Optional attribute to extract from response dsl", + "title": "dsl expressions to extract", + "type": "array" }, "group": { - "type": "integer", + "markdownDescription": "Group to extract from regex", "title": "group to extract from regex", - "description": "Group to extract from regex" + "type": "integer" }, - "kval": { - "items": { - "type": "string" - }, - "type": "array", - "title": "kval pairs to extract from response", - "description": "Kval pairs to extract from response" + "internal": { + "markdownDescription": "Internal when set to true will allow using the value extracted in the next request for some protocols", + "title": "mark extracted value for internal variable use", + "type": "boolean" }, "json": { "items": { "type": "string" }, - "type": "array", + "markdownDescription": "JSON JQ expressions to evaluate from response part", "title": "json jq expressions to extract data", - "description": "JSON JQ expressions to evaluate from response part" + "type": "array" }, - "xpath": { + "kval": { "items": { "type": "string" }, - "type": "array", - "title": "html xpath expressions to extract data", - "description": "XPath allows using xpath expressions to extract items from html response" + "markdownDescription": "Kval pairs to extract from response", + "title": "kval pairs to extract from response", + "type": "array" }, - "attribute": { - "type": "string", - "title": "optional attribute to extract from xpath", - "description": "Optional attribute to extract from response XPath" + "name": { + "markdownDescription": "Name of the extractor", + "title": "name of the extractor", + "type": "string" }, - "dsl": { + "part": { + "markdownDescription": "Part of the request response to extract data from", + "title": "part of response to extract data from", + "type": "string" + }, + "regex": { "items": { "type": "string" }, - "type": "array", - "title": "dsl expressions to extract", - "description": "Optional attribute to extract from response dsl" - }, - "part": { - "type": "string", - "title": "part of response to extract data from", - "description": "Part of the request response to extract data from" + "markdownDescription": "Regex to extract from part", + "title": "regex to extract from part", + "type": "array" }, - "internal": { - "type": "boolean", - "title": "mark extracted value for internal variable use", - "description": "Internal when set to true will allow using the value extracted in the next request for some protocols" + "type": { + "$ref": "#/$defs/ExtractorTypeHolder" }, - "case-insensitive": { - "type": "boolean", - "title": "use case insensitive extract", - "description": "use case insensitive extract" + "xpath": { + "items": { + "type": "string" + }, + "markdownDescription": "XPath allows using xpath expressions to extract items from html response", + "title": "html xpath expressions to extract data", + "type": "array" } }, - "additionalProperties": false, - "type": "object", "required": [ "type" - ] + ], + "type": "object" }, "ExtractorTypeHolder": { + "additionalProperties": false, "properties": { "ExtractorType": { "type": "integer" } }, - "additionalProperties": false, - "type": "object", "required": [ "ExtractorType" - ] + ], + "type": "object" }, "HTTPMethodTypeHolder": { - "properties": { - "MethodType": { - "type": "integer" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "MethodType" - ] + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "OPTIONS", + "TRACE", + "PATCH", + "PURGE", + "DEBUG" + ], + "markdownDescription": "Method is the HTTP Request Method", + "title": "method is the HTTP request method", + "type": "string" }, "Holder": { - "type": "string", "enum": [ "info", "low", @@ -187,554 +187,739 @@ "critical", "unknown" ], + "markdownDescription": "Seriousness of the implications of the template", "title": "severity of the template", - "description": "Seriousness of the implications of the template" + "type": "string" }, "Info": { + "additionalProperties": false, + "examples": [ + "\n\tname: Example Template\n\tauthor: pdteam\n\tseverity: info\n\tdescription: This is an example template", + "\n\tname: example-template\n\tauthor: pdteam\n\tseverity: unknown\n\tdescription: Example description of the template\n\timpact: Impact of the template\n\treference: \n\t - https://example.com\n\tmetadata: \n\t max-request: 1\n\ttags: http" + ], "properties": { - "name": { - "type": "string", - "title": "name of the template", - "description": "Name is a short summary of what the template does", - "examples": [ - "Nagios Default Credentials Check" - ] - }, "author": { - "$ref": "#/$defs/StringOrSlice", + "markdownDescription": "Author is the author of the template", "oneOf": [ { - "type": "string", + "description": "Author of the template without any special characters\nIt can be a single author or comma-separated multiple authors", "examples": [ "pdteam" - ] + ], + "type": "string" }, { - "type": "array", + "description": "Author of the template without any special characters\nIt can be a single author or comma-separated multiple authors", "examples": [ "pdteam,mr.robot" - ] + ], + "type": "array" } ], - "title": "author of the template", - "description": "Author is the author of the template" + "title": "author of the template" }, - "tags": { - "$ref": "#/$defs/StringOrSlice", - "title": "tags of the template", - "description": "Any tags for the template" + "classification": { + "$ref": "#/$defs/Classification", + "examples": [ + "\n\tcvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\n\tcvss-score: 10\n\tcve-id: CVE-2021-44228", + "\n\tcwe-id: CWE-20,CWE-917" + ], + "markdownDescription": "Classification contains classification information about the template.\nThis could be any classification information like CWE, CVE, EPSS etc.\nNote - This is autogenerated and updated regularly for CVE templates merged in projectdiscovery/nuclei-templates repository.\nExample:\n```yaml\ncvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\ncvss-score: 10\ncve-id: CVE-2021-44228\ncwe-id: CWE-20,CWE-917\nepss-score: 0.97453\nepss-percentile: 0.99942\ncpe: cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*\n```", + "title": "classification info for the template", + "type": "object" }, "description": { - "type": "string", - "title": "description of the template", - "description": "In-depth explanation on what the template does", "examples": [ - "Bower is a package manager which stores package information in the bower.json file" - ] + "Bower is a package manager which stores package information in the bower.json file", + "Example description of the template" + ], + "markdownDescription": "Description of the template.\nYou can go in-depth here on what the template actually does.\nExample:\nApache Log4j2 \u003c=2.14.1 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled.", + "title": "description of the template", + "type": "string" }, "impact": { - "type": "string", - "title": "impact of the template", - "description": "In-depth explanation on the impact of the issue found by the template", "examples": [ - "Successful exploitation of this vulnerability could allow an attacker to execute arbitrary SQL queries" - ] - }, - "reference": { - "$ref": "#/$defs/StringOrSlice", - "title": "references for the template", - "description": "Links relevant to the template" + "Successful exploitation of this vulnerability could allow an attacker to execute arbitrary SQL queries", + "Impact of the template" + ], + "markdownDescription": "Impact of the template.\nYou can go in-depth here on impact of the template.\nExample:\nSuccessful exploitation of this vulnerability can lead to remote code execution, potentially compromising the affected system.", + "title": "impact of the template", + "type": "string" }, - "severity": { - "$ref": "#/$defs/Holder" + "markdownDescription": { + "markdownDescription": "Classification contains classification information about the template.\nThis could be any classification information like CWE, CVE, EPSS etc.\nNote - This is autogenerated and updated regularly for CVE templates merged in projectdiscovery/nuclei-templates repository.\nExample:\n```yaml\ncvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\ncvss-score: 10\ncve-id: CVE-2021-44228\ncwe-id: CWE-20,CWE-917\nepss-score: 0.97453\nepss-percentile: 0.99942\ncpe: cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*\n```", + "type": "string" }, "metadata": { - "type": "object", + "examples": [ + "\n\tmax-request: 1", + "\n\tmax-request: 2\n\tvendor: apache\n\tproduct: log4j2" + ], + "markdownDescription": "Optional metadata for the template.\nthis could be any useful metadata like Vendor, Product, Version, etc.\nNote - This is autogenerated and updated regularly for templates merged in projectdiscovery/nuclei-templates repository.\nExample:\n```yaml\nVendor: Apache\nProduct: Log4j2\nVersion: \u003c=2.14.1\n```", "title": "additional metadata for the template", - "description": "Additional metadata fields for the template" + "type": "object" }, - "classification": { - "$ref": "#/$defs/Classification", - "type": "object", - "title": "classification info for the template", - "description": "Classification information for the template" + "name": { + "examples": [ + "Nagios Default Credentials Check", + "example-template", + "TeamCity - Authentication Bypass" + ], + "markdownDescription": "Name should be good short summary that identifies what the template does", + "title": "name of the template", + "type": "string" + }, + "reference": { + "$ref": "#/$defs/StringOrSlice", + "examples": [ + "\n\t- https://example.com" + ], + "markdownDescription": "References for the template.\nThis should contain links relevant to the template.\nExample:\nhttps://logging.apache.org/log4j/2.x/security.html\nhttps://nvd.nist.gov/vuln/detail/CVE-2021-44228", + "title": "references for the template", + "type": "array" }, "remediation": { - "type": "string", - "title": "remediation steps for the template", - "description": "In-depth explanation on how to fix the issues found by the template", "examples": [ "Change the default administrative username and password of Apache ActiveMQ by editing the file jetty-realm.properties" - ] + ], + "markdownDescription": "In-depth explanation on how to fix the issues found by the template", + "title": "remediation steps for the template", + "type": "string" + }, + "severity": { + "$ref": "#/$defs/Holder", + "examples": [ + "info" + ], + "markdownDescription": "Severity of the template.\nsupported values: info, low, medium, high, critical, unknown" + }, + "tags": { + "markdownDescription": "Any tags for the template", + "oneOf": [ + { + "description": "Tags for the template separated by commas (No spaces)", + "examples": [ + "cve" + ], + "type": "string" + }, + { + "description": "Multiple tags for the template separated by commas (No spaces)", + "examples": [ + "cve,http", + "http,oast", + "cve2024,cve" + ], + "type": "array" + } + ], + "title": "tags of the template" } }, - "additionalProperties": false, - "type": "object", "required": [ "name", "author" - ] + ], + "type": "object" }, "Matcher": { + "additionalProperties": false, "properties": { - "type": { - "$ref": "#/$defs/MatcherTypeHolder", - "title": "type of matcher", - "description": "Type of the matcher" + "binary": { + "items": { + "type": "string" + }, + "markdownDescription": "Binary are the binary patterns required to be present in the response part", + "title": "binary patterns to match in response", + "type": "array" + }, + "case-insensitive": { + "markdownDescription": "use case insensitive match", + "title": "use case insensitive match", + "type": "boolean" }, "condition": { - "type": "string", "enum": [ "and", "or" ], + "markdownDescription": "Condition between the matcher variables", "title": "condition between matcher variables", - "description": "Condition between the matcher variables" + "type": "string" }, - "part": { - "type": "string", - "title": "part of response to match", - "description": "Part of response to match data from" + "dsl": { + "items": { + "type": "string" + }, + "markdownDescription": "DSL are the dsl expressions that will be evaluated as part of nuclei matching rules", + "title": "dsl expressions to match in response", + "type": "array" }, - "negative": { - "type": "boolean", - "title": "negative specifies if match reversed", - "description": "Negative specifies if the match should be reversed. It will only match if the condition is not true" + "encoding": { + "enum": [ + "hex" + ], + "markdownDescription": "Optional encoding for the word fields", + "title": "encoding for word field", + "type": "string" + }, + "internal": { + "markdownDescription": "hide matcher from output", + "title": "hide matcher from output", + "type": "boolean" + }, + "match-all": { + "markdownDescription": "match all matcher values ignoring condition", + "title": "match all values", + "type": "boolean" }, "name": { - "type": "string", + "markdownDescription": "Name of the matcher", "title": "name of the matcher", - "description": "Name of the matcher" + "type": "string" }, - "status": { + "negative": { + "markdownDescription": "Negative specifies if the match should be reversed. It will only match if the condition is not true", + "title": "negative specifies if match reversed", + "type": "boolean" + }, + "part": { + "markdownDescription": "Part of response to match data from", + "title": "part of response to match", + "type": "string" + }, + "regex": { "items": { - "type": "integer" + "type": "string" }, - "type": "array", - "title": "status to match", - "description": "Status to match for the response" + "markdownDescription": "Regex contains regex patterns required to be present in the response part", + "title": "regex to match in response", + "type": "array" }, "size": { "items": { "type": "integer" }, - "type": "array", + "markdownDescription": "Size is the acceptable size for the response", "title": "acceptable size for response", - "description": "Size is the acceptable size for the response" + "type": "array" }, - "words": { - "items": { - "type": "string" - }, - "type": "array", - "title": "words to match in response", - "description": " Words contains word patterns required to be present in the response part" - }, - "regex": { + "status": { "items": { - "type": "string" + "type": "integer" }, - "type": "array", - "title": "regex to match in response", - "description": "Regex contains regex patterns required to be present in the response part" + "markdownDescription": "Status to match for the response", + "title": "status to match", + "type": "array" }, - "binary": { - "items": { - "type": "string" - }, - "type": "array", - "title": "binary patterns to match in response", - "description": "Binary are the binary patterns required to be present in the response part" + "type": { + "$ref": "#/$defs/MatcherTypeHolder", + "markdownDescription": "Type of the matcher", + "title": "type of matcher" }, - "dsl": { + "words": { "items": { "type": "string" }, - "type": "array", - "title": "dsl expressions to match in response", - "description": "DSL are the dsl expressions that will be evaluated as part of nuclei matching rules" + "markdownDescription": " Words contains word patterns required to be present in the response part", + "title": "words to match in response", + "type": "array" }, "xpath": { "items": { "type": "string" }, - "type": "array", + "markdownDescription": "xpath are the XPath queries that will be evaluated against the response part of nuclei matching rules", "title": "xpath queries to match in response", - "description": "xpath are the XPath queries that will be evaluated against the response part of nuclei matching rules" - }, - "encoding": { - "type": "string", - "enum": [ - "hex" - ], - "title": "encoding for word field", - "description": "Optional encoding for the word fields" - }, - "case-insensitive": { - "type": "boolean", - "title": "use case insensitive match", - "description": "use case insensitive match" - }, - "match-all": { - "type": "boolean", - "title": "match all values", - "description": "match all matcher values ignoring condition" - }, - "internal": { - "type": "boolean", - "title": "hide matcher from output", - "description": "hide matcher from output" + "type": "array" } }, - "additionalProperties": false, - "type": "object", "required": [ "type" - ] + ], + "type": "object" }, "MatcherTypeHolder": { + "additionalProperties": false, "properties": { "MatcherType": { "type": "integer" } }, - "additionalProperties": false, - "type": "object", "required": [ "MatcherType" - ] + ], + "type": "object" }, "OrderedMap[string,string]": { - "properties": {}, "additionalProperties": false, + "properties": {}, "type": "object" }, "Request": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "raw" + ] + }, + { + "required": [ + "filters", + "payload", + "fuzzing" + ] + }, + { + "required": [ + "payloads", + "path" + ] + }, + { + "required": [ + "payloads", + "raw" + ] + } + ], "properties": { - "matchers": { - "items": { - "$ref": "#/$defs/Matcher" - }, - "type": "array", - "title": "matchers to run on response", - "description": "Detection mechanism to identify whether the request was successful by doing pattern matching" + "attack": { + "$ref": "#/$defs/AttackTypeHolder", + "examples": [ + "batteringram", + "pitchfork", + "clusterbomb" + ], + "markdownDescription": "Attack is the type of payload combinations to perform\nbatteringram the same payload into all defined payload positions at once, pitchfork combines multiple payload sets and clusterbomb generates permutations and combinations for all payloads\nExample: \n```yaml\n- attack: clusterbomb\n```\nNote: this is only applicable when 'payloads' are defined" + }, + "body": { + "examples": [ + "key=value\u0026key2=value2" + ], + "markdownDescription": "Body contains the HTTP body to be sent with the request\nExample: \n```yaml\n- body: \"key=value\u0026key2=value2\"\n```", + "title": "body is the http request body", + "type": "string" + }, + "cookie-reuse": { + "deprecated": true, + "examples": [ + true + ], + "markdownDescription": "Deprecated: this is default behaviour now use disable-cookie to explicitly disable cookie reuse", + "title": "optional cookie reuse enable", + "type": "boolean" + }, + "digest-password": { + "examples": [ + "admin" + ], + "markdownDescription": "DigestPassword specifies the password for digest authentication\nExample: \n```yaml\n- digest-password: admin\n```", + "title": "specifies the password for digest authentication", + "type": "string" + }, + "digest-username": { + "examples": [ + "admin" + ], + "markdownDescription": "DigestUsername specifies the username for digest authentication\nExample: \n```yaml\n- digest-username: admin\n```", + "title": "specifies the username for digest authentication", + "type": "string" + }, + "disable-cookie": { + "examples": [ + true + ], + "markdownDescription": "Disable Cookie is an optional setting to disable cookie reuse for this request\nExample: \n```yaml\n- disable-cookie: true\n```", + "title": "optional disable cookie reuse", + "type": "boolean" + }, + "disable-path-automerge": { + "examples": [ + true + ], + "markdownDescription": "DisablePathAutomerge disables the automatic merging of path/query-params from input/target to path defined in template\nExample: \n```yaml\n- disable-path-automerge: true\n```", + "title": "disable auto merging of path", + "type": "boolean" }, "extractors": { "items": { "$ref": "#/$defs/Extractor" }, - "type": "array", + "markdownDescription": "Extractors contains the extraction mechanism for the request to identify and extract parts of the response", "title": "extractors to run on response", - "description": "Extractors contains the extraction mechanism for the request to identify and extract parts of the response" + "type": "array" }, - "matchers-condition": { - "type": "string", + "filter": { + "items": { + "$ref": "#/$defs/Matcher" + }, + "markdownDescription": "Filter is matcher-like field to check if fuzzing should be performed on this request or not", + "title": "filter for fuzzing", + "type": "array" + }, + "filter-condition": { "enum": [ "and", "or" ], - "title": "condition between the matchers", - "description": "Conditions between the matchers" + "markdownDescription": "Conditions between the filters", + "title": "condition between the filters", + "type": "string" }, - "path": { + "fuzzing": { "items": { - "type": "string" + "$ref": "#/$defs/Rule" }, - "type": "array", - "title": "path(s) for the http request", - "description": "Path(s) to send http requests to" + "markdownDescription": "Fuzzing or DAST specifies rules for fuzzing requests\nthese rules mutate the input request to generate multiple requests\nFor example, replacing all query param values with sqli payloads", + "title": "fuzzin rules for http fuzzing", + "type": "array" }, - "raw": { - "items": { + "headers": { + "additionalProperties": { "type": "string" }, - "type": "array", - "description": "HTTP Requests in Raw Format" + "examples": [ + "User-Agent: Mozilla/5.0", + "Authorization: Bearer {{token}}", + "X-Forwarded-For: {{Hostname}}", + "Origin: {{BaseURL}}", + "Referer: {{BaseURL}}/admin" + ], + "markdownDescription": "Headers contains the HTTP headers to be sent with the request\nExample: \n```yaml\n- headers:\n\tUser-Agent: Mozilla/5.0\n```", + "title": "headers to send with the http request", + "type": "object" + }, + "host-redirects": { + "examples": [ + true + ], + "markdownDescription": "HostRedirects specifies whether only redirects to the same host should be followed by the HTTP Client\nWhen set to true, only redirects to the same host are followed\nExample: \n```yaml\n- host-redirects: true\n```", + "title": "follow same host http redirects", + "type": "boolean" }, "id": { - "type": "string", + "examples": [ + "example-http-id" + ], + "markdownDescription": "Optional ID of this HTTP Request Block\nThis is useful when calling /executing this request block in flow\nBy default, current index in the http array is used as ID\nExample: \n```yaml\n- id: wp-login\n```", "title": "id for the http request", - "description": "ID for the HTTP Request" + "type": "string" }, - "name": { - "type": "string", - "title": "name for the http request", - "description": "Optional name for the HTTP Request" + "iterate-all": { + "deprecated": true, + "markdownDescription": "Deprecated: use flow to properly specify iteration logic", + "title": "iterate all the values", + "type": "boolean" }, - "attack": { - "$ref": "#/$defs/AttackTypeHolder", - "title": "attack is the payload combination", - "description": "Attack is the type of payload combinations to perform" + "markdownDescription": { + "markdownDescription": "Signature is the HTTP Request signature Method", + "type": "string" + }, + "matchers": { + "items": { + "$ref": "#/$defs/Matcher" + }, + "markdownDescription": "Detection mechanism to identify whether the request was successful by doing pattern matching", + "title": "matchers to run on response", + "type": "array" + }, + "matchers-condition": { + "enum": [ + "and", + "or" + ], + "markdownDescription": "Conditions between the matchers", + "title": "condition between the matchers", + "type": "string" + }, + "max-redirects": { + "examples": [ + 10 + ], + "markdownDescription": "MaxRedirects is the maximum number of redirects to follow\nExample: \n```yaml\n- max-redirects: 10\n```", + "title": "maximum number of redirects to follow", + "type": "integer" + }, + "max-size": { + "examples": [ + 1024 + ], + "markdownDescription": "MaxSize is the maximum size of the response body to be read\nExample: \n```yaml\n- max-size: 1024\n```", + "title": "maximum http response body size", + "type": "integer" }, "method": { "$ref": "#/$defs/HTTPMethodTypeHolder", - "title": "method is the http request method", - "description": "Method is the HTTP Request Method" - }, - "body": { - "type": "string", - "title": "body is the http request body", - "description": "Body is an optional parameter which contains HTTP Request body" + "examples": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "OPTIONS", + "TRACE", + "PATCH", + "PURGE", + "DEBUG" + ], + "markdownDescription": "Method is the HTTP method to be used for the request\nExample: \n```yaml\n- method: GET\n```" }, - "payloads": { - "type": "object", - "title": "payloads for the http request", - "description": "Payloads contains any payloads for the current request" + "name": { + "examples": [ + "example-http-name" + ], + "markdownDescription": "Name is the optional name of the request\nIf a name is specified, all the named request in a template can be matched upon\nin a combined manner allowing multi-request based matchers.", + "title": "name for the http request", + "type": "string" }, - "headers": { - "additionalProperties": { + "path": { + "examples": [ + "\n\t- '{{BaseURL}}'", + "\n\t- '{{RootURL}}'", + "\n\t- '{{Hostname}}:8080/ca-cert'" + ], + "items": { "type": "string" }, - "type": "object", - "title": "headers to send with the http request", - "description": "Headers contains HTTP Headers to send with the request" + "markdownDescription": "Path contains HTTP URL[s] to be sent. Here only Path portion is generally specified and host is templatetized\nExample: \n```yaml\n- path:\n\t{{BaseURL}}/admin\n```", + "title": "path(s) for the http request", + "type": "array" }, - "race_count": { - "type": "integer", - "title": "number of times to repeat request in race condition", - "description": "Number of times to send a request in Race Condition Attack" + "payloads": { + "examples": [ + " # inline payloads\n\tusername:\n\t\t- admin\n\t\t- root\n\tpassword:\n\t\t- password\n\t\t- admin\n\n", + " # file payloads\n\tusername: /path/to/usernames.txt\n\tpassword: /path/to/passwords.txt\n\n" + ], + "markdownDescription": "Payloads contains key-value pairs of payloads to be used\nThese payloads when referenced in http request will be iterated appropriately and replaced\nOptionally payload also support loading values from file instead of defining them inline\nExample: \n```yaml\n- payloads:\n\tusername:\n\t\t- admin\n\t\t- root\n\tpassword:\n\t\t- password\n\t\t- admin\n```", + "title": "payloads for the http request", + "type": "object" }, - "max-redirects": { - "type": "integer", - "title": "maximum number of redirects to follow", - "description": "Maximum number of redirects that should be followed" + "pipeline": { + "examples": [ + true + ], + "markdownDescription": "Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining\nAll requests must be idempotent (GET/POST). This can be used for race conditions/billions requests.\nExample: \n```yaml\n- pipeline: true\n```", + "title": "perform HTTP 1.1 pipelining", + "type": "boolean" }, "pipeline-concurrent-connections": { - "type": "integer", + "examples": [ + 10 + ], + "markdownDescription": "PipelineConcurrentConnections specifies the number of concurrent connections to be used for pipelining\nExample: \n```yaml\n- pipeline-concurrent-connections: 10\n pipeline: true\n```", "title": "number of pipelining connections", - "description": "Number of connections to create during pipelining" + "type": "integer" }, "pipeline-requests-per-connection": { - "type": "integer", + "examples": [ + 10 + ], + "markdownDescription": "PipelineRequestsPerConnection specifies the number of requests to be sent per connection\nExample: \n```yaml\n- pipeline: true\n pipeline-requests-per-connection: 10\n```", "title": "number of requests to send per pipelining connections", - "description": "Number of requests to send per connection when pipelining" + "type": "integer" }, - "threads": { - "type": "integer", - "title": "threads for sending requests", - "description": "Threads specifies number of threads to use sending requests. This enables Connection Pooling" + "race": { + "examples": [ + true + ], + "markdownDescription": "Race determines if all the request have to be attempted at the same time (Race Condition)\nThe actual number of requests that will be sent is determined by the `race_count` field.\nExample: \n```yaml\n- race: true\n race_count: 100\n```", + "title": "perform race-http request coordination attack", + "type": "boolean" }, - "max-size": { - "type": "integer", - "title": "maximum http response body size", - "description": "Maximum size of http response body to read in bytes" + "race_count": { + "examples": [ + 100 + ], + "markdownDescription": "RaceCount specifies the number of requests to be sent when attempting race condition attacks\nExample: \n```yaml\n- race: true\n race_count: 100\n```", + "title": "number of times to repeat request in race condition", + "type": "integer" }, - "fuzzing": { + "raw": { + "examples": [ + "\n# raw-get-request\n\t- |\n\t\tGET / HTTP/1.1\n\t\tHost: {{Hostname}}\n\n", + "\n# raw-post-request\n\t- |\n\t\tPOST / HTTP/1.1\n\t\tHost: {{Hostname}}\n\t\tContent-Type: application/json\n\t\tContent-Length: 13\n\n\n\t\t{\"key\": \"value\"}\n\n" + ], "items": { - "$ref": "#/$defs/Rule" + "type": "string" }, - "type": "array", - "title": "fuzzin rules for http fuzzing", - "description": "Fuzzing describes rule schema to fuzz http requests" - }, - "self-contained": { - "type": "boolean" - }, - "signature": { - "$ref": "#/$defs/SignatureTypeHolder", - "title": "signature is the http request signature method", - "description": "Signature is the HTTP Request signature Method" - }, - "cookie-reuse": { - "type": "boolean", - "title": "optional cookie reuse enable", - "description": "Optional setting that enables cookie reuse" - }, - "disable-cookie": { - "type": "boolean", - "title": "optional disable cookie reuse", - "description": "Optional setting that disables cookie reuse" + "markdownDescription": "Raw contains HTTP Request[s] in raw or request dump format\nExample: \n- raw:\n\t- |\n\t\tGET / HTTP/1.1\n\t\tHost: {{Hostname}}\n\n\n\nNote: If input contains any path/query-params they are automerged unless explicitly disabled using 'disable-path-automerge'", + "type": "array" }, "read-all": { - "type": "boolean", + "examples": [ + true + ], + "markdownDescription": "Enables force reading of the entire raw unsafe request body ignoring any specified content length headers\nExample: \n```yaml\n- read-all: true\n```", "title": "force read all body", - "description": "Enables force reading of entire unsafe http request body" + "type": "boolean" }, "redirects": { - "type": "boolean", + "examples": [ + true + ], + "markdownDescription": "Redirects specifies whether to follow redirects or not\nThis can be use in conjunction with max-redirects to effectively control the redirects\nExample: \n```yaml\n- redirects: true\n```", "title": "follow http redirects", - "description": "Specifies whether redirects should be followed by the HTTP Client" - }, - "host-redirects": { - "type": "boolean", - "title": "follow same host http redirects", - "description": "Specifies whether redirects to the same host should be followed by the HTTP Client" - }, - "pipeline": { - "type": "boolean", - "title": "perform HTTP 1.1 pipelining", - "description": "Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining" - }, - "unsafe": { - "type": "boolean", - "title": "use rawhttp non-strict-rfc client", - "description": "Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests" - }, - "race": { - "type": "boolean", - "title": "perform race-http request coordination attack", - "description": "Race determines if all the request have to be attempted at the same time (Race Condition)" + "type": "boolean" }, "req-condition": { - "type": "boolean", + "deprecated": true, + "markdownDescription": "Deprecated: this is automatically identified and used (https://github.com/projectdiscovery/nuclei/issues/2393)", "title": "preserve request history", - "description": "Automatically assigns numbers to requests and preserves their history" + "type": "boolean" }, - "stop-at-first-match": { - "type": "boolean", - "title": "stop at first match", - "description": "Stop the execution after a match is found" + "self-contained": { + "examples": [ + true + ], + "markdownDescription": "SelfContained specifies whether the request is self-contained or not\nThis means if the request is dependent on input or is self-contained\nExample: \n```yaml\n- self-contained: true\n```", + "type": "boolean" + }, + "signature": { + "$ref": "#/$defs/SignatureTypeHolder", + "deprecated": true, + "markdownDescription": "Signature is the HTTP Request signature Method", + "title": "signature is the http request signature method" }, "skip-variables-check": { - "type": "boolean", + "examples": [ + true + ], + "markdownDescription": "SkipVariablesCheck skips the check for unresolved variables in request\nThis is also useful in case where body contains payload in format of {{xyz}}\nExample: \n```yaml\n- skip-variables-check: true\n```", "title": "skip variable checks", - "description": "Skips the check for unresolved variables in request" - }, - "iterate-all": { - "type": "boolean", - "title": "iterate all the values", - "description": "Iterates all the values extracted from internal extractors" - }, - "digest-username": { - "type": "string", - "title": "specifies the username for digest authentication", - "description": "Optional parameter which specifies the username for digest auth" - }, - "digest-password": { - "type": "string", - "title": "specifies the password for digest authentication", - "description": "Optional parameter which specifies the password for digest auth" + "type": "boolean" }, - "disable-path-automerge": { - "type": "boolean", - "title": "disable auto merging of path", - "description": "Disable merging target url path with raw request path" + "stop-at-first-match": { + "markdownDescription": "StopAtFirstMatch specifies whether to stop at first match or not\nThis is useful for cases like brute-forcing where we want to stop once we find the first correct login credential\nExample: \n```yaml\n- stop-at-first-match: true\n```", + "title": "stop at first match", + "type": "boolean" }, - "filter": { - "items": { - "$ref": "#/$defs/Matcher" - }, - "type": "array", - "title": "filter for fuzzing", - "description": "Filter is matcher-like field to check if fuzzing should be performed on this request or not" + "threads": { + "examples": [ + 10 + ], + "markdownDescription": "Threads specifies concurreny with which requests should be sent\nIt should not be specified unless explicitly behaviour and its value is obtained from -pc (-payload-concurrency) flag\nExample: \n```yaml\n- threads: 10\n```", + "title": "threads for sending requests", + "type": "integer" }, - "filter-condition": { - "type": "string", - "enum": [ - "and", - "or" + "unsafe": { + "examples": [ + true ], - "title": "condition between the filters", - "description": "Conditions between the filters" + "markdownDescription": "Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests.\nThis uses the [rawhttp](https://github.com/projectdiscovery/rawhttp) engine to achieve complete\ncontrol over the request, with no normalization performed by the client.\nExample: \n```yaml\n- unsafe: true\n```", + "title": "use rawhttp non-strict-rfc client", + "type": "boolean" } }, - "additionalProperties": false, "type": "object" }, "Rule": { + "additionalProperties": false, "properties": { - "type": { - "type": "string", + "fuzz": { + "$ref": "#/$defs/SliceOrMapSlice", + "markdownDescription": "Payloads to perform fuzzing substitutions with", + "title": "payloads of fuzz rule" + }, + "keys": { + "items": { + "type": "string" + }, + "markdownDescription": "Keys of parameters to fuzz", + "title": "keys of parameters to fuzz", + "type": "array" + }, + "keys-regex": { + "items": { + "type": "string" + }, + "markdownDescription": "Regex of parameter keys to fuzz", + "title": "keys regex to fuzz", + "type": "array" + }, + "mode": { "enum": [ - "replace", - "prefix", - "postfix", - "infix", - "replace-regex" + "single", + "multiple" ], - "title": "type of rule", - "description": "Type of fuzzing rule to perform" + "markdownDescription": "Mode of request rule to fuzz", + "title": "mode of rule", + "type": "string" }, "part": { - "type": "string", "enum": [ "query", "header", "path", "body", "cookie", - "request" - ], - "title": "part of rule", - "description": "Part of request rule to fuzz" - }, - "mode": { - "type": "string", - "enum": [ - "single", - "multiple" + "request" ], - "title": "mode of rule", - "description": "Mode of request rule to fuzz" + "markdownDescription": "Part of request rule to fuzz", + "title": "part of rule", + "type": "string" }, - "keys": { - "items": { - "type": "string" - }, - "type": "array", - "title": "keys of parameters to fuzz", - "description": "Keys of parameters to fuzz" + "replace-regex": { + "markdownDescription": "Regex for regex-replace rule type", + "title": "replace regex of rule", + "type": "string" }, - "keys-regex": { - "items": { - "type": "string" - }, - "type": "array", - "title": "keys regex to fuzz", - "description": "Regex of parameter keys to fuzz" + "type": { + "enum": [ + "replace", + "prefix", + "postfix", + "infix", + "replace-regex" + ], + "markdownDescription": "Type of fuzzing rule to perform", + "title": "type of rule", + "type": "string" }, "values": { "items": { "type": "string" }, - "type": "array", + "markdownDescription": "Regex of parameter values to fuzz", "title": "values regex to fuzz", - "description": "Regex of parameter values to fuzz" - }, - "fuzz": { - "$ref": "#/$defs/SliceOrMapSlice", - "title": "payloads of fuzz rule", - "description": "Payloads to perform fuzzing substitutions with" - }, - "replace-regex": { - "type": "string", - "title": "replace regex of rule", - "description": "Regex for regex-replace rule type" + "type": "array" } }, - "additionalProperties": false, "type": "object" }, "SignatureTypeHolder": { - "properties": { - "Value": { - "type": "integer" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "Value" - ] + "enum": [ + "AWS" + ], + "markdownDescription": "Type of the signature", + "title": "type of the signature", + "type": "string" }, "SliceOrMapSlice": { + "additionalProperties": false, "properties": { + "KV": { + "$ref": "#/$defs/OrderedMap[string,string]" + }, "Value": { "items": { "type": "string" }, "type": "array" - }, - "KV": { - "$ref": "#/$defs/OrderedMap[string,string]" } }, - "additionalProperties": false, - "type": "object", "required": [ "Value", "KV" - ] + ], + "type": "object" }, "StringOrSlice": { "oneOf": [ @@ -747,204 +932,336 @@ ] }, "Template": { - "properties": { - "id": { - "type": "string", - "pattern": "^([a-zA-Z0-9]+[-_])*[a-zA-Z0-9]+$", - "title": "id of the template", - "description": "The Unique ID for the template", - "examples": [ - "cve-2021-19520" + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "id", + "info", + "http" ] }, - "info": { - "$ref": "#/$defs/Info", - "type": "object", - "title": "info for the template", - "description": "Info contains metadata for the template" + { + "required": [ + "id", + "info", + "dns" + ] }, - "flow": { - "type": "string", - "title": "template execution flow in js", - "description": "Flow contains js code which defines how the template should be executed", - "examples": [ - "'flow: http(0) \u0026\u0026 http(1)'" + { + "required": [ + "id", + "info", + "file" ] }, - "requests": { - "items": { - "$ref": "#/$defs/Request" - }, - "type": "array", - "title": "http requests to make", - "description": "HTTP requests to make for the template" + { + "required": [ + "id", + "info", + "tcp" + ] }, - "http": { + { + "required": [ + "id", + "info", + "headless" + ] + }, + { + "required": [ + "id", + "info", + "ssl" + ] + }, + { + "required": [ + "id", + "info", + "websocket" + ] + }, + { + "required": [ + "id", + "info", + "whois" + ] + }, + { + "required": [ + "id", + "info", + "code" + ] + }, + { + "required": [ + "id", + "info", + "javascript" + ] + }, + { + "required": [ + "id", + "info", + "requests" + ] + }, + { + "required": [ + "id", + "info", + "network" + ] + }, + { + "required": [ + "workflows" + ] + } + ], + "properties": { + "code": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "http requests to make", - "description": "HTTP requests to make for the template" + "markdownDescription": "Code snippets", + "title": "code snippets to make", + "type": "array" + }, + "constants": { + "examples": [ + "\n\texploit: 'x0x0x0x0x0x0x'" + ], + "markdownDescription": "Constants are the global constants that once defined here can be used anywhere in the template\nIt can be used in same way as variables but only difference is that constants cannot be overridden by -V flag at runtime\nExample: \n```yaml\nconstants:\n exploit: 'x0x0x0x0x0x0x'\n```", + "title": "constant for the template", + "type": "object" }, "dns": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", + "markdownDescription": "DNS requests to make for the template", "title": "dns requests to make", - "description": "DNS requests to make for the template" + "type": "array" }, "file": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", + "markdownDescription": "File requests to make for the template", "title": "file requests to make", - "description": "File requests to make for the template" + "type": "array" }, - "network": { + "flow": { + "examples": [ + "http(1) \u0026\u0026 http(2)", + " | # example-vhost-enum\n\tssl(); // -\u003e execute all ssl protocol requests\n\tdns(); // -\u003e execute all dns protocol requests\n\tfor (let got of template.domains) { // -\u003e iterate over 'domains' array variable\n\t\tset('vhost', got); // -\u003e set 'vhost' variable to current domain\n\t\thttp(); // -\u003e execute all http protocol requests\n\t}\n" + ], + "markdownDescription": "Flow describes how multiple request-blocks/protocols should be combined together and executed\nIt is a javascript code where each protocol is a function and all request-blocks are indexed by their corresponding index in request array\nExample: \n flow: http(1) \u0026\u0026 http(2)\n means that second http request will be executed only if first http request is successful (aka matched)", + "title": "template execution flow in js", + "type": "string" + }, + "headless": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "network requests to make", - "description": "Network requests to make for the template" + "markdownDescription": "Headless requests to make for the template", + "title": "headless requests to make", + "type": "array" }, - "tcp": { + "http": { + "examples": [ + " # http get\n\t- method: GET\n\t\tpath:\n\t\t\t- \"{{BaseURL}}\"\n\t\t\n\t\tmatchers:\n\t\t\t- type: status\n\t\t\t\tstatus:\n\t\t\t\t\t- 200", + " # raw get\n\t- raw:\n\t\t\t- |\n\t\t\t\t\tGET HTTP/1.1\n\t\t\t\t\tHost: {{Hostname}}\n\t\t\t\t\n\t\t\tmatchers:\n\t\t\t- type: status\n\t\t\t\tstatus:\n\t\t\t\t\t- 200", + " # http self contained\n\t- method: GET\n\t\tpath:\n\t\t - \"https://api.stripe.com/v1/charges\"\n\t\t\n\t\tself-contained: true\n\t\theaders:\n\t\t\tAuthorization: 'Basic {{base64(token)}}'\n\t\t\n\t\tmatchers:\n\t\t - type: word\n\t\t part: body\n\t\t words:\n\t\t - '\"object\":'\n\t\t - '\"url\":'\n\t\t - '\"data\":'\n\t\t condition: and" + ], "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "network(tcp) requests to make", - "description": "Network requests to make for the template" + "markdownDescription": "HTTP is the protocol block for sending HTTP requests\nIt is of array type and can contain multiple requests\nExample: \n```yaml\nhttp: \n\t- method: GET\n\t\tpath:\n\t\t\t- \"{{BaseURL}}\"\n\t\t\n\t\tmatchers:\n\t\t\t- type: status\n\t\t\tstatus:\n\t\t\t\t- 200\n```", + "title": "http requests to make", + "type": "array" }, - "headless": { + "id": { + "examples": [ + "example-id", + "git-config-exposure", + "azure-apps-nxdomain-takeover", + "cve-2021-19520" + ], + "markdownDescription": "ID is the unique id for the template.\n ### Good IDs \nA good ID uniquely identifies what the requests in the template\nare doing. Let's say you have a template that identifies a git-config\nfile on the webservers, a good name would be `git-config-exposure`. Another\nexample name is `azure-apps-nxdomain-takeover`", + "pattern": "^([a-zA-Z0-9]+[-_])*[a-zA-Z0-9]+$", + "title": "id of the template", + "type": "string" + }, + "info": { + "$ref": "#/$defs/Info", + "markdownDescription": "Info contains the required metadata information about the template\nIt is meant to provide basic but necessary info like name, author , severity\nalong with many other optional fields like metadata, classification etc\nExample: \n```yaml\ninfo: \n\tname: Example Template\n\tauthor: pdteam\n\tseverity: info\n\tdescription: This is an example template\n```\n\nNote: - \nFor a template to be valid name,author,severity of `info` section must be set", + "title": "info for the template", + "type": "object" + }, + "javascript": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "headless requests to make", - "description": "Headless requests to make for the template" + "markdownDescription": "Javascript requests to make for the template", + "title": "javascript requests to make", + "type": "array" }, - "ssl": { + "markdownDescription": { + "markdownDescription": "HTTP is the protocol block for sending HTTP requests\nIt is of array type and can contain multiple requests\nExample: \n```yaml\nhttp: \n\t- method: GET\n\t\tpath:\n\t\t\t- \"{{BaseURL}}\"\n\t\t\n\t\tmatchers:\n\t\t\t- type: status\n\t\t\tstatus:\n\t\t\t\t- 200\n```", + "type": "string" + }, + "network": { + "deprecated": true, "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "ssl requests to make", - "description": "SSL requests to make for the template" + "markdownDescription": "Network requests to make for the template", + "title": "network requests to make", + "type": "array" }, - "websocket": { + "requests": { + "deprecated": true, + "examples": [ + " # http get\n\t- method: GET\n\t\tpath:\n\t\t\t- \"{{BaseURL}}\"\n\t\t\n\t\tmatchers:\n\t\t\t- type: status\n\t\t\t\tstatus:\n\t\t\t\t\t- 200", + " # raw get\n\t- raw:\n\t\t\t- |\n\t\t\t\t\tGET HTTP/1.1\n\t\t\t\t\tHost: {{Hostname}}\n\t\t\t\t\n\t\t\tmatchers:\n\t\t\t- type: status\n\t\t\t\tstatus:\n\t\t\t\t\t- 200", + " # http self contained\n\t- method: GET\n\t\tpath:\n\t\t - \"https://api.stripe.com/v1/charges\"\n\t\t\n\t\tself-contained: true\n\t\theaders:\n\t\t\tAuthorization: 'Basic {{base64(token)}}'\n\t\t\n\t\tmatchers:\n\t\t - type: word\n\t\t part: body\n\t\t words:\n\t\t - '\"object\":'\n\t\t - '\"url\":'\n\t\t - '\"data\":'\n\t\t condition: and" + ], "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "websocket requests to make", - "description": "Websocket requests to make for the template" + "markdownDescription": "HTTP is the protocol block for sending HTTP requests\nIt is of array type and can contain multiple requests\nExample: \n```yaml\nrequests: \n\t- method: GET\n\t\tpath:\n\t\t\t- \"{{BaseURL}}\"\n\t\t\n\t\tmatchers:\n\t\t\t- type: status\n\t\t\tstatus:\n\t\t\t\t- 200\n```", + "title": "http requests to make", + "type": "array" }, - "whois": { + "self-contained": { + "default": false, + "examples": [ + true + ], + "markdownDescription": "Self-contained marks all requests in this template as independent of input which means input/target is not required for execution of template\nbut other variables defined in template need to be explicitly set using -V flag\nDefault value is false\nNote: self-contained templates only run once regardless of how many targets where provided to nuclei\nExample: \n```yaml\nself-contained: true\n```\nFull example template of self-contained is available at https://cloud.projectdiscovery.io/public/aws-app-enum", + "title": "mark requests as self-contained", + "type": "boolean" + }, + "signature": { + "deprecated": true, + "examples": [ + "aws" + ], + "markdownDescription": "Signature is the HTTP Request signature Method", + "title": "signature is the http request signature method", + "type": "string" + }, + "ssl": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "whois requests to make", - "description": "WHOIS requests to make for the template" + "markdownDescription": "SSL requests to make for the template", + "title": "ssl requests to make", + "type": "array" }, - "code": { + "stop-at-first-match": { + "default": false, + "examples": [ + true + ], + "markdownDescription": "stop-at-first-match stops the execution of template as soon as first match/result was found in a template given that template was sending multiple requests\nthis is required in case of default-login , brute-force and even detection templates where multiple requests are sent from template but we want to exit as soon as first match/result was found\nExample: \n```yaml\nstop-at-first-match: true\n```\nFull example template of stop-at-first-match is available at https://cloud.projectdiscovery.io/public/bitbucket-public-repository", + "title": "stop at first match", + "type": "boolean" + }, + "tcp": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "code snippets to make", - "description": "Code snippets" + "markdownDescription": "Network requests to make for the template", + "title": "network(tcp) requests to make", + "type": "array" }, - "javascript": { + "variables": { + "examples": [ + "\n\tdomain: \"{{FQDN}}\"\n\temail: pdteam@{{domain}}", + "\n\toast: \"{{interact-sh}}\"\n\tpayload: \"{{base64(oast)}}\"" + ], + "markdownDescription": "Variables are the global variables that once defined here can be used anywhere in the template\nVariables are evaluated in the order they are defined so one variable can be referenced in another variable\nVariables are evaluated before sending every requests so one can reference any variables that are available at runtime and they will be evaluate and used when referenced\nExample: \n```yaml\nvariables:\n oast: {{interact-sh}}\n payload: \"{{base64(oast)}}\"\n```\nFull example template of variables is available at https://cloud.projectdiscovery.io/public/screenshot\nNote: -\nThese variables can be overridden by -V flag at runtime if needed", + "title": "variables for the http request", + "type": "object" + }, + "websocket": { "items": { "$ref": "#/$defs/Request" }, - "type": "array", - "title": "javascript requests to make", - "description": "Javascript requests to make for the template" + "markdownDescription": "Websocket requests to make for the template", + "title": "websocket requests to make", + "type": "array" + }, + "whois": { + "items": { + "$ref": "#/$defs/Request" + }, + "markdownDescription": "WHOIS requests to make for the template", + "title": "whois requests to make", + "type": "array" }, "workflows": { "items": { "$ref": "#/$defs/WorkflowTemplate" }, - "type": "array", + "markdownDescription": "List of workflows to execute for template", "title": "list of workflows to execute", - "description": "List of workflows to execute for template" - }, - "self-contained": { - "type": "boolean", - "title": "mark requests as self-contained", - "description": "Mark Requests for the template as self-contained" - }, - "stop-at-first-match": { - "type": "boolean", - "title": "stop at first match", - "description": "Stop at first match for the template" - }, - "signature": { - "$ref": "#/$defs/SignatureTypeHolder", - "title": "signature is the http request signature method", - "description": "Signature is the HTTP Request signature Method" - }, - "variables": { - "$ref": "#/$defs/Variable", - "type": "object", - "title": "variables for the http request", - "description": "Variables contains any variables for the current request" - }, - "constants": { - "type": "object", - "title": "constant for the template", - "description": "constants contains any constant for the template" + "type": "array" } }, - "additionalProperties": false, - "type": "object", "required": [ "id", "info" - ] + ], + "type": "object" }, "Variable": { - "properties": {}, "additionalProperties": false, + "properties": {}, "type": "object" }, "WorkflowTemplate": { + "additionalProperties": false, "properties": { - "template": { - "type": "string", - "title": "template/directory to execute", - "description": "Template or directory to execute as part of workflow" - }, - "tags": { - "$ref": "#/$defs/StringOrSlice", - "title": "tags to execute", - "description": "Tags to run template based on" - }, "matchers": { "items": { "$ref": "#/$defs/Matcher" }, - "type": "array", + "markdownDescription": "Matchers perform name based matching to run subtemplates for a workflow", "title": "name based template result matchers", - "description": "Matchers perform name based matching to run subtemplates for a workflow" + "type": "array" }, "subtemplates": { "items": { "$ref": "#/$defs/WorkflowTemplate" }, - "type": "array", + "markdownDescription": "Subtemplates are ran if the template field Template matches", "title": "subtemplate based result matchers", - "description": "Subtemplates are ran if the template field Template matches" + "type": "array" + }, + "tags": { + "$ref": "#/$defs/StringOrSlice", + "markdownDescription": "Tags to run template based on", + "title": "tags to execute" + }, + "template": { + "markdownDescription": "Template or directory to execute as part of workflow", + "title": "template/directory to execute", + "type": "string" } }, - "additionalProperties": false, "type": "object" } - } -} + }, + "$id": "https://nuclei.projectdiscovery.io/template", + "$ref": "#/$defs/Template", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} \ No newline at end of file diff --git a/pkg/model/model.go b/pkg/model/model.go index d4aec3c071..43a96aa877 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -4,19 +4,9 @@ import ( "github.com/invopop/jsonschema" "github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity" "github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice" + "github.com/projectdiscovery/nuclei/v3/pkg/utils/schema" ) -type schemaMetadata struct { - PropName string - PropType string - Example []interface{} - OneOf []*schemaMetadata -} - -var infoSchemaMetadata = []schemaMetadata{ - {PropName: "author", OneOf: []*schemaMetadata{{PropType: "string", Example: []interface{}{`pdteam`}}, {PropType: "array", Example: []interface{}{`pdteam,mr.robot`}}}}, -} - // Info contains metadata information about a template type Info struct { // description: | @@ -96,25 +86,8 @@ type Info struct { // JSONSchemaProperty returns the JSON schema property for the Info object. func (i Info) JSONSchemaExtend(base *jsonschema.Schema) { - // since we are re-using a stringslice and rawStringSlice everywhere, we can extend/edit the schema here - // thus allowing us to add examples, descriptions, etc. to the properties - for _, metadata := range infoSchemaMetadata { - if prop, ok := base.Properties.Get(metadata.PropName); ok { - if len(metadata.OneOf) > 0 { - for _, oneOf := range metadata.OneOf { - prop.OneOf = append(prop.OneOf, &jsonschema.Schema{ - Type: oneOf.PropType, - Examples: oneOf.Example, - }) - } - } else { - if metadata.PropType != "" { - prop.Type = metadata.PropType - } - prop.Examples = []interface{}{metadata.Example} - } - } - } + base.Examples = infoSectionExamples + schema.ExtendSchema(infoSectionMetadata, base) } // Classification contains the vulnerability classification data for a template. diff --git a/pkg/model/model_schema.go b/pkg/model/model_schema.go new file mode 100644 index 0000000000..dea4f81de5 --- /dev/null +++ b/pkg/model/model_schema.go @@ -0,0 +1,184 @@ +package model + +import "github.com/projectdiscovery/nuclei/v3/pkg/utils/schema" + +// infoSectionMetadata contains all metadata information about Info struct +var infoSectionMetadata = []schema.PropertyMetadata{ + { + PropName: "name", + PropType: "string", + Description: schema.MultiLine( + "Name should be good short summary that identifies what the template does", + ), + Example: schema.PropertyExamples("example-template", "TeamCity - Authentication Bypass"), + }, + { + PropName: "author", + RemoveRef: true, + OneOf: []*schema.PropertyMetadata{ + { + PropType: "string", + Description: schema.MultiLine( + "Author of the template without any special characters", + "It can be a single author or comma-separated multiple authors", + ), + Example: schema.PropertyExample("pdteam"), + }, + { + PropType: "array", + Description: schema.MultiLine( + "Author of the template without any special characters", + "It can be a single author or comma-separated multiple authors", + ), + Example: schema.PropertyExample("pdteam,mr.robot"), + }, + }, + }, + { + PropName: "tags", + RemoveRef: true, + OneOf: []*schema.PropertyMetadata{ + { + PropType: "string", + Description: schema.MultiLine( + "Tags for the template separated by commas (No spaces)", + ), + Example: schema.PropertyExample("cve"), + }, + { + PropType: "array", + Description: schema.MultiLine( + "Multiple tags for the template separated by commas (No spaces)", + ), + Example: schema.PropertyExamples("cve,http", "http,oast", "cve2024,cve"), + }, + }, + }, + { + PropName: "description", + PropType: "string", + Description: schema.MultiLine( + "Description of the template.", + "You can go in-depth here on what the template actually does.", + "Example:", + "Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled.", + ), + Example: schema.PropertyExample("Example description of the template"), + }, + { + PropName: "impact", + PropType: "string", + Description: schema.MultiLine( + "Impact of the template.", + "You can go in-depth here on impact of the template.", + "Example:", + "Successful exploitation of this vulnerability can lead to remote code execution, potentially compromising the affected system.", + ), + Example: schema.PropertyExample("Impact of the template"), + }, + { + PropName: "reference", + PropType: "array", + Description: schema.MultiLine( + "References for the template.", + "This should contain links relevant to the template.", + "Example:", + "https://logging.apache.org/log4j/2.x/security.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-44228", + ), + Example: schema.PropertyExample("\n\t- https://example.com"), + }, + { + PropName: "severity", + Description: schema.MultiLine( + "Severity of the template.", + "supported values: info, low, medium, high, critical, unknown", + ), + Example: schema.PropertyExample("info"), + }, + { + PropName: "metadata", + Description: schema.MultiLine( + "Optional metadata for the template.", + "this could be any useful metadata like Vendor, Product, Version, etc.", + "Note - This is autogenerated and updated regularly for templates merged in projectdiscovery/nuclei-templates repository.", + "Example:", + "```yaml", + "Vendor: Apache", + "Product: Log4j2", + "Version: <=2.14.1", + "```", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + "\n\tmax-request: 1", + ), + schema.MultiLine( + "\n\tmax-request: 2", + "\tvendor: apache", + "\tproduct: log4j2", + ), + ), + }, + { + PropName: "classification", + Description: schema.MultiLine( + "Classification contains classification information about the template.", + "This could be any classification information like CWE, CVE, EPSS etc.", + "Note - This is autogenerated and updated regularly for CVE templates merged in projectdiscovery/nuclei-templates repository.", + "Example:", + "```yaml", + "cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "cvss-score: 10", + "cve-id: CVE-2021-44228", + "cwe-id: CWE-20,CWE-917", + "epss-score: 0.97453", + "epss-percentile: 0.99942", + "cpe: cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*", + "```", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + "\n\tcvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "\tcvss-score: 10", + "\tcve-id: CVE-2021-44228", + ), + schema.MultiLine( + "\n\tcwe-id: CWE-20,CWE-917", + ), + ), + }, + { + PropName: "Remediation", + PropType: "string", + Description: schema.MultiLine( + "Remediation steps for the template.", + "You can go in-depth here on how to mitigate the problem found by this template.", + "Example:", + "Upgrade to Log4j 2.3.1 (for Java 6), 2.12.3 (for Java 7), or 2.17.0 (for Java 8 and later).", + ), + Example: schema.PropertyExample("Remediation steps for the template"), + }, +} + +// infoSectionExamples contains examples for Info struct +var infoSectionExamples = schema.PropertyExamples( + schema.MultiLine( + "\n\tname: Example Template", + "\tauthor: pdteam", + "\tseverity: info", + "\tdescription: This is an example template", + ), + schema.MultiLine( + "\n\tname: example-template", + "\tauthor: pdteam", + "\tseverity: unknown", + "\tdescription: Example description of the template", + "\timpact: Impact of the template", + "\treference: ", + "\t - https://example.com", + "\tmetadata: ", + "\t max-request: 1", + "\ttags: http", + ), +) diff --git a/pkg/protocols/common/generators/attack_types.go b/pkg/protocols/common/generators/attack_types.go index c0ad882f81..33f30ff477 100644 --- a/pkg/protocols/common/generators/attack_types.go +++ b/pkg/protocols/common/generators/attack_types.go @@ -61,16 +61,17 @@ type AttackTypeHolder struct { Value AttackType `mapping:"true"` } -func (holder AttackTypeHolder) JSONSchemaType() *jsonschema.Schema { - gotType := &jsonschema.Schema{ - Type: "string", - Title: "type of the attack", - Description: "Type of the attack", +func (AttackTypeHolder) JSONSchema() *jsonschema.Schema { + enums := []interface{}{} + for _, severity := range GetSupportedAttackTypes() { + enums = append(enums, severity.String()) } - for _, types := range GetSupportedAttackTypes() { - gotType.Enum = append(gotType.Enum, types.String()) + return &jsonschema.Schema{ + Title: "attack is the payload combination", + Description: "Attack is the type of payload combinations to perform", + Type: "string", + Enum: enums, } - return gotType } func (holder *AttackTypeHolder) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/protocols/http/http.go b/pkg/protocols/http/http.go index 5ed5b63964..d03f074470 100644 --- a/pkg/protocols/http/http.go +++ b/pkg/protocols/http/http.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/invopop/jsonschema" json "github.com/json-iterator/go" "github.com/pkg/errors" @@ -17,6 +18,7 @@ import ( "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool" httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http" + "github.com/projectdiscovery/nuclei/v3/pkg/utils/schema" "github.com/projectdiscovery/rawhttp" "github.com/projectdiscovery/retryablehttp-go" fileutil "github.com/projectdiscovery/utils/file" @@ -58,10 +60,10 @@ type Request struct { // - "batteringram" // - "pitchfork" // - "clusterbomb" - AttackType generators.AttackTypeHolder `yaml:"attack,omitempty" json:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=batteringram,enum=pitchfork,enum=clusterbomb"` + AttackType generators.AttackTypeHolder `yaml:"attack,omitempty" json:"attack,omitempty"` // description: | // Method is the HTTP Request Method. - Method HTTPMethodTypeHolder `yaml:"method,omitempty" json:"method,omitempty" jsonschema:"title=method is the http request method,description=Method is the HTTP Request Method,enum=GET,enum=HEAD,enum=POST,enum=PUT,enum=DELETE,enum=CONNECT,enum=OPTIONS,enum=TRACE,enum=PATCH,enum=PURGE"` + Method HTTPMethodTypeHolder `yaml:"method,omitempty" json:"method,omitempty"` // description: | // Body is an optional parameter which contains HTTP Request body. // examples: @@ -74,14 +76,14 @@ type Request struct { // Payloads support both key-values combinations where a list // of payloads is provided, or optionally a single file can also // be provided as payload which will be read on run-time. - Payloads map[string]interface{} `yaml:"payloads,omitempty" json:"payloads,omitempty" jsonschema:"title=payloads for the http request,description=Payloads contains any payloads for the current request"` + Payloads map[string]interface{} `yaml:"payloads,omitempty" json:"payloads,omitempty" jsonschema:"type=object,title=payloads for the http request,description=Payloads contains any payloads for the current request"` // description: | // Headers contains HTTP Headers to send with the request. // examples: // - value: | // map[string]string{"Content-Type": "application/x-www-form-urlencoded", "Content-Length": "1", "Any-Header": "Any-Value"} - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" jsonschema:"title=headers to send with the http request,description=Headers contains HTTP Headers to send with the request"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" jsonschema:"type=object,title=headers to send with the http request,description=Headers contains HTTP Headers to send with the request"` // description: | // RaceCount is the number of times to send a request in Race Condition Attack. // examples: @@ -220,6 +222,12 @@ type Request struct { fuzzingFilterCondition matchers.ConditionType `yaml:"-" json:"-"` } +// JSONSchemaExtend extends json schema definations from struct +func (Request) JSONSchemaExtend(base *jsonschema.Schema) { + schema.ExtendSchema(httpRequestMetadata, base) + schema.ApplyAnyOfRequired(httpRequestAnyOfRequired, base) +} + // Options returns executer options for http request func (r *Request) Options() *protocols.ExecutorOptions { return r.options diff --git a/pkg/protocols/http/http_method_types.go b/pkg/protocols/http/http_method_types.go index 9ef78911b0..a0a8b7bed1 100644 --- a/pkg/protocols/http/http_method_types.go +++ b/pkg/protocols/http/http_method_types.go @@ -62,6 +62,14 @@ func GetSupportedHTTPMethodTypes() []HTTPMethodType { return result } +func GetHTTPMethods() []interface{} { + var result []interface{} + for _, method := range GetSupportedHTTPMethodTypes() { + result = append(result, method.String()) + } + return result +} + func toHTTPMethodTypes(valueToMap string) (HTTPMethodType, error) { normalizedValue := normalizeValue(valueToMap) for key, currentValue := range HTTPMethodMapping { @@ -89,16 +97,17 @@ func (holder HTTPMethodTypeHolder) String() string { return holder.MethodType.String() } -func (holder HTTPMethodTypeHolder) JSONSchemaType() *jsonschema.Schema { - gotType := &jsonschema.Schema{ - Type: "string", +func (HTTPMethodTypeHolder) JSONSchema() *jsonschema.Schema { + enums := []interface{}{} + for _, severity := range GetSupportedHTTPMethodTypes() { + enums = append(enums, severity.String()) + } + return &jsonschema.Schema{ Title: "method is the HTTP request method", Description: "Method is the HTTP Request Method", + Type: "string", + Enum: enums, } - for _, types := range GetSupportedHTTPMethodTypes() { - gotType.Enum = append(gotType.Enum, types.String()) - } - return gotType } func (holder *HTTPMethodTypeHolder) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/protocols/http/http_schema.go b/pkg/protocols/http/http_schema.go new file mode 100644 index 0000000000..3421089461 --- /dev/null +++ b/pkg/protocols/http/http_schema.go @@ -0,0 +1,508 @@ +package http + +import "github.com/projectdiscovery/nuclei/v3/pkg/utils/schema" + +// httpRequestMetadata / properties +var httpRequestMetadata = []schema.PropertyMetadata{ + { + PropName: "id", + PropType: "string", + Description: schema.MultiLine( + "Optional ID of this HTTP Request Block", + "This is useful when calling /executing this request block in flow", + "By default, current index in the http array is used as ID", + "Example: ", + "```yaml", + "- id: wp-login", + "```", + ), + Example: schema.PropertyExample("example-http-id"), + }, + { + PropName: "path", + Description: schema.MultiLine( + "Path contains HTTP URL[s] to be sent. Here only Path portion is generally specified and host is templatetized", + "Example: ", + "```yaml", + "- path:", + "\t"+`{{BaseURL}}/admin`, + "```", + ), + Example: schema.PropertyExamples( + "\n\t- '{{BaseURL}}'", + "\n\t- '{{RootURL}}'", + "\n\t- '{{Hostname}}:8080/ca-cert'", + ), + }, + { + PropName: "raw", + Description: schema.MultiLine( + "Raw contains HTTP Request[s] in raw or request dump format", + "Example: ", + schema.MultiLine( + "- raw:", + "\t- |", + "\t\tGET / HTTP/1.1", + "\t\tHost: {{Hostname}}", + "\n", + ), + "\nNote: If input contains any path/query-params they are automerged unless explicitly disabled using 'disable-path-automerge'", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + "\n# raw-get-request", + "\t- |", + "\t\tGET / HTTP/1.1", + "\t\tHost: {{Hostname}}", + "\n", + ), + schema.MultiLine( + "\n# raw-post-request", + "\t- |", + "\t\tPOST / HTTP/1.1", + "\t\tHost: {{Hostname}}", + "\t\tContent-Type: application/json", + "\t\tContent-Length: 13", + "\n", + "\t\t{\"key\": \"value\"}", + "\n", + ), + ), + }, + { + PropName: "name", + Description: schema.MultiLine( + "Name is the optional name of the request", + "If a name is specified, all the named request in a template can be matched upon", + "in a combined manner allowing multi-request based matchers.", + ), + Example: schema.PropertyExample("example-http-name"), + }, + { + PropName: "method", + Description: schema.MultiLine( + "Method is the HTTP method to be used for the request", + "Example: ", + "```yaml", + "- method: GET", + "```", + ), + Example: schema.PropertyExamples(GetHTTPMethods()...), + }, + { + PropName: "headers", + Description: schema.MultiLine( + "Headers contains the HTTP headers to be sent with the request", + "Example: ", + "```yaml", + "- headers:", + "\tUser-Agent: Mozilla/5.0", + "```", + ), + Example: schema.PropertyExamples( + "User-Agent: Mozilla/5.0", + "Authorization: Bearer {{token}}", + "X-Forwarded-For: {{Hostname}}", + "Origin: {{BaseURL}}", + "Referer: {{BaseURL}}/admin", + ), + }, + { + PropName: "body", + Description: schema.MultiLine( + "Body contains the HTTP body to be sent with the request", + "Example: ", + "```yaml", + "- body: \"key=value&key2=value2\"", + "```", + ), + Example: schema.PropertyExample("key=value&key2=value2"), + }, + { + PropName: "redirects", + Description: schema.MultiLine( + "Redirects specifies whether to follow redirects or not", + "This can be use in conjunction with max-redirects to effectively control the redirects", + "Example: ", + "```yaml", + "- redirects: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "host-redirects", + Description: schema.MultiLine( + "HostRedirects specifies whether only redirects to the same host should be followed by the HTTP Client", + "When set to true, only redirects to the same host are followed", + "Example: ", + "```yaml", + "- host-redirects: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "max-redirects", + Description: schema.MultiLine( + "MaxRedirects is the maximum number of redirects to follow", + "Example: ", + "```yaml", + "- max-redirects: 10", + "```", + ), + Example: schema.PropertyExample(10), + }, + { + PropName: "disable-path-automerge", + Description: schema.MultiLine( + "DisablePathAutomerge disables the automatic merging of path/query-params from input/target to path defined in template", + "Example: ", + "```yaml", + "- disable-path-automerge: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "unsafe", + Description: schema.MultiLine( + "Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests.", + "This uses the [rawhttp](https://github.com/projectdiscovery/rawhttp) engine to achieve complete", + "control over the request, with no normalization performed by the client.", + "Example: ", + "```yaml", + "- unsafe: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "skip-variables-check", + Description: schema.MultiLine( + "SkipVariablesCheck skips the check for unresolved variables in request", + "This is also useful in case where body contains payload in format of {{xyz}}", + "Example: ", + "```yaml", + "- skip-variables-check: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "max-size", + Description: schema.MultiLine( + "MaxSize is the maximum size of the response body to be read", + "Example: ", + "```yaml", + "- max-size: 1024", + "```", + ), + Example: schema.PropertyExample(1024), + }, + { + PropName: "read-all", + Description: schema.MultiLine( + "Enables force reading of the entire raw unsafe request body ignoring any specified content length headers", + "Example: ", + "```yaml", + "- read-all: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "self-contained", + Description: schema.MultiLine( + "SelfContained specifies whether the request is self-contained or not", + "This means if the request is dependent on input or is self-contained", + "Example: ", + "```yaml", + "- self-contained: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "cookie-reuse", + Description: schema.MultiLine( + "CookieReuse specifies whether to reuse the cookies from previous requests", + "Example: ", + "```yaml", + "- cookie-reuse: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "disable-cookie", + Description: schema.MultiLine( + "Disable Cookie is an optional setting to disable cookie reuse for this request", + "Example: ", + "```yaml", + "- disable-cookie: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "stop-at-first-match", + Description: schema.MultiLine( + "StopAtFirstMatch specifies whether to stop at first match or not", + "This is useful for cases like brute-forcing where we want to stop once we find the first correct login credential", + "Example: ", + "```yaml", + "- stop-at-first-match: true", + "```", + ), + }, + { + PropName: "digest-username", + Description: schema.MultiLine( + "DigestUsername specifies the username for digest authentication", + "Example: ", + "```yaml", + "- digest-username: admin", + "```", + ), + Example: schema.PropertyExample("admin"), + }, + { + PropName: "digest-password", + Description: schema.MultiLine( + "DigestPassword specifies the password for digest authentication", + "Example: ", + "```yaml", + "- digest-password: admin", + "```", + ), + Example: schema.PropertyExample("admin"), + }, + { + PropName: "filters", + Description: schema.MultiLine( + "Filter is only applicable for fuzzing requests and specifies whether this template should be run on this input or not", + "It is a twin of matchers and it can be used to write any pre-conditions that should be met before running the template", + "Example: ", + "```yaml", + "- filters:", + "\t- type: dsl", + "\t\tdsl:", + "\t\t\t- 'len(query) > 0'", + "```", + "With this filter only those requests will be executed where query param is present in input will be executed by this template", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + " # has query params", + "\t- type: dsl", + "\t\tdsl:", + "\t\t\t- 'len(query) > 0'", + "\n", + ), + schema.MultiLine( + " # has request body", + "\t- type: dsl", + "\t\tdsl:", + "\t\t\t- 'len(body) > 0'", + "\n", + ), + schema.MultiLine( + " # has post method", + "\t- type: dsl", + "\t\tdsl:", + "\t\t\t- 'method == \"POST\"'", + "\n", + ), + schema.MultiLine( + " # has post method and body", + "\t- type: dsl", + "\t\tdsl:", + "\t\t\t- 'method == \"POST\"'", + "\t\t\t- 'len(body) > 0'", + "\t\tcondition: and", + "\n", + ), + ), + }, + { + PropName: "filters-condition", + Description: schema.MultiLine( + "FiltersCondition specifies the condition to be used for combining multiple filters", + "By default, it is OR", + "Example: ", + "```yaml", + "- filters-condition: and", + "```", + ), + Example: schema.PropertyExample("and"), + Default: "OR", + }, + { + PropName: "payloads", + Description: schema.MultiLine( + "Payloads contains key-value pairs of payloads to be used", + "These payloads when referenced in http request will be iterated appropriately and replaced", + "Optionally payload also support loading values from file instead of defining them inline", + "Example: ", + "```yaml", + "- payloads:", + "\tusername:", + "\t\t- admin", + "\t\t- root", + "\tpassword:", + "\t\t- password", + "\t\t- admin", + "```", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + " # inline payloads", + "\tusername:", + "\t\t- admin", + "\t\t- root", + "\tpassword:", + "\t\t- password", + "\t\t- admin", + "\n", + ), + schema.MultiLine( + " # file payloads", + "\tusername: /path/to/usernames.txt", + "\tpassword: /path/to/passwords.txt", + "\n", + ), + ), + }, + { + PropName: "attack", + Description: schema.MultiLine( + "Attack is the type of payload combinations to perform", + "batteringram the same payload into all defined payload positions at once, pitchfork combines multiple payload sets and clusterbomb generates permutations and combinations for all payloads", + "Example: ", + "```yaml", + "- attack: clusterbomb", + "```", + "Note: this is only applicable when 'payloads' are defined", + ), + Example: schema.PropertyExamples("batteringram", "pitchfork", "clusterbomb"), + }, + { + PropName: "threads", + Description: schema.MultiLine( + "Threads specifies concurreny with which requests should be sent", + "It should not be specified unless explicitly behaviour and its value is obtained from -pc (-payload-concurrency) flag", + "Example: ", + "```yaml", + "- threads: 10", + "```", + ), + Example: schema.PropertyExample(10), + }, + { + PropName: "race", + Description: schema.MultiLine( + "Race determines if all the request have to be attempted at the same time (Race Condition)", + "The actual number of requests that will be sent is determined by the `race_count` field.", + "Example: ", + "```yaml", + "- race: true", + " race_count: 100", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "race_count", + Description: schema.MultiLine( + "RaceCount specifies the number of requests to be sent when attempting race condition attacks", + "Example: ", + "```yaml", + "- race: true", + " race_count: 100", + "```", + ), + Example: schema.PropertyExample(100), + }, + { + PropName: "pipeline", + Description: schema.MultiLine( + "Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining", + "All requests must be idempotent (GET/POST). This can be used for race conditions/billions requests.", + "Example: ", + "```yaml", + "- pipeline: true", + "```", + ), + Example: schema.PropertyExample(true), + }, + { + PropName: "pipeline-concurrent-connections", + Description: schema.MultiLine( + "PipelineConcurrentConnections specifies the number of concurrent connections to be used for pipelining", + "Example: ", + "```yaml", + "- pipeline-concurrent-connections: 10", + " pipeline: true", + "```", + ), + Example: schema.PropertyExample(10), + }, + { + PropName: "pipeline-requests-per-connection", + Description: schema.MultiLine( + "PipelineRequestsPerConnection specifies the number of requests to be sent per connection", + "Example: ", + "```yaml", + "- pipeline: true", + " pipeline-requests-per-connection: 10", + "```", + ), + Example: schema.PropertyExample(10), + }, + { + PropName: "fuzzing", + Description: schema.MultiLine( + "Fuzzing or DAST specifies rules for fuzzing requests", + "these rules mutate the input request to generate multiple requests", + "For example, replacing all query param values with sqli payloads", + ), + }, + { + PropName: "req-condition", + Description: schema.MultiLine( + "Deprecated: this is automatically identified and used (https://github.com/projectdiscovery/nuclei/issues/2393)", + ), + Deprecated: true, + }, + { + PropName: "iterate-all", + Description: schema.MultiLine( + "Deprecated: use flow to properly specify iteration logic", + ), + Deprecated: true, + }, + { + PropName: "cookie-reuse", + Description: schema.MultiLine( + "Deprecated: this is default behaviour now use disable-cookie to explicitly disable cookie reuse", + ), + Deprecated: true, + }, + { + PropName: "signature", + Deprecated: true, + }, +} + +// httpRequestAnyOf required combinations +var httpRequestAnyOfRequired = []schema.RequiredCombos{ + schema.Require("path"), + schema.Require("raw"), + schema.Require("filters", "payload", "fuzzing"), + schema.RequireBase( + []string{"payloads"}, + schema.Require("path"), + schema.Require("raw"), + ), +} diff --git a/pkg/protocols/http/signature.go b/pkg/protocols/http/signature.go index c28ecf6827..edd3958cca 100644 --- a/pkg/protocols/http/signature.go +++ b/pkg/protocols/http/signature.go @@ -51,16 +51,17 @@ type SignatureTypeHolder struct { Value SignatureType } -func (holder SignatureTypeHolder) JSONSchemaType() *jsonschema.Schema { - gotType := &jsonschema.Schema{ +func (signature SignatureTypeHolder) JSONSchema() *jsonschema.Schema { + enums := []interface{}{} + for _, severity := range GetSupportedSignaturesTypes() { + enums = append(enums, severity.String()) + } + return &jsonschema.Schema{ Type: "string", Title: "type of the signature", Description: "Type of the signature", + Enum: enums, } - for _, types := range GetSupportedSignaturesTypes() { - gotType.Enum = append(gotType.Enum, types.String()) - } - return gotType } func (holder *SignatureTypeHolder) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go index 68f9324a87..8659157f3c 100644 --- a/pkg/templates/templates.go +++ b/pkg/templates/templates.go @@ -9,6 +9,7 @@ import ( "strings" validate "github.com/go-playground/validator/v10" + "github.com/invopop/jsonschema" "github.com/projectdiscovery/nuclei/v3/pkg/model" "github.com/projectdiscovery/nuclei/v3/pkg/protocols" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/code" @@ -23,6 +24,7 @@ import ( "github.com/projectdiscovery/nuclei/v3/pkg/protocols/websocket" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/whois" "github.com/projectdiscovery/nuclei/v3/pkg/templates/types" + "github.com/projectdiscovery/nuclei/v3/pkg/utils/schema" "github.com/projectdiscovery/nuclei/v3/pkg/workflows" errorutil "github.com/projectdiscovery/utils/errors" fileutil "github.com/projectdiscovery/utils/file" @@ -45,7 +47,7 @@ type Template struct { // examples: // - name: ID Example // value: "\"CVE-2021-19520\"" - ID string `yaml:"id" json:"id" jsonschema:"title=id of the template,description=The Unique ID for the template,required,example=cve-2021-19520,pattern=^([a-zA-Z0-9]+[-_])*[a-zA-Z0-9]+$"` + ID string `yaml:"id" json:"id" jsonschema:"title=id of the template,pattern=^([a-zA-Z0-9]+[-_])*[a-zA-Z0-9]+$,required"` // description: | // Info contains metadata information about the template. // examples: @@ -62,7 +64,7 @@ type Template struct { // http(1) // } // - Flow string `yaml:"flow,omitempty" json:"flow,omitempty" jsonschema:"title=template execution flow in js,description=Flow contains js code which defines how the template should be executed,type=string,example='flow: http(0) && http(1)'"` + Flow string `yaml:"flow,omitempty" json:"flow,omitempty" jsonschema:"title=template execution flow in js,description=Flow contains js code which defines how the template should be executed,type=string"` // description: | // Requests contains the http request to make in the template. // WARNING: 'requests' will be deprecated and will be removed in a future release. Please use 'http' instead. @@ -161,6 +163,11 @@ type Template struct { ImportedFiles []string `yaml:"-" json:"-"` } +func (template Template) JSONSchemaExtend(base *jsonschema.Schema) { + schema.ExtendSchema(templateMetadata, base) + schema.ApplyAnyOfRequired(templateAnyOfRequired, base) +} + // Type returns the type of the template func (template *Template) Type() types.ProtocolType { switch { diff --git a/pkg/templates/templates_schema.go b/pkg/templates/templates_schema.go new file mode 100644 index 0000000000..fb26414526 --- /dev/null +++ b/pkg/templates/templates_schema.go @@ -0,0 +1,303 @@ +package templates + +import "github.com/projectdiscovery/nuclei/v3/pkg/utils/schema" + +// contains metadata information about template +var templateMetadata = []schema.PropertyMetadata{ + { + PropName: "id", + PropType: "string", + Description: schema.MultiLine( + "ID is the unique id for the template.", + " ### Good IDs ", + "A good ID uniquely identifies what the requests in the template", + "are doing. Let's say you have a template that identifies a git-config", + "file on the webservers, a good name would be `git-config-exposure`. Another", + "example name is `azure-apps-nxdomain-takeover`", + ), + Example: schema.PropertyExamples("example-id", "git-config-exposure", "azure-apps-nxdomain-takeover", "cve-2021-19520"), + }, + { + PropName: "flow", + PropType: "string", + Description: schema.MultiLine( + "Flow describes how multiple request-blocks/protocols should be combined together and executed", + "It is a javascript code where each protocol is a function and all request-blocks are indexed by their corresponding index in request array", + "Example: ", + " flow: http(1) && http(2)", + " means that second http request will be executed only if first http request is successful (aka matched)", + ), + Example: schema.PropertyExamples( + "http(1) && http(2)", + schema.MultiLine( + " | # example-vhost-enum", + "\tssl(); // -> execute all ssl protocol requests", + "\tdns(); // -> execute all dns protocol requests", + "\tfor (let got of template.domains) { // -> iterate over 'domains' array variable", + "\t\tset('vhost', got); // -> set 'vhost' variable to current domain", + "\t\thttp(); // -> execute all http protocol requests", + "\t}\n", + ), + ), + }, + { + PropName: "requests", + Deprecated: true, + }, + { + PropName: "network", + Deprecated: true, + }, + { + PropName: "signature", + RemoveRef: true, + PropType: "string", + Deprecated: true, + Example: schema.PropertyExample("aws"), + }, + { + PropName: "self-contained", + Description: schema.MultiLine( + "Self-contained marks all requests in this template as independent of input which means input/target is not required for execution of template", + "but other variables defined in template need to be explicitly set using -V flag", + "Default value is false", + "Note: self-contained templates only run once regardless of how many targets where provided to nuclei", + "Example: ", + "```yaml", + "self-contained: true", + "```", + "Full example template of self-contained is available at https://cloud.projectdiscovery.io/public/aws-app-enum", + ), + Default: false, + Example: schema.PropertyExample(true), + }, + { + PropName: "info", + Description: schema.MultiLine( + "Info contains the required metadata information about the template", + "It is meant to provide basic but necessary info like name, author , severity", + "along with many other optional fields like metadata, classification etc", + "Example: ", + "```yaml", + "info: ", + "\tname: Example Template", + "\tauthor: pdteam", + "\tseverity: info", + "\tdescription: This is an example template", + "```", + "\nNote: - ", + "For a template to be valid name,author,severity of `info` section must be set", + ), + }, + { + PropName: "stop-at-first-match", + Description: schema.MultiLine( + "stop-at-first-match stops the execution of template as soon as first match/result was found in a template given that template was sending multiple requests", + "this is required in case of default-login , brute-force and even detection templates where multiple requests are sent from template but we want to exit as soon as first match/result was found", + "Example: ", + "```yaml", + "stop-at-first-match: true", + "```", + "Full example template of stop-at-first-match is available at https://cloud.projectdiscovery.io/public/bitbucket-public-repository", + ), + Default: false, + Example: schema.PropertyExample(true), + }, + { + PropName: "variables", + PropType: "object", + RemoveRef: true, + Description: schema.MultiLine( + "Variables are the global variables that once defined here can be used anywhere in the template", + "Variables are evaluated in the order they are defined so one variable can be referenced in another variable", + "Variables are evaluated before sending every requests so one can reference any variables that are available at runtime and they will be evaluate and used when referenced", + "Example: ", + "```yaml", + "variables:", + ` oast: {{interact-sh}}`, + ` payload: "{{base64(oast)}}"`, + "```", + "Full example template of variables is available at https://cloud.projectdiscovery.io/public/screenshot", + "Note: -", + "These variables can be overridden by -V flag at runtime if needed", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + "\n\tdomain: \"{{FQDN}}\"", + "\temail: pdteam@{{domain}}", + ), + schema.MultiLine( + "\n\toast: \"{{interact-sh}}\"", + "\tpayload: \"{{base64(oast)}}\"", + ), + ), + }, + { + PropName: "constants", + PropType: "object", + RemoveRef: true, + Description: schema.MultiLine( + "Constants are the global constants that once defined here can be used anywhere in the template", + "It can be used in same way as variables but only difference is that constants cannot be overridden by -V flag at runtime", + "Example: ", + "```yaml", + "constants:", + ` exploit: 'x0x0x0x0x0x0x'`, + "```", + ), + Example: schema.PropertyExample( + schema.MultiLine( + "\n\texploit: 'x0x0x0x0x0x0x'", + ), + ), + }, + { + PropName: "requests", + Deprecated: true, + Description: schema.MultiLine( + "HTTP is the protocol block for sending HTTP requests", + "It is of array type and can contain multiple requests", + "Example: ", + "```yaml", + "requests: ", + "\t- method: GET", + "\t\tpath:", + "\t\t\t- \"{{BaseURL}}\"", + "\t\t", + "\t\tmatchers:", + "\t\t\t- type: status", + "\t\t\tstatus:", + "\t\t\t\t- 200", + "```", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + " # http get", + "\t- method: GET", + "\t\tpath:", + "\t\t\t- \"{{BaseURL}}\"", + "\t\t", + "\t\tmatchers:", + "\t\t\t- type: status", + "\t\t\t\tstatus:", + "\t\t\t\t\t- 200", + ), + schema.MultiLine( + " # raw get", + "\t- raw:", + "\t\t\t- |", + "\t\t\t\t\tGET HTTP/1.1", + "\t\t\t\t\tHost: {{Hostname}}", + "\t\t\t\t", + "\t\t\tmatchers:", + "\t\t\t- type: status", + "\t\t\t\tstatus:", + "\t\t\t\t\t- 200", + ), + schema.MultiLine( + " # http self contained", + "\t- method: GET", + "\t\tpath:", + "\t\t - \"https://api.stripe.com/v1/charges\"", + "\t\t", + "\t\tself-contained: true", + "\t\theaders:", + "\t\t\t"+`Authorization: 'Basic {{base64(token)}}'`, + "\t\t", + "\t\tmatchers:", + "\t\t - type: word", + "\t\t part: body", + "\t\t words:", + "\t\t - '\"object\":'", + "\t\t - '\"url\":'", + "\t\t - '\"data\":'", + "\t\t condition: and", + ), + ), + }, + { + PropName: "http", + Description: schema.MultiLine( + "HTTP is the protocol block for sending HTTP requests", + "It is of array type and can contain multiple requests", + "Example: ", + "```yaml", + "http: ", + "\t- method: GET", + "\t\tpath:", + "\t\t\t- \"{{BaseURL}}\"", + "\t\t", + "\t\tmatchers:", + "\t\t\t- type: status", + "\t\t\tstatus:", + "\t\t\t\t- 200", + "```", + ), + Example: schema.PropertyExamples( + schema.MultiLine( + " # http get", + "\t- method: GET", + "\t\tpath:", + "\t\t\t- \"{{BaseURL}}\"", + "\t\t", + "\t\tmatchers:", + "\t\t\t- type: status", + "\t\t\t\tstatus:", + "\t\t\t\t\t- 200", + ), + schema.MultiLine( + " # raw get", + "\t- raw:", + "\t\t\t- |", + "\t\t\t\t\tGET HTTP/1.1", + "\t\t\t\t\tHost: {{Hostname}}", + "\t\t\t\t", + "\t\t\tmatchers:", + "\t\t\t- type: status", + "\t\t\t\tstatus:", + "\t\t\t\t\t- 200", + ), + schema.MultiLine( + " # http self contained", + "\t- method: GET", + "\t\tpath:", + "\t\t - \"https://api.stripe.com/v1/charges\"", + "\t\t", + "\t\tself-contained: true", + "\t\theaders:", + "\t\t\t"+`Authorization: 'Basic {{base64(token)}}'`, + "\t\t", + "\t\tmatchers:", + "\t\t - type: word", + "\t\t part: body", + "\t\t words:", + "\t\t - '\"object\":'", + "\t\t - '\"url\":'", + "\t\t - '\"data\":'", + "\t\t condition: and", + ), + ), + }, +} + +// valid template should contain at least on below combinations +// requireBase uses two arguments to generate all combinations in format of +// base_[0] +// base_[1] etc +var templateAnyOfRequired = []schema.RequiredCombos{ + schema.RequireBase([]string{"id", "info"}, + schema.Require("http"), + schema.Require("dns"), + schema.Require("file"), + schema.Require("tcp"), + schema.Require("headless"), + schema.Require("ssl"), + schema.Require("websocket"), + schema.Require("whois"), + schema.Require("code"), + schema.Require("javascript"), + schema.Require("requests"), + schema.Require("network"), + ), + schema.Require("workflows"), +} diff --git a/pkg/utils/schema/helpers.go b/pkg/utils/schema/helpers.go new file mode 100644 index 0000000000..bd6bca7613 --- /dev/null +++ b/pkg/utils/schema/helpers.go @@ -0,0 +1,150 @@ +// schema implements helper types & functions for generating better json schema +package schema + +import ( + "strings" + + "github.com/invopop/jsonschema" + sliceutil "github.com/projectdiscovery/utils/slice" +) + +// PropertyMetadata is a metadata for a property in a schema / struct +type PropertyMetadata struct { + PropName string + Description string + PropType string + Example []interface{} + Default any + OneOf []*PropertyMetadata + RemoveRef bool + Deprecated bool +} + +// PropertyExamples returns a list of examples for a property +func PropertyExamples(values ...any) []interface{} { + examples := []interface{}{} + for _, value := range values { + if value != nil { + examples = append(examples, value) + } + } + return examples +} + +// PropertyExample returns a list of examples for a property +func PropertyExample(values any) []interface{} { + return []interface{}{values} +} + +// TrimmedString trims the string and returns it +func TrimmedString(value string) string { + return strings.TrimSpace(value) +} + +func MultiLine(values ...string) string { + return strings.Join(values, "\n") +} + +// ExtendSchema extends the schema with the metadata +// This could be patching or adding additional information to the schema +func ExtendSchema(metadata []PropertyMetadata, base *jsonschema.Schema) { + for _, meta := range metadata { + if prop, ok := base.Properties.Get(meta.PropName); ok { + // if it has oneof, we need to add it + if len(meta.OneOf) > 0 { + for _, oneOf := range meta.OneOf { + prop.OneOf = append(prop.OneOf, &jsonschema.Schema{ + Type: oneOf.PropType, + Description: oneOf.Description, + Examples: oneOf.Example, + Default: oneOf.Default, + }) + } + } else { + if meta.PropType != "" { + prop.Type = meta.PropType + } + if meta.Description != "" { + prop.Description = meta.Description + } + if len(meta.Example) > 0 { + prop.Examples = append(prop.Examples, meta.Example...) + prop.Examples = sliceutil.Dedupe(prop.Examples) + } + if meta.Default != nil { + prop.Default = meta.Default + } + + } + prop.Examples = purgeNil(prop.Examples) + if meta.RemoveRef { + prop.Ref = "" + } + prop.Deprecated = meta.Deprecated + // add new property called markdownDescription + if prop.Description != "" { + base.Properties.Set("markdownDescription", &jsonschema.Schema{ + Type: "string", + Description: prop.Description, + }) + } + } + } +} + +// RequiredCombos is a list of required field combinations +// and at least on of it is inforced if none is satisfied +type RequiredCombos struct { + RequireBase []string + Require []string + required []RequiredCombos +} + +func RequireBase(base []string, requires ...RequiredCombos) RequiredCombos { + x := RequiredCombos{RequireBase: base} + x.required = requires + return x +} + +func Require(require ...string) RequiredCombos { + return RequiredCombos{Require: require} +} + +// ApplyAnyOfRequired applies any of required field combinations +func ApplyAnyOfRequired(meta []RequiredCombos, base *jsonschema.Schema) { + if len(meta) == 0 { + return + } + for _, anyOf := range meta { + if len(anyOf.Require) == 0 && len(anyOf.RequireBase) == 0 { + continue + } + if len(anyOf.RequireBase) > 0 && len(anyOf.required) > 0 { + // iterate over all required combinations present + // in required base and add them to the anyof + for _, r := range anyOf.required { + required := sliceutil.Clone(anyOf.RequireBase) + required = append(required, r.Require...) + base.AnyOf = append(base.AnyOf, &jsonschema.Schema{ + Required: required, + }) + } + } + if len(anyOf.Require) > 0 { + base.AnyOf = append(base.AnyOf, &jsonschema.Schema{ + Required: anyOf.Require, + }) + + } + } +} + +func purgeNil(s []any) []any { + var r []any + for _, i := range s { + if i != nil { + r = append(r, i) + } + } + return r +}