Files
nuclei/pkg/loader/workflow/workflow_loader_test.go
Dwi Siswanto c2cecb92a8 fix(workflow): skip -es for steps whose subtemplates loaded
A step whose subtemplates loaded applied every exclusion, -es included.
Most workflows start from a detection template with info severity, so
-es info, often used to hide low-value results, left them running
nothing, while -s low,medium,high,critical, which selects the same
results, still ran them. A step with subtemplates reports no results of
its own beyond late out-of-band matches, so excluding its severity
mostly keeps the checks below it from running. Such a step now skips
-es as it skips -s.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-02 18:33:15 +07:00

156 lines
6.0 KiB
Go

package workflow
import (
"maps"
"slices"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"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"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/dns"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
"github.com/projectdiscovery/nuclei/v3/pkg/workflows"
"github.com/stretchr/testify/require"
)
type countingCatalog struct {
catalog.Catalog
paths []string
walks int
}
func (c *countingCatalog) GetTemplatesPath([]string) ([]string, map[string]error) {
c.walks++
return c.paths, nil
}
type countingParser struct {
templates map[string]*templates.Template
parses map[string]int
}
func (p *countingParser) ParseTemplate(path string, _ catalog.Catalog) (any, error) {
p.parses[path]++
return p.templates[path], nil
}
func (p *countingParser) LoadTemplate(string, any, []string, catalog.Catalog) (bool, error) {
panic("tag lookups must not reparse through LoadTemplate")
}
func (p *countingParser) LoadWorkflow(string, catalog.Catalog) (bool, error) {
return false, nil
}
func TestGetTemplatePathsByTagsReadsTemplatesDirectoryOnce(t *testing.T) {
template := func(id, tags string) *templates.Template {
return &templates.Template{ID: id, Info: model.Info{
Name: id,
Authors: stringslice.StringSlice{Value: "author"},
Tags: stringslice.StringSlice{Value: tags},
SeverityHolder: severity.Holder{Severity: severity.Info},
}}
}
parser := &countingParser{
templates: map[string]*templates.Template{
"wordpress.yaml": template("wordpress", "wordpress"),
"jira.yaml": template("jira", "jira"),
},
parses: make(map[string]int),
}
cat := &countingCatalog{paths: []string{"wordpress.yaml", "jira.yaml"}}
loader, err := NewLoader(&protocols.ExecutorOptions{Options: types.DefaultOptions(), Catalog: cat, Parser: parser})
require.NoError(t, err)
walksBeforeLookups := cat.walks
require.Equal(t, []string{"wordpress.yaml"}, loader.GetTemplatePathsByTags([]string{"wordpress"}, false))
require.Equal(t, []string{"jira.yaml"}, loader.GetTemplatePathsByTags([]string{"jira"}, false))
require.Equal(t, 1, cat.walks-walksBeforeLookups, "templates directory walked per lookup")
require.Equal(t, map[string]int{"wordpress.yaml": 1, "jira.yaml": 1}, parser.parses, "templates parsed per lookup")
}
func TestGetTemplatePathsByTagsNoValidateAppliesExclusionsButSeverity(t *testing.T) {
template := func(id string, tags ...string) *templates.Template {
return &templates.Template{ID: id, Info: model.Info{
Name: id,
Authors: stringslice.StringSlice{Value: "author"},
Tags: stringslice.StringSlice{Value: tags},
SeverityHolder: severity.Holder{Severity: severity.Info},
}}
}
excludedSeverity := template("excluded-severity", "step")
excludedSeverity.Info.SeverityHolder.Severity = severity.Low
excludedProtocol := template("excluded-protocol", "step")
excludedProtocol.RequestsDNS = []*dns.Request{{}}
workflow := template("workflow", "step")
workflow.Workflows = []*workflows.WorkflowTemplate{{Template: "selected.yaml"}}
parser := &countingParser{
templates: map[string]*templates.Template{
"selected.yaml": template("selected", "step"),
"included.yaml": template("included", "step", "included"),
"excluded-tag.yaml": template("excluded-tag", "step", "excluded"),
"excluded-id.yaml": template("excluded-id", "step"),
"excluded-severity.yaml": excludedSeverity,
"excluded-protocol.yaml": excludedProtocol,
"untagged.yaml": template("untagged", "other"),
"workflow.yaml": workflow,
},
parses: make(map[string]int),
}
options := types.DefaultOptions()
options.IncludeIds = []string{"untagged"}
options.ExcludeTags = []string{"excluded", "included"}
options.IncludeTags = []string{"included"}
options.ExcludeIds = []string{"excluded-id"}
options.ExcludeSeverities = severity.Severities{severity.Low}
options.ExcludeProtocols = templateTypes.ProtocolTypes{templateTypes.DNSProtocol}
cat := &countingCatalog{paths: slices.Collect(maps.Keys(parser.templates))}
loader, err := NewLoader(&protocols.ExecutorOptions{Options: options, Catalog: cat, Parser: parser})
require.NoError(t, err)
require.Empty(t, loader.GetTemplatePathsByTags([]string{"step"}, false))
loaded := loader.GetTemplatePathsByTags([]string{"step"}, true)
require.ElementsMatch(t, []string{"selected.yaml", "included.yaml", "excluded-severity.yaml"}, loaded)
}
func TestGetTemplatePathsByTagsCountsSyntaxWarningsOfLoadedTemplates(t *testing.T) {
// a template without severity has a syntax warning
unrated := &templates.Template{ID: "unrated", Info: model.Info{
Name: "unrated",
Authors: stringslice.StringSlice{Value: "author"},
Tags: stringslice.StringSlice{Value: "step"},
}}
tests := []struct {
name string
ids []string
warnings int64
}{
{name: "loaded", warnings: 1},
{name: "filtered out", ids: []string{"other"}, warnings: 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
parser := &countingParser{
templates: map[string]*templates.Template{"unrated.yaml": unrated},
parses: make(map[string]int),
}
options := types.DefaultOptions()
options.IncludeIds = tc.ids
cat := &countingCatalog{paths: []string{"unrated.yaml"}}
loader, err := NewLoader(&protocols.ExecutorOptions{Options: options, Catalog: cat, Parser: parser})
require.NoError(t, err)
before := stats.GetValue(templates.TemplateSyntaxWarningStats)
loader.GetTemplatePathsByTags([]string{"step"}, false)
require.Equal(t, tc.warnings, stats.GetValue(templates.TemplateSyntaxWarningStats)-before)
})
}
}