mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 02:08:33 -10:00
Compare commits
15
Commits
dev
...
4950-jsonschema
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b4576ff3 | ||
|
|
310d62b125 | ||
|
|
f0984f1e7c | ||
|
|
038a1ef688 | ||
|
|
a9a3a348ae | ||
|
|
cbf782051d | ||
|
|
ff563ae047 | ||
|
|
1f0494e771 | ||
|
|
a31104eb22 | ||
|
|
8cc9ff3a9b | ||
|
|
279bb0476a | ||
|
|
473d4bdaae | ||
|
|
1e5c5a30f7 | ||
|
|
0a0a9a1108 | ||
|
|
1c0dc311fa |
No files matched your search
@@ -22,7 +22,7 @@ ifeq ($(shell go env GOOS),windows)
|
||||
endif
|
||||
|
||||
.PHONY: all build build-stats clean devtools-all devtools-bindgen devtools-scrapefuncs fuzz fuzz-ci fuzz-tools
|
||||
.PHONY: devtools-tsgen docs docgen dsl-docs functional go-build lint lint-strict fuzzplayground syntax-docs
|
||||
.PHONY: devtools-tsgen docs docgen dsl-docs functional go-build lint lint-strict fuzzplayground syntax-docs genschema
|
||||
.PHONY: integration integration-debug regression jsupdate-all jsupdate-bindgen jsupdate-tsgen memogen scan-charts test test-with-lint
|
||||
.PHONY: tidy ts verify download vet template-validate build-fuzz discover-fuzz-packages
|
||||
|
||||
@@ -83,6 +83,12 @@ syntax-docs: docgen
|
||||
syntax-docs:
|
||||
./bin/docgen SYNTAX-REFERENCE.md nuclei-jsonschema.json
|
||||
|
||||
# Regenerate nuclei-jsonschema.json only (skips markdown docs output).
|
||||
genschema:
|
||||
rm -f ./bin/docgen
|
||||
$(GOBUILD) -o ./bin/docgen cmd/docgen/docgen.go
|
||||
./bin/docgen /dev/null nuclei-jsonschema.json
|
||||
|
||||
# RACE controls the data-race detector (on by default for local runs). CI builds
|
||||
# the race variant on a single OS and passes RACE= elsewhere, since data races are
|
||||
# OS-independent and the detector costs ~2-3x build+run time on every runner.
|
||||
|
||||
+91
-5
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
nucleijson "github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
var pathRegex = regexp.MustCompile(`github\.com/projectdiscovery/nuclei/v3/(?:internal|pkg)/(?:.*/)?([A-Za-z.]+)`)
|
||||
@@ -23,8 +24,7 @@ func writeToFile(filename string, data []byte) {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
if _, err := file.Write(data); err != nil {
|
||||
log.Fatalf("Could not write to file %s: %s\n", filename, err)
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,98 @@ func main() {
|
||||
jsonschemaData := r.Reflect(&templates.Template{})
|
||||
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder := nucleijson.NewEncoder(&buf)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(jsonschemaData); err != nil {
|
||||
log.Fatalf("Could not encode JSON schema: %s\n", err)
|
||||
}
|
||||
|
||||
schema := pathRegex.ReplaceAllString(buf.String(), "$1")
|
||||
writeToFile(os.Args[2], []byte(schema))
|
||||
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(schema), &m); err != nil {
|
||||
log.Fatalf("Could not unmarshal jsonschema: %s\n", err)
|
||||
}
|
||||
|
||||
// Stable schema identity for editors / tooling.
|
||||
m["$id"] = "https://nuclei.projectdiscovery.io/template-schema.json"
|
||||
|
||||
// Enable markdown descriptions in Monaco / VS Code.
|
||||
updateDescriptionKeyName(m)
|
||||
|
||||
schemax, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not marshal jsonschema: %s\n", err)
|
||||
}
|
||||
schemax = append(schemax, '\n')
|
||||
writeToFile(os.Args[2], schemax)
|
||||
}
|
||||
|
||||
// schemaMapKeywords hold a map of names to subschemas.
|
||||
var schemaMapKeywords = map[string]struct{}{
|
||||
"properties": {},
|
||||
"patternProperties": {},
|
||||
"$defs": {},
|
||||
"definitions": {},
|
||||
"dependentSchemas": {},
|
||||
}
|
||||
|
||||
// schemaKeywords hold a single subschema.
|
||||
var schemaKeywords = map[string]struct{}{
|
||||
"items": {},
|
||||
"additionalProperties": {},
|
||||
"unevaluatedProperties": {},
|
||||
"unevaluatedItems": {},
|
||||
"contains": {},
|
||||
"propertyNames": {},
|
||||
"not": {},
|
||||
"if": {},
|
||||
"then": {},
|
||||
"else": {},
|
||||
}
|
||||
|
||||
// schemaListKeywords hold a list of subschemas.
|
||||
var schemaListKeywords = map[string]struct{}{
|
||||
"allOf": {},
|
||||
"anyOf": {},
|
||||
"oneOf": {},
|
||||
"prefixItems": {},
|
||||
}
|
||||
|
||||
// updateDescriptionKeyName copies description to markdownDescription on every
|
||||
// schema node. It walks only schema keywords, so description keys inside
|
||||
// examples, defaults, enums and consts stay as they are.
|
||||
func updateDescriptionKeyName(node map[string]interface{}) {
|
||||
if v, ok := node["description"]; ok {
|
||||
// Keep standard JSON Schema "description" for non-Monaco consumers
|
||||
// and also emit Monaco/VS Code "markdownDescription".
|
||||
node["markdownDescription"] = v
|
||||
}
|
||||
for k, v := range node {
|
||||
if _, ok := schemaMapKeywords[k]; ok {
|
||||
if children, ok := v.(map[string]interface{}); ok {
|
||||
for _, child := range children {
|
||||
if childMap, ok := child.(map[string]interface{}); ok {
|
||||
updateDescriptionKeyName(childMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := schemaKeywords[k]; ok {
|
||||
if childMap, ok := v.(map[string]interface{}); ok {
|
||||
updateDescriptionKeyName(childMap)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := schemaListKeywords[k]; ok {
|
||||
if children, ok := v.([]interface{}); ok {
|
||||
for _, child := range children {
|
||||
if childMap, ok := child.(map[string]interface{}); ok {
|
||||
updateDescriptionKeyName(childMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateDescriptionKeyName(t *testing.T) {
|
||||
var m map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal([]byte(`{
|
||||
"description": "root",
|
||||
"$defs": {
|
||||
"info": {
|
||||
"description": "info schema",
|
||||
"properties": {
|
||||
"description": {"type": "string", "description": "a property named description"},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"examples": [{"description": "example data"}],
|
||||
"default": {"description": "default data"},
|
||||
"additionalProperties": {"description": "map value"}
|
||||
}
|
||||
},
|
||||
"oneOf": [{"description": "branch"}],
|
||||
"items": {"description": "item"}
|
||||
}
|
||||
}
|
||||
}`), &m))
|
||||
|
||||
updateDescriptionKeyName(m)
|
||||
|
||||
require.Equal(t, "root", m["markdownDescription"])
|
||||
info := m["$defs"].(map[string]interface{})["info"].(map[string]interface{})
|
||||
require.Equal(t, "info schema", info["markdownDescription"])
|
||||
props := info["properties"].(map[string]interface{})
|
||||
require.NotContains(t, props, "markdownDescription")
|
||||
require.Equal(t, "a property named description", props["description"].(map[string]interface{})["markdownDescription"])
|
||||
meta := props["meta"].(map[string]interface{})
|
||||
require.Equal(t, map[string]interface{}{"description": "example data"}, meta["examples"].([]interface{})[0])
|
||||
require.Equal(t, map[string]interface{}{"description": "default data"}, meta["default"])
|
||||
require.Equal(t, "map value", meta["additionalProperties"].(map[string]interface{})["markdownDescription"])
|
||||
require.Equal(t, "branch", info["oneOf"].([]interface{})[0].(map[string]interface{})["markdownDescription"])
|
||||
require.Equal(t, "item", info["items"].(map[string]interface{})["markdownDescription"])
|
||||
}
|
||||
+2563
-1288
File diff suppressed because it is too large.
Load diff
+4
-31
@@ -8,19 +8,9 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/marker"
|
||||
"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: |
|
||||
@@ -157,27 +147,10 @@ func (i *Info) FilledFields(written *Info) []TextField {
|
||||
return filled
|
||||
}
|
||||
|
||||
// JSONSchemaProperty returns the JSON schema property for the Info object.
|
||||
// JSONSchemaExtend extends the Info JSON schema with richer descriptions and examples.
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
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([]string{"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([]string{"cve", "http"}, []string{"http", "oast"}, []string{"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",
|
||||
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.PropertyExamples(
|
||||
"https://example.com",
|
||||
[]string{"https://example.com", "https://nvd.nist.gov/vuln/detail/CVE-2021-44228"},
|
||||
),
|
||||
},
|
||||
{
|
||||
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(
|
||||
map[string]any{"max-request": 1},
|
||||
map[string]any{"max-request": 2, "vendor": "apache", "product": "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(
|
||||
map[string]any{"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"},
|
||||
map[string]any{"cwe-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",
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInfoJSONSchemaObjectExamples(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Info{})
|
||||
infoSchema := reflected.Definitions["model.Info"]
|
||||
require.NotNil(t, infoSchema)
|
||||
for _, name := range []string{"metadata", "classification"} {
|
||||
property, ok := infoSchema.Properties.Get(name)
|
||||
require.True(t, ok, name)
|
||||
require.NotEmpty(t, property.Examples, name)
|
||||
for _, example := range property.Examples {
|
||||
require.IsType(t, map[string]any{}, example, "%s is an object, so its examples must be objects", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoJSONSchemaArrayBranchExamples(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Info{})
|
||||
infoSchema := reflected.Definitions["model.Info"]
|
||||
require.NotNil(t, infoSchema)
|
||||
for _, name := range []string{"author", "tags"} {
|
||||
property, ok := infoSchema.Properties.Get(name)
|
||||
require.True(t, ok, name)
|
||||
for _, branch := range property.OneOf {
|
||||
require.NotEmpty(t, branch.Examples, name)
|
||||
for _, example := range branch.Examples {
|
||||
if branch.Type == "array" {
|
||||
require.IsType(t, []string{}, example, "%s array branch needs array examples", name)
|
||||
} else {
|
||||
require.IsType(t, "", example, "%s string branch needs string examples", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package extractors
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var extractorMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "name",
|
||||
Description: schema.MultiLine(
|
||||
"Optional extractor name used as a variable key",
|
||||
"Prefer lowercase without spaces or underscores",
|
||||
),
|
||||
Example: schema.PropertyExample("cookie-extractor"),
|
||||
},
|
||||
{
|
||||
PropName: "type",
|
||||
Description: schema.MultiLine(
|
||||
"Type of the extractor",
|
||||
"Supported values: regex, kval, json, xpath, dsl",
|
||||
),
|
||||
Example: schema.PropertyExamples("regex", "kval", "json", "xpath", "dsl"),
|
||||
},
|
||||
{
|
||||
PropName: "regex",
|
||||
Description: schema.MultiLine("Regular expression patterns used to extract values"),
|
||||
Example: schema.PropertyExample([]string{`token=([a-z0-9]+)`}),
|
||||
},
|
||||
{
|
||||
PropName: "group",
|
||||
Description: schema.MultiLine("Numbered capture group to extract from the regex"),
|
||||
Example: schema.PropertyExample(1),
|
||||
},
|
||||
{
|
||||
PropName: "kval",
|
||||
Description: schema.MultiLine(
|
||||
"Key names to extract from headers/cookies (case-insensitive)",
|
||||
"Dashes in header names must be replaced with underscores",
|
||||
),
|
||||
Example: schema.PropertyExamples([]string{"server"}, []string{"content_type"}),
|
||||
},
|
||||
{
|
||||
PropName: "json",
|
||||
Description: schema.MultiLine("jq-style expressions used to extract from JSON responses"),
|
||||
Example: schema.PropertyExample([]string{".[] | .id"}),
|
||||
},
|
||||
{
|
||||
PropName: "xpath",
|
||||
Description: schema.MultiLine("XPath expressions used to extract from HTML responses"),
|
||||
Example: schema.PropertyExample([]string{"/html/body/div/p[2]/a"}),
|
||||
},
|
||||
{
|
||||
PropName: "attribute",
|
||||
Description: schema.MultiLine("Optional HTML attribute to extract from XPath matches"),
|
||||
Example: schema.PropertyExample("href"),
|
||||
},
|
||||
{
|
||||
PropName: "dsl",
|
||||
Description: schema.MultiLine("DSL expressions used to extract values"),
|
||||
Example: schema.PropertyExample([]string{"body"}),
|
||||
},
|
||||
{
|
||||
PropName: "part",
|
||||
Description: schema.MultiLine(
|
||||
"Part of the protocol response to extract from",
|
||||
"Common values: body, header, all, raw",
|
||||
),
|
||||
Example: schema.PropertyExamples("body", "header", "all"),
|
||||
},
|
||||
{
|
||||
PropName: "internal",
|
||||
Description: schema.MultiLine("Mark extracted value for internal use in subsequent requests"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "case-insensitive",
|
||||
Description: schema.MultiLine("Perform case-insensitive extraction when supported"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
}
|
||||
|
||||
// extractorAnyOfRequired binds each extractor type to the field that holds its values.
|
||||
var extractorAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.RequireConst("type", "regex", "regex"),
|
||||
schema.RequireConst("type", "kval", "kval"),
|
||||
schema.RequireConst("type", "json", "json"),
|
||||
schema.RequireConst("type", "xpath", "xpath"),
|
||||
schema.RequireConst("type", "dsl", "dsl"),
|
||||
schema.RequireConst("type", "llm", "prompt"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the Extractor JSON schema.
|
||||
func (Extractor) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(extractorMetadata, base)
|
||||
schema.ApplyAnyOfRequired(extractorAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package extractors
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractorJSONSchemaBindsTypeToField(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Extractor{})
|
||||
extractorSchema := reflected.Definitions["extractors.Extractor"]
|
||||
require.NotNil(t, extractorSchema)
|
||||
|
||||
got := map[string]string{}
|
||||
for _, alternative := range extractorSchema.AnyOf {
|
||||
require.Len(t, alternative.Required, 2)
|
||||
require.Equal(t, "type", alternative.Required[0])
|
||||
typ, ok := alternative.Properties.Get("type")
|
||||
require.True(t, ok, "alternative %v must constrain the type", alternative.Required)
|
||||
got[typ.Const.(string)] = alternative.Required[1]
|
||||
}
|
||||
require.Equal(t, map[string]string{
|
||||
"regex": "regex",
|
||||
"kval": "kval",
|
||||
"json": "json",
|
||||
"xpath": "xpath",
|
||||
"dsl": "dsl",
|
||||
"llm": "prompt",
|
||||
}, got)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var matcherMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "type",
|
||||
Description: schema.MultiLine(
|
||||
"Type of the matcher",
|
||||
"Supported values: status, size, word, regex, binary, dsl, xpath",
|
||||
),
|
||||
Example: schema.PropertyExamples("status", "word", "regex", "dsl"),
|
||||
},
|
||||
{
|
||||
PropName: "condition",
|
||||
Description: schema.MultiLine(
|
||||
"Condition between matcher values (default: or)",
|
||||
"Supported values: and, or",
|
||||
),
|
||||
Example: schema.PropertyExamples("and", "or"),
|
||||
Default: "or",
|
||||
},
|
||||
{
|
||||
PropName: "part",
|
||||
Description: schema.MultiLine(
|
||||
"Part of the protocol response to match against",
|
||||
"Common values: body, header, all, raw (protocol-specific parts also exist)",
|
||||
),
|
||||
Example: schema.PropertyExamples("body", "header", "all", "raw"),
|
||||
},
|
||||
{
|
||||
PropName: "negative",
|
||||
Description: schema.MultiLine("Reverse the match; only matches when the condition is not true"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "name",
|
||||
Description: schema.MultiLine(
|
||||
"Optional matcher name used for named matching / workflows",
|
||||
"Prefer lowercase without spaces or underscores",
|
||||
),
|
||||
Example: schema.PropertyExample("cookie-matcher"),
|
||||
},
|
||||
{
|
||||
PropName: "status",
|
||||
Description: schema.MultiLine("Acceptable HTTP status codes (status matcher)"),
|
||||
Example: schema.PropertyExample([]int{200, 302}),
|
||||
},
|
||||
{
|
||||
PropName: "size",
|
||||
Description: schema.MultiLine("Acceptable response sizes (size matcher)"),
|
||||
Example: schema.PropertyExample([]int{1024, 2048}),
|
||||
},
|
||||
{
|
||||
PropName: "words",
|
||||
Description: schema.MultiLine("Word patterns that must be present (word matcher)"),
|
||||
Example: schema.PropertyExample([]string{"application/json"}),
|
||||
},
|
||||
{
|
||||
PropName: "regex",
|
||||
Description: schema.MultiLine("Regular expressions that must match (regex matcher)"),
|
||||
Example: schema.PropertyExample([]string{`(?i)admin`}),
|
||||
},
|
||||
{
|
||||
PropName: "binary",
|
||||
Description: schema.MultiLine("Hex binary patterns that must be present (binary matcher)"),
|
||||
Example: schema.PropertyExample([]string{"1f8b080000000000"}),
|
||||
},
|
||||
{
|
||||
PropName: "dsl",
|
||||
Description: schema.MultiLine("DSL expressions that must evaluate to true (dsl matcher)"),
|
||||
Example: schema.PropertyExample([]string{"status_code == 200", "contains(body, 'ok')"}),
|
||||
},
|
||||
{
|
||||
PropName: "xpath",
|
||||
Description: schema.MultiLine("XPath expressions that must match (xpath matcher)"),
|
||||
},
|
||||
{
|
||||
PropName: "case-insensitive",
|
||||
Description: schema.MultiLine("Perform case-insensitive matching when supported"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "match-all",
|
||||
Description: schema.MultiLine("Require all values to match instead of any"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "internal",
|
||||
Description: schema.MultiLine("Mark matcher as internal (used for flow / internal logic)"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
}
|
||||
|
||||
// matcherAnyOfRequired binds each matcher type to the field that holds its values.
|
||||
var matcherAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.RequireConst("type", "status", "status"),
|
||||
schema.RequireConst("type", "size", "size"),
|
||||
schema.RequireConst("type", "word", "words"),
|
||||
schema.RequireConst("type", "regex", "regex"),
|
||||
schema.RequireConst("type", "binary", "binary"),
|
||||
schema.RequireConst("type", "dsl", "dsl"),
|
||||
schema.RequireConst("type", "xpath", "xpath"),
|
||||
schema.RequireConst("type", "llm", "prompt"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the Matcher JSON schema.
|
||||
func (Matcher) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(matcherMetadata, base)
|
||||
schema.ApplyAnyOfRequired(matcherAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatcherJSONSchemaBindsTypeToField(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Matcher{})
|
||||
matcherSchema := reflected.Definitions["matchers.Matcher"]
|
||||
require.NotNil(t, matcherSchema)
|
||||
|
||||
got := map[string]string{}
|
||||
for _, alternative := range matcherSchema.AnyOf {
|
||||
require.Len(t, alternative.Required, 2)
|
||||
require.Equal(t, "type", alternative.Required[0])
|
||||
typ, ok := alternative.Properties.Get("type")
|
||||
require.True(t, ok, "alternative %v must constrain the type", alternative.Required)
|
||||
got[typ.Const.(string)] = alternative.Required[1]
|
||||
}
|
||||
require.Equal(t, map[string]string{
|
||||
"status": "status",
|
||||
"size": "size",
|
||||
"word": "words",
|
||||
"regex": "regex",
|
||||
"binary": "binary",
|
||||
"dsl": "dsl",
|
||||
"xpath": "xpath",
|
||||
"llm": "prompt",
|
||||
}, got)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var codeRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this code request block"),
|
||||
Example: schema.PropertyExample("code-py"),
|
||||
},
|
||||
{
|
||||
PropName: "engine",
|
||||
Description: schema.MultiLine(
|
||||
"Runtime engine(s) used to execute the source",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"engine:",
|
||||
" - py",
|
||||
" - python3",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExamples([]string{"py", "python3"}, []string{"sh"}, []string{"powershell"}),
|
||||
},
|
||||
{
|
||||
PropName: "source",
|
||||
Description: schema.MultiLine(
|
||||
"Inline source snippet or file path to execute",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"source: |",
|
||||
" print(\"hello\")",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "args",
|
||||
Description: schema.MultiLine("Arguments passed to the code engine"),
|
||||
Example: schema.PropertyExample([]string{"-c"}),
|
||||
},
|
||||
{
|
||||
PropName: "pattern",
|
||||
Description: schema.MultiLine("Optional pattern used by some engines for file selection"),
|
||||
},
|
||||
{
|
||||
PropName: "pre-condition",
|
||||
Description: schema.MultiLine(
|
||||
"JavaScript condition evaluated before executing the code request",
|
||||
),
|
||||
Example: schema.PropertyExample("len(Host) > 0"),
|
||||
},
|
||||
{
|
||||
PropName: "sandbox",
|
||||
Description: schema.MultiLine(
|
||||
"Optional sandbox configuration (working directory / image) for isolated execution",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
var codeRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("engine", "source"),
|
||||
schema.Require("engine", "args"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the code request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(codeRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(codeRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var dnsRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine(
|
||||
"Optional ID of this DNS request block",
|
||||
"Useful when calling / executing this request block from flow",
|
||||
),
|
||||
Example: schema.PropertyExample("dns-a-lookup"),
|
||||
},
|
||||
{
|
||||
PropName: "name",
|
||||
Description: schema.MultiLine(
|
||||
"Hostname to make the DNS request for",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"- name: \"{{FQDN}}\"",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExamples("{{FQDN}}", "{{Hostname}}"),
|
||||
},
|
||||
{
|
||||
PropName: "type",
|
||||
Description: schema.MultiLine(
|
||||
"DNS request type",
|
||||
"Supported values: A, NS, DS, CNAME, SOA, PTR, MX, TXT, AAAA, CAA, TLSA, ANY, SRV",
|
||||
),
|
||||
Example: schema.PropertyExamples("A", "AAAA", "CNAME", "MX", "TXT"),
|
||||
},
|
||||
{
|
||||
PropName: "class",
|
||||
Description: schema.MultiLine(
|
||||
"DNS request class",
|
||||
"Supported values: inet, csnet, chaos, hesiod, none, any",
|
||||
),
|
||||
Example: schema.PropertyExample("inet"),
|
||||
Default: "inet",
|
||||
},
|
||||
{
|
||||
PropName: "retries",
|
||||
Description: schema.MultiLine("Number of retries for the DNS request"),
|
||||
Example: schema.PropertyExample(3),
|
||||
},
|
||||
{
|
||||
PropName: "resolvers",
|
||||
Description: schema.MultiLine(
|
||||
"Custom resolvers to use for this request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"resolvers:",
|
||||
" - 1.1.1.1",
|
||||
" - 8.8.8.8",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExample([]string{"1.1.1.1", "8.8.8.8"}),
|
||||
},
|
||||
{
|
||||
PropName: "recursion",
|
||||
Description: schema.MultiLine("Whether the resolver should recurse records"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "trace",
|
||||
Description: schema.MultiLine("Perform a DNS trace operation for the target"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "payloads",
|
||||
Description: schema.MultiLine(
|
||||
"Payloads for the DNS request",
|
||||
"Referenced payload keys are iterated and replaced in the request",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "attack",
|
||||
Description: schema.MultiLine(
|
||||
"Payload combination strategy when payloads are defined",
|
||||
"Supported values: batteringram, pitchfork, clusterbomb",
|
||||
),
|
||||
Example: schema.PropertyExamples("batteringram", "pitchfork", "clusterbomb"),
|
||||
},
|
||||
}
|
||||
|
||||
var dnsRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("name"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the DNS request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(dnsRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(dnsRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var fileRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this file request block"),
|
||||
Example: schema.PropertyExample("file-secret"),
|
||||
},
|
||||
{
|
||||
PropName: "extensions",
|
||||
Description: schema.MultiLine(
|
||||
"List of extensions to perform matching on",
|
||||
"Use `all` to match every file",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"extensions:",
|
||||
" - .txt",
|
||||
" - .conf",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExamples([]string{".txt", ".conf"}, []string{"all"}),
|
||||
},
|
||||
{
|
||||
PropName: "denylist",
|
||||
Description: schema.MultiLine(
|
||||
"Files, directories, and extensions to deny during matching",
|
||||
),
|
||||
Example: schema.PropertyExample([]string{".git", "node_modules"}),
|
||||
},
|
||||
{
|
||||
PropName: "max-size",
|
||||
Description: schema.MultiLine("Maximum size of the file to run the request on"),
|
||||
Example: schema.PropertyExamples("5Mb", "1Kb"),
|
||||
},
|
||||
{
|
||||
PropName: "archive",
|
||||
Description: schema.MultiLine("Process compressed archives without unpacking"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "mime-type",
|
||||
Description: schema.MultiLine("Filter files by mime-type"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "no-recursive",
|
||||
Description: schema.MultiLine("Do not recurse into folders when directories are provided"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the file request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(fileRequestMetadata, base)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package headless
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var headlessRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this headless request block"),
|
||||
Example: schema.PropertyExample("headless-login"),
|
||||
},
|
||||
{
|
||||
PropName: "steps",
|
||||
Description: schema.MultiLine(
|
||||
"List of browser actions to run for the headless request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"steps:",
|
||||
" - action: navigate",
|
||||
" args:",
|
||||
" url: \"{{BaseURL}}\"",
|
||||
" - action: waitload",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "user_agent",
|
||||
Description: schema.MultiLine(
|
||||
"Built-in user agent selection for the headless request",
|
||||
"Supported values depend on UserAgentHolder enums",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "custom_user_agent",
|
||||
Description: schema.MultiLine("Custom user agent string for the headless request"),
|
||||
Example: schema.PropertyExample("Mozilla/5.0 (compatible; Nuclei)"),
|
||||
},
|
||||
{
|
||||
PropName: "payloads",
|
||||
Description: schema.MultiLine("Payloads for the headless request"),
|
||||
},
|
||||
{
|
||||
PropName: "attack",
|
||||
Description: schema.MultiLine(
|
||||
"Payload combination strategy when payloads are defined",
|
||||
"Supported values: batteringram, pitchfork, clusterbomb",
|
||||
),
|
||||
Example: schema.PropertyExamples("batteringram", "pitchfork", "clusterbomb"),
|
||||
},
|
||||
{
|
||||
PropName: "fuzzing",
|
||||
Description: schema.MultiLine("Fuzzing/DAST rules that mutate headless requests"),
|
||||
},
|
||||
{
|
||||
PropName: "stop-at-first-match",
|
||||
Description: schema.MultiLine("Stop execution after the first match is found"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "cookie-reuse",
|
||||
Description: schema.MultiLine(
|
||||
"Deprecated: cookie reuse is the default behaviour; use disable-cookie to disable it",
|
||||
),
|
||||
Deprecated: true,
|
||||
},
|
||||
{
|
||||
PropName: "disable-cookie",
|
||||
Description: schema.MultiLine("Disable cookie reuse for this headless request"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
}
|
||||
|
||||
var headlessRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("steps"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the headless request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(headlessRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(headlessRequestAnyOfRequired, base)
|
||||
}
|
||||
+10
-11
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
|
||||
"github.com/projectdiscovery/rawhttp"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
@@ -255,23 +256,21 @@ type Request struct {
|
||||
GlobalMatchers bool `yaml:"global-matchers,omitempty" json:"global-matchers,omitempty" jsonschema:"title=global matchers,description=marks matchers as static and applies globally to all result events from other templates"`
|
||||
}
|
||||
|
||||
func (e Request) JSONSchemaExtend(schema *jsonschema.Schema) {
|
||||
headersSchema, ok := schema.Properties.Get("headers")
|
||||
func (e Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(httpRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(httpRequestAnyOfRequired, base)
|
||||
|
||||
// Headers accept string/int/bool values in templates.
|
||||
headersSchema, ok := base.Properties.Get("headers")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
headersSchema.PatternProperties = map[string]*jsonschema.Schema{
|
||||
".*": {
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Type: "boolean",
|
||||
},
|
||||
{Type: "string"},
|
||||
{Type: "integer"},
|
||||
{Type: "boolean"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,6 +62,15 @@ func GetSupportedHTTPMethodTypes() []HTTPMethodType {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetHTTPMethods returns supported HTTP method names for schema examples.
|
||||
func GetHTTPMethods() []interface{} {
|
||||
result := make([]interface{}, 0, limit-1)
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
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(
|
||||
[]string{"{{BaseURL}}"},
|
||||
[]string{"{{RootURL}}"},
|
||||
[]string{"{{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(
|
||||
[]string{"GET / HTTP/1.1\nHost: {{Hostname}}\n\n"},
|
||||
[]string{"POST / HTTP/1.1\nHost: {{Hostname}}\nContent-Type: application/json\nContent-Length: 13\n\n{\"key\": \"value\"}"},
|
||||
),
|
||||
},
|
||||
{
|
||||
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()...),
|
||||
Default: "GET",
|
||||
},
|
||||
{
|
||||
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(
|
||||
map[string]string{
|
||||
"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: "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: "pre-condition",
|
||||
Description: schema.MultiLine(
|
||||
"Pre-condition is only applicable for fuzzing/DAST requests and specifies whether fuzzing should run on this input",
|
||||
"It is a twin of matchers and can express pre-conditions that must be met before fuzzing",
|
||||
"Example: ",
|
||||
"```yaml",
|
||||
"- pre-condition:",
|
||||
"\t- type: dsl",
|
||||
"\t\tdsl:",
|
||||
"\t\t\t- 'len(query) > 0'",
|
||||
"```",
|
||||
"With this pre-condition only inputs that have query params will be fuzzed by this template",
|
||||
),
|
||||
Example: schema.PropertyExamples(
|
||||
// has query params
|
||||
[]map[string]any{{"type": "dsl", "dsl": []string{"len(query) > 0"}}},
|
||||
// has request body
|
||||
[]map[string]any{{"type": "dsl", "dsl": []string{"len(body) > 0"}}},
|
||||
// has post method
|
||||
[]map[string]any{{"type": "dsl", "dsl": []string{`method == "POST"`}}},
|
||||
// has post method and body
|
||||
[]map[string]any{{"type": "dsl", "dsl": []string{`method == "POST"`, "len(body) > 0"}, "condition": "and"}},
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "pre-condition-operator",
|
||||
Description: schema.MultiLine(
|
||||
"Pre-condition-operator specifies the condition used for combining multiple pre-conditions",
|
||||
"By default, it is OR",
|
||||
"Example: ",
|
||||
"```yaml",
|
||||
"- pre-condition-operator: 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(
|
||||
// inline payloads
|
||||
map[string]any{"username": []string{"admin", "root"}, "password": []string{"password", "admin"}},
|
||||
// file payloads
|
||||
map[string]any{"username": "/path/to/usernames.txt", "password": "/path/to/passwords.txt"},
|
||||
),
|
||||
},
|
||||
{
|
||||
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 concurrency 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: cookie reuse is the default behaviour; use disable-cookie to explicitly disable it",
|
||||
),
|
||||
Deprecated: true,
|
||||
},
|
||||
{
|
||||
PropName: "signature",
|
||||
Deprecated: true,
|
||||
},
|
||||
}
|
||||
|
||||
// httpRequestAnyOf required combinations
|
||||
var httpRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("path"),
|
||||
schema.Require("raw"),
|
||||
schema.Require("fuzzing"),
|
||||
// global matchers run on the results of other templates and send nothing
|
||||
schema.RequireConst("global-matchers", true),
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRequestJSONSchema(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Request{})
|
||||
requestSchema := reflected.Definitions["http.Request"]
|
||||
require.NotNil(t, requestSchema)
|
||||
|
||||
t.Run("accepts fuzzing-only requests", func(t *testing.T) {
|
||||
require.True(t, hasRequiredAlternative(requestSchema, "fuzzing"))
|
||||
})
|
||||
|
||||
t.Run("accepts global matcher requests only when enabled", func(t *testing.T) {
|
||||
require.True(t, hasRequiredAlternative(requestSchema, "global-matchers"))
|
||||
for _, alternative := range requestSchema.AnyOf {
|
||||
if slices.Equal([]string{"global-matchers"}, alternative.Required) {
|
||||
globalMatchers, ok := alternative.Properties.Get("global-matchers")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, true, globalMatchers.Const)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses typed examples for compound fields", func(t *testing.T) {
|
||||
for name, kind := range map[string]reflect.Kind{
|
||||
"path": reflect.Slice,
|
||||
"raw": reflect.Slice,
|
||||
"pre-condition": reflect.Slice,
|
||||
"payloads": reflect.Map,
|
||||
} {
|
||||
property, ok := requestSchema.Properties.Get(name)
|
||||
require.True(t, ok, name)
|
||||
require.NotEmpty(t, property.Examples, name)
|
||||
for _, example := range property.Examples {
|
||||
require.Equal(t, kind, reflect.TypeOf(example).Kind(), name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses mappings for header examples", func(t *testing.T) {
|
||||
headers, ok := requestSchema.Properties.Get("headers")
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, headers.Examples)
|
||||
for _, example := range headers.Examples {
|
||||
require.IsType(t, map[string]string{}, example)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func hasRequiredAlternative(schema *jsonschema.Schema, fields ...string) bool {
|
||||
for _, alternative := range schema.AnyOf {
|
||||
if slices.Equal(fields, alternative.Required) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var javascriptRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this javascript request block"),
|
||||
Example: schema.PropertyExample("js-probe"),
|
||||
},
|
||||
{
|
||||
PropName: "code",
|
||||
Description: schema.MultiLine(
|
||||
"Inline JavaScript code executed for the request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"code: |",
|
||||
" let m = require('nuclei/net');",
|
||||
" Export(m.OpenTCP(Host, Port));",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "init",
|
||||
Description: schema.MultiLine(
|
||||
"JavaScript code executed after compiling the template (before request execution)",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "args",
|
||||
Description: schema.MultiLine(
|
||||
"Arguments exposed to the JavaScript runtime",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"args:",
|
||||
" Host: \"{{Host}}\"",
|
||||
" Port: \"443\"",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "pre-condition",
|
||||
Description: schema.MultiLine(
|
||||
"JavaScript condition evaluated before executing the request",
|
||||
),
|
||||
Example: schema.PropertyExample("Port == 443"),
|
||||
},
|
||||
{
|
||||
PropName: "payloads",
|
||||
Description: schema.MultiLine("Payloads for the javascript request"),
|
||||
},
|
||||
{
|
||||
PropName: "attack",
|
||||
Description: schema.MultiLine(
|
||||
"Payload combination strategy when payloads are defined",
|
||||
"Supported values: batteringram, pitchfork, clusterbomb",
|
||||
),
|
||||
Example: schema.PropertyExamples("batteringram", "pitchfork", "clusterbomb"),
|
||||
},
|
||||
{
|
||||
PropName: "threads",
|
||||
Description: schema.MultiLine("Concurrency for sending javascript requests with payloads"),
|
||||
Example: schema.PropertyExample(10),
|
||||
},
|
||||
{
|
||||
PropName: "stop-at-first-match",
|
||||
Description: schema.MultiLine("Stop execution after the first match is found"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
}
|
||||
|
||||
var javascriptRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("code"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the javascript request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(javascriptRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(javascriptRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var networkRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine(
|
||||
"Optional ID of this TCP/network request block",
|
||||
"Useful when calling / executing this request block from flow",
|
||||
),
|
||||
Example: schema.PropertyExample("tcp-banner"),
|
||||
},
|
||||
{
|
||||
PropName: "host",
|
||||
Description: schema.MultiLine(
|
||||
"Host(s) to send network requests to",
|
||||
"Supports network helpers like `tls://` and port suffixes",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"host:",
|
||||
" - \"{{Hostname}}\"",
|
||||
" - \"tls://{{Hostname}}\"",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExamples([]string{"{{Hostname}}"}, []string{"tls://{{Hostname}}"}),
|
||||
},
|
||||
{
|
||||
PropName: "port",
|
||||
Description: schema.MultiLine(
|
||||
"Port to send network requests to",
|
||||
"Supports numeric ports and service names (ftp, ssh, smtp)",
|
||||
),
|
||||
Example: schema.PropertyExamples("80", "443", "ssh"),
|
||||
},
|
||||
{
|
||||
PropName: "exclude-ports",
|
||||
Description: schema.MultiLine("Ports to exclude from being scanned"),
|
||||
Example: schema.PropertyExample("22,25"),
|
||||
},
|
||||
{
|
||||
PropName: "inputs",
|
||||
Description: schema.MultiLine(
|
||||
"Input/output steps for the network request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"inputs:",
|
||||
" - data: \"HELP\\r\\n\"",
|
||||
" read: 1024",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "read-size",
|
||||
Description: schema.MultiLine("Size of response to read at the end. Default is 1024 bytes"),
|
||||
Example: schema.PropertyExample(1024),
|
||||
Default: 1024,
|
||||
},
|
||||
{
|
||||
PropName: "read-all",
|
||||
Description: schema.MultiLine("Read the entire response stream until the server stops sending"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "stop-at-first-match",
|
||||
Description: schema.MultiLine("Stop execution after the first match is found"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "payloads",
|
||||
Description: schema.MultiLine("Payloads for the network request"),
|
||||
},
|
||||
{
|
||||
PropName: "attack",
|
||||
Description: schema.MultiLine(
|
||||
"Payload combination strategy when payloads are defined",
|
||||
"Supported values: batteringram, pitchfork, clusterbomb",
|
||||
),
|
||||
Example: schema.PropertyExamples("batteringram", "pitchfork", "clusterbomb"),
|
||||
},
|
||||
{
|
||||
PropName: "threads",
|
||||
Description: schema.MultiLine("Concurrency for sending network requests with payloads"),
|
||||
Example: schema.PropertyExample(10),
|
||||
},
|
||||
}
|
||||
|
||||
var networkRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("host"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the network/TCP request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(networkRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(networkRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRequestJSONSchemaRequiresHost(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Request{})
|
||||
requestSchema := reflected.Definitions["network.Request"]
|
||||
require.NotNil(t, requestSchema)
|
||||
require.Len(t, requestSchema.AnyOf, 1)
|
||||
require.Equal(t, []string{"host"}, requestSchema.AnyOf[0].Required)
|
||||
}
|
||||
|
||||
func TestRequestJSONSchemaHostExamplesAreArrays(t *testing.T) {
|
||||
reflected := (&jsonschema.Reflector{
|
||||
Namer: func(t reflect.Type) string {
|
||||
return t.String()
|
||||
},
|
||||
}).Reflect(&Request{})
|
||||
requestSchema := reflected.Definitions["network.Request"]
|
||||
require.NotNil(t, requestSchema)
|
||||
host, ok := requestSchema.Properties.Get("host")
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, host.Examples)
|
||||
for _, example := range host.Examples {
|
||||
require.IsType(t, []string{}, example)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ssl
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var sslRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this SSL request block"),
|
||||
Example: schema.PropertyExample("ssl-cert"),
|
||||
},
|
||||
{
|
||||
PropName: "address",
|
||||
Description: schema.MultiLine(
|
||||
"Address for the SSL request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"address: \"{{Host}}:{{Port}}\"",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExamples("{{Host}}:{{Port}}", "{{Hostname}}"),
|
||||
},
|
||||
{
|
||||
PropName: "min_version",
|
||||
Description: schema.MultiLine(
|
||||
"Minimum TLS version",
|
||||
"Supported values: sslv3, tls10, tls11, tls12, tls13",
|
||||
),
|
||||
Example: schema.PropertyExamples("tls12", "tls13"),
|
||||
},
|
||||
{
|
||||
PropName: "max_version",
|
||||
Description: schema.MultiLine(
|
||||
"Maximum TLS version",
|
||||
"Supported values: sslv3, tls10, tls11, tls12, tls13",
|
||||
),
|
||||
Example: schema.PropertyExamples("tls12", "tls13"),
|
||||
},
|
||||
{
|
||||
PropName: "cipher_suites",
|
||||
Description: schema.MultiLine("Explicit cipher suites to negotiate"),
|
||||
},
|
||||
{
|
||||
PropName: "scan_mode",
|
||||
Description: schema.MultiLine(
|
||||
"Scan mode for SSL enumeration",
|
||||
"Supported values: ctls, ztls, auto",
|
||||
),
|
||||
Example: schema.PropertyExamples("auto", "ctls", "ztls"),
|
||||
Default: "auto",
|
||||
},
|
||||
{
|
||||
PropName: "tls_version_enum",
|
||||
Description: schema.MultiLine("Enumerate supported TLS versions"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "tls_cipher_enum",
|
||||
Description: schema.MultiLine("Enumerate supported TLS ciphers"),
|
||||
Example: schema.PropertyExample(true),
|
||||
},
|
||||
{
|
||||
PropName: "tls_cipher_types",
|
||||
Description: schema.MultiLine(
|
||||
"TLS cipher types to enumerate",
|
||||
"Supported values: weak, secure, insecure, all",
|
||||
),
|
||||
Example: schema.PropertyExamples("weak", "secure", "all"),
|
||||
},
|
||||
}
|
||||
|
||||
var sslRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("address"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the SSL request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(sslRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(sslRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var websocketRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this websocket request block"),
|
||||
Example: schema.PropertyExample("ws-echo"),
|
||||
},
|
||||
{
|
||||
PropName: "address",
|
||||
Description: schema.MultiLine(
|
||||
"Websocket address for the request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"address: \"{{Hostname}}\"",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExample("{{Hostname}}"),
|
||||
},
|
||||
{
|
||||
PropName: "inputs",
|
||||
Description: schema.MultiLine(
|
||||
"Input messages to send over the websocket",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"inputs:",
|
||||
" - data: \"ping\"",
|
||||
" name: ping",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "headers",
|
||||
Description: schema.MultiLine("Headers for the websocket handshake request"),
|
||||
Example: schema.PropertyExample(map[string]string{
|
||||
"Origin": "https://example.com",
|
||||
}),
|
||||
},
|
||||
{
|
||||
PropName: "payloads",
|
||||
Description: schema.MultiLine("Payloads for the websocket request"),
|
||||
},
|
||||
{
|
||||
PropName: "attack",
|
||||
Description: schema.MultiLine(
|
||||
"Payload combination strategy when payloads are defined",
|
||||
"Supported values: batteringram, pitchfork, clusterbomb",
|
||||
),
|
||||
Example: schema.PropertyExamples("batteringram", "pitchfork", "clusterbomb"),
|
||||
},
|
||||
}
|
||||
|
||||
var websocketRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("address"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the websocket request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(websocketRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(websocketRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package whois
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var whoisRequestMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "id",
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Optional ID of this WHOIS request block"),
|
||||
Example: schema.PropertyExample("whois-lookup"),
|
||||
},
|
||||
{
|
||||
PropName: "query",
|
||||
Description: schema.MultiLine(
|
||||
"Query for the WHOIS request",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"query: \"{{FQDN}}\"",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExamples("{{FQDN}}", "example.com"),
|
||||
},
|
||||
{
|
||||
PropName: "server",
|
||||
Description: schema.MultiLine(
|
||||
"Optional WHOIS server URL to execute the request on",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"server: \"whois.verisign-grs.com:43\"",
|
||||
"```",
|
||||
),
|
||||
Example: schema.PropertyExample("whois.verisign-grs.com:43"),
|
||||
},
|
||||
}
|
||||
|
||||
var whoisRequestAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("query"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the WHOIS request JSON schema.
|
||||
func (Request) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(whoisRequestMetadata, base)
|
||||
schema.ApplyAnyOfRequired(whoisRequestAnyOfRequired, base)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/workflows"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
@@ -171,6 +173,12 @@ type Template struct {
|
||||
importedFileContentsCaptured bool
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the Template JSON schema with richer docs, examples, and required combinations.
|
||||
func (template Template) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(templateMetadata, base)
|
||||
schema.ApplyAnyOfRequired(templateAnyOfRequired, base)
|
||||
}
|
||||
|
||||
// HasCodeProtocol returns true if the template has a code protocol section
|
||||
//
|
||||
// Deprecated: Use [HasCodeRequest] instead.
|
||||
|
||||
@@ -469,6 +469,10 @@ func init() {
|
||||
Key: "cipher",
|
||||
Value: "TLS cipher suite negotiated for the HTTP connection",
|
||||
},
|
||||
{
|
||||
Key: "key_exchange",
|
||||
Value: "TLS key exchange group negotiated for the HTTP connection",
|
||||
},
|
||||
{
|
||||
Key: "sni",
|
||||
Value: "SNI value used in the TLS handshake",
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
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: "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",
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "dns",
|
||||
Description: schema.MultiLine(
|
||||
"DNS protocol block for sending DNS queries",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"dns:",
|
||||
" - name: \"{{FQDN}}\"",
|
||||
" type: A",
|
||||
" matchers:",
|
||||
" - type: word",
|
||||
" words:",
|
||||
" - \"IN\\tA\"",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "tcp",
|
||||
Description: schema.MultiLine(
|
||||
"TCP/network protocol block for raw socket requests",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"tcp:",
|
||||
" - host:",
|
||||
" - \"{{Hostname}}\"",
|
||||
" inputs:",
|
||||
" - data: \"\\n\"",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "network",
|
||||
Deprecated: true,
|
||||
Description: schema.MultiLine(
|
||||
"Deprecated: use `tcp` instead of `network`",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "file",
|
||||
Description: schema.MultiLine(
|
||||
"File protocol block for matching against local files",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"file:",
|
||||
" - extensions:",
|
||||
" - all",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "headless",
|
||||
Description: schema.MultiLine(
|
||||
"Headless protocol block for browser automation",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"headless:",
|
||||
" - steps:",
|
||||
" - action: navigate",
|
||||
" args:",
|
||||
" url: \"{{BaseURL}}\"",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "ssl",
|
||||
Description: schema.MultiLine(
|
||||
"SSL protocol block for TLS certificate / cipher checks",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"ssl:",
|
||||
" - address: \"{{Host}}:{{Port}}\"",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "websocket",
|
||||
Description: schema.MultiLine(
|
||||
"Websocket protocol block for websocket requests",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "whois",
|
||||
Description: schema.MultiLine(
|
||||
"WHOIS protocol block for domain/IP WHOIS lookups",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"whois:",
|
||||
" - query: \"{{FQDN}}\"",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "code",
|
||||
Description: schema.MultiLine(
|
||||
"Code protocol block for executing external engines (python, sh, etc.)",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"code:",
|
||||
" - engine:",
|
||||
" - py",
|
||||
" source: |",
|
||||
" print(\"hello\")",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "javascript",
|
||||
Description: schema.MultiLine(
|
||||
"JavaScript protocol block for nuclei JS runtime requests",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"javascript:",
|
||||
" - code: |",
|
||||
" Export(Host)",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "workflows",
|
||||
Description: schema.MultiLine(
|
||||
"Workflows define multi-template execution graphs",
|
||||
"A workflow template usually only needs `id`, `info`, and `workflows`",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"workflows:",
|
||||
" - template: http/technologies/",
|
||||
" - tags: xss",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
// 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.RequireBase([]string{"id", "info"},
|
||||
schema.Require("workflows"),
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Package schema implements helper types & functions for generating better json schema.
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
sliceutil "github.com/projectdiscovery/utils/slice"
|
||||
)
|
||||
|
||||
// PropertyMetadata is 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 := make([]interface{}, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value != nil {
|
||||
examples = append(examples, value)
|
||||
}
|
||||
}
|
||||
return examples
|
||||
}
|
||||
|
||||
// PropertyExample returns a single-item examples list for a property.
|
||||
func PropertyExample(value any) []interface{} {
|
||||
return []interface{}{value}
|
||||
}
|
||||
|
||||
// MultiLine joins description lines with newlines.
|
||||
func MultiLine(values ...string) string {
|
||||
return strings.Join(values, "\n")
|
||||
}
|
||||
|
||||
// ExtendSchema extends the schema with the metadata.
|
||||
// This patches or adds additional information to existing properties.
|
||||
func ExtendSchema(metadata []PropertyMetadata, base *jsonschema.Schema) {
|
||||
for _, meta := range metadata {
|
||||
prop, ok := base.Properties.Get(meta.PropName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(meta.OneOf) > 0 {
|
||||
// Preserve array item constraints from the original property so
|
||||
// string-array branches (e.g. info.author / info.tags) still reject
|
||||
// non-string elements after we clear the parent Ref/Type.
|
||||
originalItems := prop.Items
|
||||
prop.OneOf = nil
|
||||
for _, oneOf := range meta.OneOf {
|
||||
branch := &jsonschema.Schema{
|
||||
Type: oneOf.PropType,
|
||||
Description: oneOf.Description,
|
||||
Examples: oneOf.Example,
|
||||
Default: oneOf.Default,
|
||||
}
|
||||
if oneOf.PropType == "array" {
|
||||
if originalItems != nil {
|
||||
branch.Items = originalItems
|
||||
} else {
|
||||
branch.Items = &jsonschema.Schema{Type: "string"}
|
||||
}
|
||||
}
|
||||
prop.OneOf = append(prop.OneOf, branch)
|
||||
}
|
||||
prop.Ref = ""
|
||||
prop.Type = ""
|
||||
} else {
|
||||
if meta.PropType != "" {
|
||||
prop.Type = meta.PropType
|
||||
}
|
||||
if meta.Description != "" {
|
||||
prop.Description = meta.Description
|
||||
}
|
||||
if len(meta.Example) > 0 {
|
||||
prop.Examples = dedupeExamples(append(prop.Examples, meta.Example...))
|
||||
}
|
||||
if meta.Default != nil {
|
||||
prop.Default = meta.Default
|
||||
}
|
||||
}
|
||||
prop.Examples = purgeNil(prop.Examples)
|
||||
if meta.RemoveRef {
|
||||
prop.Ref = ""
|
||||
}
|
||||
prop.Deprecated = meta.Deprecated
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredCombos is a list of required field combinations.
|
||||
// At least one combination must be satisfied.
|
||||
type RequiredCombos struct {
|
||||
RequireBase []string
|
||||
Require []string
|
||||
required []RequiredCombos
|
||||
// constField and constValue, when set, bind this combination to a fixed value of a field.
|
||||
constField string
|
||||
constValue any
|
||||
}
|
||||
|
||||
// RequireBase builds combinations of a shared base with each nested require set.
|
||||
func RequireBase(base []string, requires ...RequiredCombos) RequiredCombos {
|
||||
return RequiredCombos{RequireBase: base, required: requires}
|
||||
}
|
||||
|
||||
// Require builds a single required-fields combination.
|
||||
func Require(require ...string) RequiredCombos {
|
||||
return RequiredCombos{Require: require}
|
||||
}
|
||||
|
||||
// RequireConst builds a combination that requires field to equal value and
|
||||
// the other fields to be present. It binds required fields to a discriminator
|
||||
// such as a matcher type, or requires a boolean flag to be true.
|
||||
func RequireConst(field string, value any, require ...string) RequiredCombos {
|
||||
return RequiredCombos{
|
||||
Require: append([]string{field}, require...),
|
||||
constField: field,
|
||||
constValue: value,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyAnyOfRequired applies anyOf 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 {
|
||||
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 {
|
||||
combo := &jsonschema.Schema{
|
||||
Required: anyOf.Require,
|
||||
}
|
||||
if anyOf.constField != "" {
|
||||
combo.Properties = jsonschema.NewProperties()
|
||||
combo.Properties.Set(anyOf.constField, &jsonschema.Schema{Const: anyOf.constValue})
|
||||
}
|
||||
base.AnyOf = append(base.AnyOf, combo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func purgeNil(s []any) []any {
|
||||
var r []any
|
||||
for _, i := range s {
|
||||
if i != nil {
|
||||
r = append(r, i)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func dedupeExamples(s []any) []any {
|
||||
seen := make(map[string]struct{}, len(s))
|
||||
var out []any
|
||||
for _, item := range s {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%#v", item)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/schema"
|
||||
)
|
||||
|
||||
var workflowMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "workflows",
|
||||
Description: schema.MultiLine(
|
||||
"List of workflow templates / tags to execute",
|
||||
"Example:",
|
||||
"```yaml",
|
||||
"workflows:",
|
||||
" - template: http/technologies/",
|
||||
" - tags: xss",
|
||||
" subtemplates:",
|
||||
" - tags: xss",
|
||||
"```",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
var workflowTemplateMetadata = []schema.PropertyMetadata{
|
||||
{
|
||||
PropName: "template",
|
||||
Description: schema.MultiLine(
|
||||
"Template file or directory to execute as part of the workflow",
|
||||
),
|
||||
Example: schema.PropertyExamples("http/cves/", "network/detection/"),
|
||||
},
|
||||
{
|
||||
PropName: "tags",
|
||||
RemoveRef: true,
|
||||
OneOf: []*schema.PropertyMetadata{
|
||||
{
|
||||
PropType: "string",
|
||||
Description: schema.MultiLine("Comma-separated tags used to select templates"),
|
||||
Example: schema.PropertyExample("cve,xss"),
|
||||
},
|
||||
{
|
||||
PropType: "array",
|
||||
Description: schema.MultiLine("List of tags used to select templates"),
|
||||
Example: schema.PropertyExample([]string{"cve", "xss"}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PropName: "matchers",
|
||||
Description: schema.MultiLine(
|
||||
"Name-based matchers that decide which subtemplates to run",
|
||||
),
|
||||
},
|
||||
{
|
||||
PropName: "subtemplates",
|
||||
Description: schema.MultiLine(
|
||||
"Nested workflow entries ran when the parent template/tag matches",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
var workflowTemplateAnyOfRequired = []schema.RequiredCombos{
|
||||
schema.Require("template"),
|
||||
schema.Require("tags"),
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the Workflow JSON schema.
|
||||
func (Workflow) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(workflowMetadata, base)
|
||||
}
|
||||
|
||||
// JSONSchemaExtend extends the WorkflowTemplate JSON schema.
|
||||
func (WorkflowTemplate) JSONSchemaExtend(base *jsonschema.Schema) {
|
||||
schema.ExtendSchema(workflowTemplateMetadata, base)
|
||||
schema.ApplyAnyOfRequired(workflowTemplateAnyOfRequired, base)
|
||||
}
|
||||
Reference in new issue
Block a user