diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index de2a43c9..9c748525 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -6,7 +6,7 @@ on: jobs: - build: + functional: name: Functional Test runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index d91c70de..e65180cd 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -28,7 +28,7 @@ jobs: fi go generate pkg/templates/templates.go go build -o "cmd/docgen/docgen" cmd/docgen/docgen.go - ./cmd/docgen/docgen syntax-reference.md + ./cmd/docgen/docgen syntax-reference.md nuclei-jsonschema.json echo "::set-output name=changes::$(git status -s | wc -l)" working-directory: v2 @@ -37,7 +37,7 @@ jobs: run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git add v2/syntax-reference.md + git add v2/syntax-reference.md v2/nuclei-jsonschema.json git commit -m "Auto Generate Syntax Docs [$(date)] :robot:" -a cat v2/syntax-reference.md diff --git a/v2/Makefile b/v2/Makefile index 0682b184..396bd0d0 100644 --- a/v2/Makefile +++ b/v2/Makefile @@ -15,7 +15,7 @@ docs: fi $(GOCMD) generate pkg/templates/templates.go $(GOBUILD) -o "cmd/docgen/docgen" cmd/docgen/docgen.go - ./cmd/docgen/docgen docs.md + ./cmd/docgen/docgen docs.md nuclei-jsonschema.json test: $(GOTEST) -v ./... tidy: diff --git a/v2/cmd/docgen/docgen.go b/v2/cmd/docgen/docgen.go index 907fc3da..5cd359c9 100644 --- a/v2/cmd/docgen/docgen.go +++ b/v2/cmd/docgen/docgen.go @@ -1,14 +1,22 @@ package main import ( + "bytes" + "encoding/json" "io/ioutil" "log" "os" + "regexp" + "strings" + "github.com/alecthomas/jsonschema" "github.com/projectdiscovery/nuclei/v2/pkg/templates" ) +var pathRegex = regexp.MustCompile(`github.com/projectdiscovery/nuclei/v2/(?:internal|pkg)/(?:.*/)?([A-Za-z\.]+)`) + func main() { + // Generate yaml syntax documentation data, err := templates.GetTemplateDoc().Encode() if err != nil { log.Fatalf("Could not encode docs: %s\n", err) @@ -17,4 +25,26 @@ func main() { if err != nil { log.Fatalf("Could not write docs: %s\n", err) } + + // Generate jsonschema + r := &jsonschema.Reflector{ + PreferYAMLSchema: true, + YAMLEmbeddedStructs: true, + FullyQualifyTypeNames: true, + } + jsonschemaData := r.Reflect(&templates.Template{}) + + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + encoder.SetIndent("", " ") + _ = encoder.Encode(jsonschemaData) + + schema := buf.String() + for _, match := range pathRegex.FindAllStringSubmatch(schema, -1) { + schema = strings.ReplaceAll(schema, match[0], match[1]) + } + err = ioutil.WriteFile(os.Args[2], []byte(schema), 0777) + if err != nil { + log.Fatalf("Could not write jsonschema: %s\n", err) + } } diff --git a/v2/go.mod b/v2/go.mod index 6a1afbb9..f77b5cbc 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/Knetic/govaluate v3.0.0+incompatible + github.com/alecthomas/jsonschema v0.0.0-20210818095345-1014919a589c github.com/andygrunwald/go-jira v1.14.0 github.com/antchfx/htmlquery v1.2.3 github.com/apex/log v1.9.0 @@ -33,9 +34,9 @@ require ( github.com/projectdiscovery/interactsh v0.0.4 github.com/projectdiscovery/rawhttp v0.0.7 github.com/projectdiscovery/retryabledns v1.0.12 + github.com/projectdiscovery/retryablehttp-go v1.0.2-0.20210524224054-9fbe1f2b0727 github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe github.com/projectdiscovery/yamldoc-go v1.0.2 - github.com/projectdiscovery/retryablehttp-go v1.0.2-0.20210524224054-9fbe1f2b0727 github.com/remeh/sizedwaitgroup v1.0.0 github.com/rs/xid v1.3.0 github.com/segmentio/ksuid v1.0.4 diff --git a/v2/go.sum b/v2/go.sum index b918b889..85c10376 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -49,6 +49,8 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/akrylysov/pogreb v0.10.0 h1:pVKi+uf3EzZUmiwr9bZnPk4W379KP8QsFzAa9IUuOog= github.com/akrylysov/pogreb v0.10.0/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= +github.com/alecthomas/jsonschema v0.0.0-20210818095345-1014919a589c h1:oJsq4z4xKgZWWOhrSZuLZ5KyYfRFytddLL1E5+psfIY= +github.com/alecthomas/jsonschema v0.0.0-20210818095345-1014919a589c/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/andygrunwald/go-jira v1.14.0 h1:7GT/3qhar2dGJ0kq8w0d63liNyHOnxZsUZ9Pe4+AKBI= @@ -224,6 +226,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hooklift/assert v0.1.0 h1:UZzFxx5dSb9aBtvMHTtnPuvFnBvcEhHTPb9+0+jpEjs= github.com/hooklift/assert v0.1.0/go.mod h1:pfexfvIHnKCdjh6CkkIZv5ic6dQ6aU2jhKghBlXuwwY= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= +github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA= @@ -389,6 +393,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/v2/internal/severity/severity.go b/v2/internal/severity/severity.go index c889356a..784dfa41 100644 --- a/v2/internal/severity/severity.go +++ b/v2/internal/severity/severity.go @@ -4,6 +4,7 @@ import ( "encoding/json" "strings" + "github.com/alecthomas/jsonschema" "github.com/pkg/errors" ) @@ -57,6 +58,18 @@ type SeverityHolder struct { Severity Severity } +func (severityHolder SeverityHolder) JSONSchemaType() *jsonschema.Type { + gotType := &jsonschema.Type{ + Type: "string", + Title: "severity of the template", + Description: "Seriousness of the implications of the template", + } + for _, severity := range GetSupportedSeverities() { + gotType.Enum = append(gotType.Enum, severity.String()) + } + return gotType +} + func (severityHolder *SeverityHolder) UnmarshalYAML(unmarshal func(interface{}) error) error { var marshalledSeverity string if err := unmarshal(&marshalledSeverity); err != nil { diff --git a/v2/pkg/model/model.go b/v2/pkg/model/model.go index e1bf3c43..54a99f54 100644 --- a/v2/pkg/model/model.go +++ b/v2/pkg/model/model.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/alecthomas/jsonschema" "github.com/projectdiscovery/nuclei/v2/internal/severity" "github.com/projectdiscovery/nuclei/v2/pkg/utils" ) @@ -17,13 +18,13 @@ type Info struct { // examples: // - value: "\"bower.json file disclosure\"" // - value: "\"Nagios Default Credentials Check\"" - Name string `json:"name,omitempty" yaml:"name,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty" jsonschema:"title=name of the template,description=Name is a short summary of what the template does,example=Nagios Default Credentials Check"` // description: | // Author of the template. // // examples: // - value: "\"\"" - Authors StringSlice `json:"author,omitempty" yaml:"author,omitempty"` + Authors StringSlice `json:"author,omitempty" yaml:"author,omitempty" jsonschema:"title=author of the template,description=Author is the author of the template,example=username"` // description: | // Any tags for the template. // @@ -32,7 +33,7 @@ type Info struct { // examples: // - name: Example tags // value: "\"cve,cve2019,grafana,auth-bypass,dos\"" - Tags StringSlice `json:"tags,omitempty" yaml:"tags,omitempty"` + Tags StringSlice `json:"tags,omitempty" yaml:"tags,omitempty" jsonschema:"title=tags of the template,description=Any tags for the template"` // description: | // Description of the template. // @@ -41,7 +42,7 @@ type Info struct { // examples: // - value: "\"Bower is a package manager which stores packages informations in bower.json file\"" // - value: "\"Subversion ALM for the enterprise before 8.8.2 allows reflected XSS at multiple locations\"" - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty" jsonschema:"title=description of the template,description=In-depth explanation on what the template does,example=Bower is a package manager which stores packages informations in bower.json file"` // description: | // References for the template. // @@ -50,7 +51,7 @@ type Info struct { // examples: // - value: > // []string{"https://github.com/strapi/strapi", "https://github.com/getgrav/grav"} - Reference StringSlice `json:"reference,omitempty" yaml:"reference,omitempty"` + Reference StringSlice `json:"reference,omitempty" yaml:"reference,omitempty" jsonschema:"title=references for the template,description=Links relevant to the template"` // description: | // Severity of the template. // @@ -67,7 +68,7 @@ type Info struct { // examples: // - value: > // map[string]string{"customField1":"customValue1"} - AdditionalFields map[string]string `json:"additional-fields,omitempty" yaml:"additional-fields,omitempty"` + AdditionalFields map[string]string `json:"additional-fields,omitempty" yaml:"additional-fields,omitempty" jsonschema:"title=additional metadata for the template,description=Additional metadata fields for the template"` } // StringSlice represents a single (in-lined) or multiple string value(s). @@ -76,6 +77,13 @@ type StringSlice struct { Value interface{} } +func (stringSlice StringSlice) JSONSchemaType() *jsonschema.Type { + gotType := &jsonschema.Type{ + OneOf: []*jsonschema.Type{{Type: "string"}, {Type: "array"}}, + } + return gotType +} + func (stringSlice *StringSlice) IsEmpty() bool { return len(stringSlice.ToSlice()) == 0 } diff --git a/v2/pkg/operators/extractors/extractors.go b/v2/pkg/operators/extractors/extractors.go index c9d6ab9e..4ca72679 100644 --- a/v2/pkg/operators/extractors/extractors.go +++ b/v2/pkg/operators/extractors/extractors.go @@ -13,13 +13,15 @@ type Extractor struct { // spaces or dashes (-). // examples: // - value: "\"cookie-extractor\"" - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=name of the extractor,description=Name of the extractor"` // description: | // Type is the type of the extractor. // values: // - "regex" // - "kval" - Type string `yaml:"type"` + // - "json" + // - "xpath" + Type string `yaml:"type" jsonschema:"title=type of the extractor,description=Type of the extractor,enum=regex,enum=kval,enum=json,enum=xpath"` // extractorType is the internal type of the extractor extractorType ExtractorType @@ -35,13 +37,13 @@ type Extractor struct { // - name: Wordpress Author Extraction regex // value: > // []string{"Author:(?:[A-Za-z0-9 -\\_=\"]+)?([A-Za-z0-9]+)<\\/span>"} - Regex []string `yaml:"regex,omitempty"` + Regex []string `yaml:"regex,omitempty" jsonschema:"title=regex to extract from part,description=Regex to extract from part"` // description: | // Group specifies a numbered group to extract from the regex. // examples: // - name: Example Regex Group // value: "1" - RegexGroup int `yaml:"group,omitempty"` + RegexGroup int `yaml:"group,omitempty" jsonschema:"title=group to extract from regex,description=Group to extract from regex"` // regexCompiled is the compiled variant regexCompiled []*regexp.Regexp @@ -58,7 +60,7 @@ type Extractor struct { // - name: Extracting value of PHPSESSID Cookie // value: > // []string{"PHPSESSID"} - KVal []string `yaml:"kval,omitempty"` + KVal []string `yaml:"kval,omitempty" jsonschema:"title=kval pairs to extract from response,description=Kval pairs to extract from response"` // description: | // JSON allows using jq-style syntax to extract items from json response @@ -68,7 +70,7 @@ type Extractor struct { // []string{".[] | .id"} // - value: > // []string{".batters | .batter | .[] | .id"} - JSON []string `yaml:"json,omitempty"` + JSON []string `yaml:"json,omitempty" jsonschema:"title=json jq expressions to extract data,description=JSON JQ expressions to evaluate from response part"` // description: | // XPath allows using xpath expressions to extract items from html response // @@ -77,13 +79,13 @@ type Extractor struct { // []string{"/html/body/div/p[2]/a"} // - value: > // []string{".batters | .batter | .[] | .id"} - XPath []string `yaml:"xpath,omitempty"` + XPath []string `yaml:"xpath,omitempty" jsonschema:"title=html xpath expressions to extract data,description=XPath allows using xpath expressions to extract items from html response"` // description: | // Attribute is an optional attribute to extract from response XPath. // // examples: // - value: "\"href\"" - Attribute string `yaml:"attribute,omitempty"` + Attribute string `yaml:"attribute,omitempty" jsonschema:"title=optional attribute to extract from xpath,description=Optional attribute to extract from response XPath"` // jsonCompiled is the compiled variant jsonCompiled []*gojq.Code @@ -96,11 +98,11 @@ type Extractor struct { // examples: // - value: "\"body\"" // - value: "\"raw\"" - Part string `yaml:"part,omitempty"` + Part string `yaml:"part,omitempty" jsonschema:"title=part of response to extract data from,description=Part of the request response to extract data from"` // description: | // Internal, when set to true will allow using the value extracted // in the next request for some protocols (like HTTP). - Internal bool `yaml:"internal,omitempty"` + Internal bool `yaml:"internal,omitempty" jsonschema:"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"` } // ExtractorType is the type of the extractor specified diff --git a/v2/pkg/operators/matchers/matchers.go b/v2/pkg/operators/matchers/matchers.go index c8484c0c..19cb3c8e 100644 --- a/v2/pkg/operators/matchers/matchers.go +++ b/v2/pkg/operators/matchers/matchers.go @@ -17,14 +17,14 @@ type Matcher struct { // - "regex" // - "binary" // - "dsl" - Type string `yaml:"type"` + Type string `yaml:"type" jsonschema:"title=type of matcher,description=Type of the matcher,enum=status,enum=size,enum=word,enum=regex,enum=dsl"` // description: | // Condition is the optional condition between two matcher variables. By default, // the condition is assumed to be OR. // values: // - "and" // - "or" - Condition string `yaml:"condition,omitempty"` + Condition string `yaml:"condition,omitempty" jsonschema:"title=condition between matcher variables,description=Condition between the matcher variables,enum=and,enum=or"` // description: | // Part is the part of the request response to match data from. @@ -34,31 +34,31 @@ type Matcher struct { // examples: // - value: "\"body\"" // - value: "\"raw\"" - Part string `yaml:"part,omitempty"` + Part string `yaml:"part,omitempty" jsonschema:"title=part of response to match,description=Part of response to match data from"` // description: | // Negative specifies if the match should be reversed // It will only match if the condition is not true. - Negative bool `yaml:"negative,omitempty"` + Negative bool `yaml:"negative,omitempty" jsonschema:"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"` // description: | // Name of the matcher. Name should be lowercase and must not contain // spaces or dashes (-). // examples: // - value: "\"cookie-matcher\"" - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=name of the matcher,description=Name of the matcher"` // description: | // Status are the acceptable status codes for the response. // examples: // - value: > // []int{200, 302} - Status []int `yaml:"status,omitempty"` + Status []int `yaml:"status,omitempty" jsonschema:"title=status to match,description=Status to match for the response"` // description: | // Size is the acceptable size for the response // examples: // - value: > // []int{3029, 2042} - Size []int `yaml:"size,omitempty"` + Size []int `yaml:"size,omitempty" jsonschema:"title=acceptable size for response,description=Size is the acceptable size for the response"` // description: | // Words contains word patterns required to be present in the response part. // examples: @@ -68,7 +68,7 @@ type Matcher struct { // - name: Match for application/json in response headers // value: > // []string{"application/json"} - Words []string `yaml:"words,omitempty"` + Words []string `yaml:"words,omitempty" jsonschema:"title=words to match in response,description= Words contains word patterns required to be present in the response part"` // description: | // Regex contains Regular Expression patterns required to be present in the response part. // examples: @@ -78,7 +78,7 @@ type Matcher struct { // - name: Match for Open Redirect via Location header // value: > // []string{`(?m)^(?:Location\\s*?:\\s*?)(?:https?://|//)?(?:[a-zA-Z0-9\\-_\\.@]*)example\\.com.*$`} - Regex []string `yaml:"regex,omitempty"` + Regex []string `yaml:"regex,omitempty" jsonschema:"title=regex to match in response,description=Regex contains regex patterns required to be present in the response part"` // description: | // Binary are the binary patterns required to be present in the response part. // examples: @@ -88,7 +88,7 @@ type Matcher struct { // - name: Match for 7zip files // value: > // []string{"377ABCAF271C"} - Binary []string `yaml:"binary,omitempty"` + Binary []string `yaml:"binary,omitempty" jsonschema:"title=binary patterns to match in response,description=Binary are the binary patterns required to be present in the response part"` // description: | // DSL are the dsl expressions that will be evaluated as part of nuclei matching rules. // A list of these helper functions are available [here](https://nuclei.projectdiscovery.io/templating-guide/helper-functions/). @@ -99,12 +99,12 @@ type Matcher struct { // - name: DSL Matcher for missing strict transport security header // value: > // []string{"!contains(tolower(all_headers), ''strict-transport-security'')"} - DSL []string `yaml:"dsl,omitempty"` + DSL []string `yaml:"dsl,omitempty" jsonschema:"title=dsl expressions to match in response,description=DSL are the dsl expressions that will be evaluated as part of nuclei matching rules"` // description: | // Encoding specifies the encoding for the words field if any. // values: // - "hex" - Encoding string `yaml:"encoding,omitempty"` + Encoding string `yaml:"encoding,omitempty" jsonschema:"title=encoding for word field,description=Optional encoding for the word fields,enum=hex"` // cached data for the compiled matcher condition ConditionType diff --git a/v2/pkg/operators/operators.go b/v2/pkg/operators/operators.go index d37d0626..3bf0f0ef 100644 --- a/v2/pkg/operators/operators.go +++ b/v2/pkg/operators/operators.go @@ -16,17 +16,17 @@ type Operators struct { // // Multiple matchers can be combined together with `matcher-condition` flag // which accepts either `and` or `or` as argument. - Matchers []*matchers.Matcher `yaml:"matchers,omitempty"` + Matchers []*matchers.Matcher `yaml:"matchers,omitempty" jsonschema:"title=matchers to run on response,description=Detection mechanism to identify whether the request was successful by doing pattern matching"` // description: | // Extractors contains the extraction mechanism for the request to identify // and extract parts of the response. - Extractors []*extractors.Extractor `yaml:"extractors,omitempty"` + Extractors []*extractors.Extractor `yaml:"extractors,omitempty" jsonschema:"title=extractors to run on response,description=Extractors contains the extraction mechanism for the request to identify and extract parts of the response"` // description: | // MatchersCondition is the condition between the matchers. Default is OR. // values: // - "and" // - "or" - MatchersCondition string `yaml:"matchers-condition,omitempty"` + MatchersCondition string `yaml:"matchers-condition,omitempty" jsonschema:"title=condition between the matchers,description=Conditions between the matchers,enum=and,enum=or"` // cached variables that may be used along with request. matchersCondition matchers.ConditionType } diff --git a/v2/pkg/protocols/dns/dns.go b/v2/pkg/protocols/dns/dns.go index c322883b..106671fa 100644 --- a/v2/pkg/protocols/dns/dns.go +++ b/v2/pkg/protocols/dns/dns.go @@ -19,7 +19,7 @@ type Request struct { operators.Operators `yaml:",inline"` // ID is the ID of the request - ID string `yaml:"id,omitempty"` + ID string `yaml:"id,omitempty" jsonschema:"title=id of the dns request,description=ID is the optional ID of the DNS Request"` // description: | // Name is the Hostname to make DNS request for. @@ -27,19 +27,20 @@ type Request struct { // Generally, it is set to {{FQDN}} which is the domain we get from input. // examples: // - value: "\"{{FQDN}}\"" - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=hostname to make dns request for,description=Name is the Hostname to make DNS request for"` // description: | // Type is the type of DNS request to make. // values: // - "A" // - "NS" + // - "DS" // - "CNAME" // - "SOA" // - "PTR" // - "MX" // - "TXT" // - "AAAA" - Type string `yaml:"type,omitempty"` + Type string `yaml:"type,omitempty" jsonschema:"title=type of dns request to make,description=Type is the type of DNS request to make,enum=A,enum=NS,enum=DS,enum=CNAME,enum=SOA,enum=PTR,enum=MX,enum=TXT,enum=AAAA"` // description: | // Class is the class of the DNS request. // @@ -51,13 +52,13 @@ type Request struct { // - "HESIOD" // - "NONE" // - "ANY" - Class string `yaml:"class,omitempty"` + Class string `yaml:"class,omitempty" jsonschema:"title=class of DNS request,description=Class is the class of the DNS request,enum=INET,enum=CSNET,enum=CHAOS,enum=HESIOD,enum=NONE,enum=ANY"` // description: | // Retries is the number of retries for the DNS request // examples: // - name: Use a retry of 3 to 5 generally // value: 5 - Retries int `yaml:"retries,omitempty"` + Retries int `yaml:"retries,omitempty" jsonschema:"title=retries for dns request,description=Retries is the number of retries for the DNS request"` CompiledOperators *operators.Operators `yaml:"-"` dnsClient *retryabledns.Client @@ -69,7 +70,7 @@ type Request struct { // description: | // Recursion determines if resolver should recurse all records to get fresh results. - Recursion bool `yaml:"recursion,omitempty"` + Recursion bool `yaml:"recursion,omitempty" jsonschema:"title=recurse all servers,description=Recursion determines if resolver should recurse all records to get fresh results"` } // GetID returns the unique ID of the request if any. diff --git a/v2/pkg/protocols/file/file.go b/v2/pkg/protocols/file/file.go index 62f85eec..8072e1e0 100644 --- a/v2/pkg/protocols/file/file.go +++ b/v2/pkg/protocols/file/file.go @@ -16,7 +16,7 @@ type Request struct { // Extensions is the list of extensions to perform matching on. // examples: // - value: '[]string{".txt", ".go", ".json"}' - Extensions []string `yaml:"extensions,omitempty"` + Extensions []string `yaml:"extensions,omitempty" jsonschema:"title=extensions to match,description=List of extensions to perform matching on"` // description: | // ExtensionDenylist is the list of file extensions to deny during matching. // @@ -24,10 +24,10 @@ type Request struct { // in nuclei. // examples: // - value: '[]string{".avi", ".mov", ".mp3"}' - ExtensionDenylist []string `yaml:"denylist,omitempty"` + ExtensionDenylist []string `yaml:"denylist,omitempty" jsonschema:"title=extensions to deny match,description=List of file extensions to deny during matching"` // ID is the ID of the request - ID string `yaml:"id,omitempty"` + ID string `yaml:"id,omitempty" jsonschema:"title=id of the request,description=ID is the optional ID for the request"` // description: | // MaxSize is the maximum size of the file to run request on. @@ -36,7 +36,7 @@ type Request struct { // It can be set to much lower or higher depending on use. // examples: // - value: 2048 - MaxSize int `yaml:"max-size,omitempty"` + MaxSize int `yaml:"max-size,omitempty" jsonschema:"title=max size data to run request on,description=Maximum size of the file to run request on"` CompiledOperators *operators.Operators `yaml:"-"` // cache any variables that may be needed for operation. @@ -46,7 +46,7 @@ type Request struct { // description: | // NoRecursive specifies whether to not do recursive checks if folders are provided. - NoRecursive bool `yaml:"no-recursive,omitempty"` + NoRecursive bool `yaml:"no-recursive,omitempty" jsonschema:"title=do not perform recursion,description=Specifies whether to not do recursive checks if folders are provided"` allExtensions bool } diff --git a/v2/pkg/protocols/headless/engine/action.go b/v2/pkg/protocols/headless/engine/action.go index 6ad6973f..1da88140 100644 --- a/v2/pkg/protocols/headless/engine/action.go +++ b/v2/pkg/protocols/headless/engine/action.go @@ -113,17 +113,17 @@ type Action struct { // Args contain arguments for the headless action. // // Per action arguments are described in detail [here](https://nuclei.projectdiscovery.io/templating-guide/protocols/headless/). - Data map[string]string `yaml:"args,omitempty"` + Data map[string]string `yaml:"args,omitempty" jsonschema:"title=arguments for headless action,description=Args contain arguments for the headless action"` // description: | // Name is the name assigned to the headless action. // // This can be used to execute code, for instance in browser // DOM using script action, and get the result in a variable // which can be matched upon by nuclei. An Example template [here](https://github.com/projectdiscovery/nuclei-templates/blob/master/headless/prototype-pollution-check.yaml). - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=name for headless action,description=Name is the name assigned to the headless action"` // description: | // Description is the optional description of the headless action - Description string `yaml:"description,omitempty"` + Description string `yaml:"description,omitempty" jsonschema:"title=description for headless action,description=Description of the headless action"` // description: | // Action is the type of the action to perform. // values: @@ -148,7 +148,7 @@ type Action struct { // - "keyboard" // - "debug" // - "sleep" - ActionType string `yaml:"action"` + ActionType string `yaml:"action" jsonschema:"title=action to perform,description=Type of actions to perform,enum=navigate,enum=script,enum=click,enum=rightclick,enum=text,enum=screenshot,enum=time,enum=select,enum=files,enum=waitload,enum=getresource,enum=extract,enum=setmethod,enum=addheader,enum=setheader,enum=deleteheader,enum=setbody,enum=waitevent,enum=keyboard,enum=debug,enum=sleep"` } // String returns the string representation of an action diff --git a/v2/pkg/protocols/headless/headless.go b/v2/pkg/protocols/headless/headless.go index 00a5af25..b95615ab 100644 --- a/v2/pkg/protocols/headless/headless.go +++ b/v2/pkg/protocols/headless/headless.go @@ -10,11 +10,11 @@ import ( // Request contains a Headless protocol request to be made from a template type Request struct { // ID is the ID of the request - ID string `yaml:"id,omitempty"` + ID string `yaml:"id,omitempty" jsonschema:"title=id of the request,description=Optional ID of the headless request"` // description: | // Steps is the list of actions to run for headless request - Steps []*engine.Action `yaml:"steps,omitempty"` + Steps []*engine.Action `yaml:"steps,omitempty" jsonschema:"title=list of actions for headless request,description=List of actions to run for headless request"` // Operators for the current request go here. operators.Operators `yaml:",inline,omitempty"` diff --git a/v2/pkg/protocols/http/http.go b/v2/pkg/protocols/http/http.go index 4e93753a..6651fa03 100644 --- a/v2/pkg/protocols/http/http.go +++ b/v2/pkg/protocols/http/http.go @@ -23,22 +23,22 @@ type Request struct { // - name: Some example path values // value: > // []string{"{{BaseURL}}", "{{BaseURL}}/+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions"} - Path []string `yaml:"path,omitempty"` + Path []string `yaml:"path,omitempty" jsonschema:"title=path(s) for the http request,description=Path(s) to send http requests to"` // description: | // Raw contains HTTP Requests in Raw format. // examples: // - name: Some example raw requests // value: | // []string{"GET /etc/passwd HTTP/1.1\nHost:\nContent-Length: 4", "POST /.%0d./.%0d./.%0d./.%0d./bin/sh HTTP/1.1\nHost: {{Hostname}}\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0\nContent-Length: 1\nConnection: close\n\necho\necho\ncat /etc/passwd 2>&1"} - Raw []string `yaml:"raw,omitempty"` + Raw []string `yaml:"raw,omitempty" jsonschema:"http requests in raw format,description=HTTP Requests in Raw Format"` // ID is the ID of the request - ID string `yaml:"id,omitempty"` + ID string `yaml:"id,omitempty" jsonschema:"title=id for the http request,description=ID for the HTTP Request"` // description: | // 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 multirequest based matchers. - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=name for the http request,description=Optional name for the HTTP Request"` // description: | // Attack is the type of payload combinations to perform. // @@ -48,58 +48,63 @@ type Request struct { // - "sniper" // - "pitchfork" // - "clusterbomb" - AttackType string `yaml:"attack,omitempty"` + AttackType string `yaml:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=sniper,enum=pitchfork,enum=clusterbomb"` // description: | // Method is the HTTP Request Method. // values: // - "GET" + // - "HEAD" // - "POST" // - "PUT" // - "DELETE" - Method string `yaml:"method,omitempty"` + // - "CONNECT" + // - "OPTIONS" + // - "TRACE" + // - "PATCH" + Method string `yaml:"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"` // description: | // Body is an optional parameter which contains HTTP Request body. // examples: // - name: Same Body for a Login POST request // value: "\"username=test&password=test\"" - Body string `yaml:"body,omitempty"` + Body string `yaml:"body,omitempty" jsonschema:"title=body is the http request body,description=Body is an optional parameter which contains HTTP Request body"` // description: | // Payloads contains any payloads for the current request. // // 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"` + Payloads map[string]interface{} `yaml:"payloads,omitempty" jsonschema:"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"` + Headers map[string]string `yaml:"headers,omitempty" jsonschema:"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: // - name: Send a request 5 times // value: "5" - RaceNumberRequests int `yaml:"race_count,omitempty"` + RaceNumberRequests int `yaml:"race_count,omitempty" jsonschema:"title=number of times to repeat request in race condition,description=Number of times to send a request in Race Condition Attack"` // description: | // MaxRedirects is the maximum number of redirects that should be followed. // examples: // - name: Follow upto 5 redirects // value: "5" - MaxRedirects int `yaml:"max-redirects,omitempty"` + MaxRedirects int `yaml:"max-redirects,omitempty" jsonschema:"title=maximum number of redirects to follow,description=Maximum number of redirects that should be followed"` // description: | // PipelineConcurrentConnections is number of connections to create during pipelining. // examples: // - name: Create 40 concurrent connections // value: 40 - PipelineConcurrentConnections int `yaml:"pipeline-concurrent-connections,omitempty"` + PipelineConcurrentConnections int `yaml:"pipeline-concurrent-connections,omitempty" jsonschema:"title=number of pipelining connections,description=Number of connections to create during pipelining"` // description: | // PipelineRequestsPerConnection is number of requests to send per connection when pipelining. // examples: // - name: Send 100 requests per pipeline connection // value: 100 - PipelineRequestsPerConnection int `yaml:"pipeline-requests-per-connection,omitempty"` + PipelineRequestsPerConnection int `yaml:"pipeline-requests-per-connection,omitempty" jsonschema:"title=number of requests to send per pipelining connections,description=Number of requests to send per connection when pipelining"` // description: | // Threads specifies number of threads to use sending requests. This enables Connection Pooling. // @@ -108,14 +113,13 @@ type Request struct { // examples: // - name: Send requests using 10 concurrent threads // value: 10 - Threads int `yaml:"threads,omitempty"` - + Threads int `yaml:"threads,omitempty" jsonschema:"title=threads for sending requests,description=Threads specifies number of threads to use sending requests. This enables Connection Pooling"` // description: | // MaxSize is the maximum size of http response body to read in bytes. // examples: // - name: Read max 2048 bytes of the response // value: 2048 - MaxSize int `yaml:"max-size,omitempty"` + MaxSize int `yaml:"max-size,omitempty" jsonschema:"title=maximum http response body size,description=Maximum size of http response body to read in bytes"` CompiledOperators *operators.Operators `yaml:"-"` @@ -130,33 +134,33 @@ type Request struct { // description: | // CookieReuse is an optional setting that enables cookie reuse for // all requests defined in raw section. - CookieReuse bool `yaml:"cookie-reuse,omitempty"` + CookieReuse bool `yaml:"cookie-reuse,omitempty" jsonschema:"title=optional cookie reuse enable,description=Optional setting that enables cookie reuse"` // description: | // Redirects specifies whether redirects should be followed by the HTTP Client. // // This can be used in conjunction with `max-redirects` to control the HTTP request redirects. - Redirects bool `yaml:"redirects,omitempty"` + Redirects bool `yaml:"redirects,omitempty" jsonschema:"title=follow http redirects,description=Specifies whether redirects should be followed by the HTTP Client"` // description: | // Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining // // All requests must be indempotent (GET/POST). This can be used for race conditions/billions requests. - Pipeline bool `yaml:"pipeline,omitempty"` + Pipeline bool `yaml:"pipeline,omitempty" jsonschema:"title=perform HTTP 1.1 pipelining,description=Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining"` // description: | // 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. - Unsafe bool `yaml:"unsafe,omitempty"` + Unsafe bool `yaml:"unsafe,omitempty" jsonschema:"title=use rawhttp non-strict-rfc client,description=Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests"` // description: | // 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. - Race bool `yaml:"race,omitempty"` + Race bool `yaml:"race,omitempty" jsonschema:"title=perform race-http request coordination attack,description=Race determines if all the request have to be attempted at the same time (Race Condition)"` // description: | // ReqCondition automatically assigns numbers to requests and preserves their history. // // This allows matching on them later for multi-request conditions. - ReqCondition bool `yaml:"req-condition,omitempty"` + ReqCondition bool `yaml:"req-condition,omitempty" jsonschema:"title=preserve request history,description=Automatically assigns numbers to requests and preserves their history"` } // GetID returns the unique ID of the request if any. diff --git a/v2/pkg/protocols/network/network.go b/v2/pkg/protocols/network/network.go index 180a5df7..e4ea521b 100644 --- a/v2/pkg/protocols/network/network.go +++ b/v2/pkg/protocols/network/network.go @@ -16,17 +16,17 @@ import ( // Request contains a Network protocol request to be made from a template type Request struct { // ID is the ID of the request - ID string `yaml:"id,omitempty"` + ID string `yaml:"id,omitempty" jsonschema:"title=id of the request,description=ID of the network request"` // description: | - // Address is the address to send requests to. + // Host to send network requests to. // // Usually it's set to `{{Hostname}}`. If you want to enable TLS for // TCP Connection, you can use `tls://{{Hostname}}`. // examples: // - value: | // []string{"{{Hostname}}"} - Address []string `yaml:"host,omitempty"` + Address []string `yaml:"host,omitempty" jsonschema:"title=host to send requests to,description=Host to send network requests to"` addresses []addressKV // description: | @@ -38,25 +38,25 @@ type Request struct { // - "sniper" // - "pitchfork" // - "clusterbomb" - AttackType string `yaml:"attack,omitempty"` + AttackType string `yaml:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=sniper,enum=pitchfork,enum=clusterbomb"` // description: | // Payloads contains any payloads for the current request. // // 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"` + Payloads map[string]interface{} `yaml:"payloads,omitempty" jsonschema:"title=payloads for the network request,description=Payloads contains any payloads for the current request"` // description: | // Inputs contains inputs for the network socket - Inputs []*Input `yaml:"inputs,omitempty"` + Inputs []*Input `yaml:"inputs,omitempty" jsonschema:"title=inputs for the network request,description=Inputs contains any input/output for the current request"` // description: | // ReadSize is the size of response to read at the end // // Default value for read-size is 1024. // examples: // - value: "2048" - ReadSize int `yaml:"read-size,omitempty"` + ReadSize int `yaml:"read-size,omitempty" jsonschema:"title=size of network response to read,description=Size of response to read at the end. Default is 1024 bytes"` // Operators for the current request go here. operators.Operators `yaml:",inline,omitempty"` @@ -84,7 +84,7 @@ type Input struct { // examples: // - value: "\"TEST\"" // - value: "\"hex_decode('50494e47')\"" - Data string `yaml:"data,omitempty"` + Data string `yaml:"data,omitempty" jsonschema:"title=data to send as input,description=Data is the data to send as the input"` // description: | // Type is the type of input specified in `data` field. // @@ -92,7 +92,7 @@ type Input struct { // values: // - "hex" // - "text" - Type string `yaml:"type,omitempty"` + Type string `yaml:"type,omitempty" jsonschema:"title=type is the type of input data,description=Type of input specified in data field,enum=hex,enum=text"` // description: | // Read is the number of bytes to read from socket. // @@ -103,12 +103,12 @@ type Input struct { // The [network docs](https://nuclei.projectdiscovery.io/templating-guide/protocols/network/) highlight more on how to do this. // examples: // - value: "1024" - Read int `yaml:"read,omitempty"` + Read int `yaml:"read,omitempty" jsonschema:"title=bytes to read from socket,description=Number of bytes to read from socket"` // description: | // Name is the optional name of the data read to provide matching on. // examples: // - value: "\"prefix\"" - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=optional name for data read,description=Optional name of the data read to provide matching on"` } // GetID returns the unique ID of the request if any. diff --git a/v2/pkg/reporting/exporters/sarif/sarif.go b/v2/pkg/reporting/exporters/sarif/sarif.go index 7f78c077..50fdaa4e 100644 --- a/v2/pkg/reporting/exporters/sarif/sarif.go +++ b/v2/pkg/reporting/exporters/sarif/sarif.go @@ -55,7 +55,7 @@ func (i *Exporter) Export(event *output.ResultEvent) error { templatePath := strings.TrimPrefix(event.TemplatePath, i.home) h := sha1.New() - h.Write([]byte(event.Host)) + _, _ = h.Write([]byte(event.Host)) templateID := event.TemplateID + "-" + hex.EncodeToString(h.Sum(nil)) fullDescription := format.MarkdownDescription(event) diff --git a/v2/pkg/templates/templates.go b/v2/pkg/templates/templates.go index 2cd1e209..3194425c 100644 --- a/v2/pkg/templates/templates.go +++ b/v2/pkg/templates/templates.go @@ -28,39 +28,39 @@ type Template struct { // examples: // - name: ID Example // value: "\"cve-2021-19520\"" - ID string `yaml:"id"` + ID string `yaml:"id" jsonschema:"title=id of the template,description=The Unique ID for the template,example=cve-2021-19520"` // description: | // Info contains metadata information about the template. // examples: // - value: exampleInfoStructure - Info model.Info `yaml:"info"` + Info model.Info `yaml:"info" jsonschema:"title=info for the template,description=Info contains metadata for the template"` // description: | // Requests contains the http request to make in the template. // examples: // - value: exampleNormalHTTPRequest - RequestsHTTP []*http.Request `yaml:"requests,omitempty" json:"requests"` + RequestsHTTP []*http.Request `yaml:"requests,omitempty" json:"requests,omitempty" jsonschema:"title=http requests to make,description=HTTP requests to make for the template"` // description: | // DNS contains the dns request to make in the template // examples: // - value: exampleNormalDNSRequest - RequestsDNS []*dns.Request `yaml:"dns,omitempty" json:"dns"` + RequestsDNS []*dns.Request `yaml:"dns,omitempty" json:"dns,omitempty" jsonschema:"title=dns requests to make,description=DNS requests to make for the template"` // description: | // File contains the file request to make in the template // examples: // - value: exampleNormalFileRequest - RequestsFile []*file.Request `yaml:"file,omitempty" json:"file"` + RequestsFile []*file.Request `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"title=file requests to make,description=File requests to make for the template"` // description: | // Network contains the network request to make in the template // examples: // - value: exampleNormalNetworkRequest - RequestsNetwork []*network.Request `yaml:"network,omitempty" json:"network"` + RequestsNetwork []*network.Request `yaml:"network,omitempty" json:"network,omitempty" jsonschema:"title=network requests to make,description=Network requests to make for the template"` // description: | // Headless contains the headless request to make in the template. - RequestsHeadless []*headless.Request `yaml:"headless,omitempty" json:"headless"` + RequestsHeadless []*headless.Request `yaml:"headless,omitempty" json:"headless,omitempty" jsonschema:"title=headless requests to make,description=Headless requests to make for the template"` // description: | // Workflows is a yaml based workflow declaration code. - workflows.Workflow `yaml:",inline,omitempty"` + workflows.Workflow `yaml:",inline,omitempty" jsonschema:"title=workflows to run,description=Workflows to run for the template"` CompiledWorkflow *workflows.Workflow `yaml:"-" json:"-" jsonschema:"-"` // TotalRequests is the total number of requests for the template. diff --git a/v2/pkg/workflows/workflows.go b/v2/pkg/workflows/workflows.go index d9da7e99..fb53948f 100644 --- a/v2/pkg/workflows/workflows.go +++ b/v2/pkg/workflows/workflows.go @@ -9,9 +9,9 @@ import ( type Workflow struct { // description: | // Workflows is a list of workflows to execute for a template. - Workflows []*WorkflowTemplate `yaml:"workflows,omitempty"` + Workflows []*WorkflowTemplate `yaml:"workflows,omitempty" jsonschema:"title=list of workflows to execute,description=List of workflows to execute for template"` - Options *protocols.ExecuterOptions + Options *protocols.ExecuterOptions `yaml:"-"` } // WorkflowTemplate is a template to be ran as part of a workflow @@ -23,16 +23,16 @@ type WorkflowTemplate struct { // value: "\"dns/worksites-detection.yaml\"" // - name: A template directory // value: "\"misconfigurations/aem\"" - Template string `yaml:"template,omitempty"` + Template string `yaml:"template,omitempty" jsonschema:"title=template/directory to execute,description=Template or directory to execute as part of workflow"` // description: | // Tags to run templates based on. - Tags model.StringSlice `yaml:"tags,omitempty"` + Tags model.StringSlice `yaml:"tags,omitempty" jsonschema:"title=tags to execute,description=Tags to run template based on"` // description: | // Matchers perform name based matching to run subtemplates for a workflow. - Matchers []*Matcher `yaml:"matchers,omitempty"` + Matchers []*Matcher `yaml:"matchers,omitempty" jsonschema:"title=name based template result matchers,description=Matchers perform name based matching to run subtemplates for a workflow"` // description: | // Subtemplates are ran if the `template` field Template matches. - Subtemplates []*WorkflowTemplate `yaml:"subtemplates,omitempty"` + Subtemplates []*WorkflowTemplate `yaml:"subtemplates,omitempty" jsonschema:"title=subtemplate based result matchers,description=Subtemplates are ran if the template field Template matches"` // Executers perform the actual execution for the workflow template Executers []*ProtocolExecuterPair `yaml:"-"` } @@ -47,8 +47,8 @@ type ProtocolExecuterPair struct { type Matcher struct { // description: | // Name is the name of the item to match. - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" jsonschema:"title=name of item to match,description=Name of item to match"` // description: | // Subtemplates are ran if the name of matcher matches. - Subtemplates []*WorkflowTemplate `yaml:"subtemplates,omitempty"` + Subtemplates []*WorkflowTemplate `yaml:"subtemplates,omitempty" jsonschema:"title=templates to run after match,description=Templates to run after match"` }