Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pkg/catalog/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"net/url"
"os"
"slices"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -786,6 +787,26 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
}
}

// noteExcludedByTag surfaces a template the index filter dropped because it
// carries a tag from the .nuclei-ignore defaults. Without it the exclusion is
// silent at every verbosity level.
//
// indexFilter.ExcludeTags is a merge of CLI -exclude-tags and the ignore-file
// tags, so it must be matched against the ignore-file tags specifically:
// otherwise user-requested -exclude-tags drops would be mislabeled as
// .nuclei-ignore exclusions.
ignoreFileTags := config.ReadIgnoreFile().Tags
noteExcludedByTag := func(templatePath string, metadata *index.Metadata) {
if len(ignoreFileTags) == 0 || !slices.ContainsFunc(ignoreFileTags, metadata.HasTag) {
return
}

stats.Increment(templates.TemplatesExcludedStats)
if config.DefaultConfig.LogAllEvents {
store.logger.Print().Msgf("[%v] %v excluded from default run using .nuclei-ignore\n", aurora.Yellow("WRN").String(), templatePath)
}
}
Comment on lines +790 to +808

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for where options.ExcludeTags is set, particularly checking if .nuclei-ignore
# tags are merged with CLI tags or kept separate

# Find where ExcludeTags field is assigned
rg -n -B3 -A3 '\.ExcludeTags\s*='

# Find references to .nuclei-ignore file loading
rg -n -B3 -A3 'nuclei-ignore'

# Check if there's a separate field or filtering for .nuclei-ignore vs CLI tags
ast-grep --pattern $'struct $_ {
  $$$
  ExcludeTags $$$
  $$$
}'

Repository: projectdiscovery/nuclei

Length of output: 9432


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect loader.go around noteExcludedByTag and how indexFilter is built
echo "=== loader.go context (lines ~740-850) ==="
sed -n '740,850p' pkg/catalog/loader/loader.go | nl -ba

# 2) Find where indexFilter.ExcludeTags is assigned in loader.go
echo
echo "=== indexFilter.ExcludeTags assignments in loader.go ==="
rg -n 'indexFilter\.ExcludeTags\s*=' pkg/catalog/loader/loader.go

# 3) Trace how CLI `-exclude-tags` maps into filters.ExcludeTags / options.ExcludeTags
echo
echo "=== CLI flag parsing for exclude-tags ==="
rg -n --hidden --no-ignore-vcs 'exclude-?tags' .

# 4) Locate filters.ExcludeTags source
echo
echo "=== assignments to filters.ExcludeTags ==="
rg -n 'filters\.ExcludeTags\s*=' .

# 5) Locate flag definition (if any) for ExcludeTags and its destination
echo
echo "=== struct fields or options related to ExcludeTags ==="
rg -n 'ExcludeTags\s+(\[\]string|string)' .

Repository: projectdiscovery/nuclei

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== loader.go (around noteExcludedByTag) ==="
awk 'NR>=770 && NR<=820 {print NR ":" $0}' pkg/catalog/loader/loader.go

echo
echo "=== indexFilter.ExcludeTags assignments (loader.go) ==="
rg -n 'indexFilter\.ExcludeTags\s*=' pkg/catalog/loader/loader.go

echo
echo "=== flag / parsing for exclude-tags ==="
rg -n --hidden --no-ignore-vcs 'exclude-?tags' .

echo
echo "=== assignments into filters.ExcludeTags ==="
rg -n --hidden --no-ignore-vcs 'filters\.ExcludeTags\s*=' .

echo
echo "=== places where config.ReadIgnoreFile().Tags is appended to exclude tags ==="
rg -n --hidden --no-ignore-vcs 'ReadIgnoreFile\(\)\.Tags' .

Repository: projectdiscovery/nuclei

Length of output: 2069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where indexFilter is defined/initialized in loader.go
echo "=== indexFilter occurrences in loader.go ==="
rg -n 'indexFilter' pkg/catalog/loader/loader.go

echo
echo "=== context around indexFilter setup/initialization (approx 650-780) ==="
awk 'NR>=600 && NR<=820 {print NR ":" $0}' pkg/catalog/loader/loader.go

# 2) Trace ExcludeTags assignment into indexFilter in the whole repo (not just loader.go)
echo
echo "=== assignments to *.ExcludeTags used for index filtering ==="
rg -n 'indexFilter\.(ExcludeTags|IncludeTags)\s*=' . || true

# 3) Find CLI flag / option mapping for exclude-tags
echo
echo "=== places defining exclude-tags flag ==="
rg -n --hidden --no-ignore-vcs 'exclude-?tags' .

# 4) Find where filters.ExcludeTags is populated from CLI/options
echo
echo "=== filters.ExcludeTags population ==="
rg -n --hidden --no-ignore-vcs 'filters\.ExcludeTags\s*[:=]' .

echo
echo "=== structs with ExcludeTags field definitions (limit to relevant hits) ==="
rg -n --hidden --no-ignore-vcs 'ExcludeTags\s+(\[\]string|[]string)' .

# 5) Narrow to command-line/options parsing for exclude tags if flag exists
# (helps connect CLI -> filters.ExcludeTags -> options.ExcludeTags -> store.config -> indexFilter)
echo
echo "=== search for '-exclude-tags' / 'ExcludeTags' in cmd/ or main package ==="
rg -n --hidden --no-ignore-vcs 'ExcludeTags' cmd . | head -n 200 || true

Repository: projectdiscovery/nuclei

Length of output: 11225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== buildIndexFilter in loader.go ==="
rg -n 'buildIndexFilter' pkg/catalog/loader/loader.go
awk 'NR>=130 && NR<=260 {print NR ":" $0}' pkg/catalog/loader/loader.go

echo
echo "=== store.buildIndexFilter definition (full function if not too large) ==="
# Print exact function body by extracting from "func (store *Store) buildIndexFilter" to next "func"
start=$(rg -n 'func \(store \*Store\) buildIndexFilter' -n pkg/catalog/loader/loader.go | head -n1 | cut -d: -f1)
awk -v s="$start" 'NR>=s {print} NR>s && /^func / {exit}' pkg/catalog/loader/loader.go || true

echo
echo "=== index.Filter struct and ExcludeTags usage ==="
rg -n 'type Filter|ExcludeTags|HasTag|Matches\(' pkg/catalog/index/filter.go

echo
echo "=== index/filter.go around ExcludeTags handling ==="
awk 'NR>=300 && NR<=480 {print NR ":" $0}' pkg/catalog/index/filter.go

echo
echo "=== index/filter.go around struct definition ==="
awk 'NR>=1 && NR<=120 {print NR ":" $0}' pkg/catalog/index/filter.go

echo
echo "=== where config.ReadIgnoreFile().Tags gets appended into ExcludeTags ==="
rg -n 'ReadIgnoreFile\(\)\.Tags' -S .

Repository: projectdiscovery/nuclei

Length of output: 13621


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== where options.ExcludeTags is used ==="
rg -n --hidden --no-ignore-vcs 'options\.ExcludeTags' .

echo
echo "=== where filters.ExcludeTags is filled from options ==="
rg -n --hidden --no-ignore-vcs 'filters\.ExcludeTags' lib pkg cmd internal .

echo
echo "=== trace store.config.ExcludeTags assignments ==="
rg -n --hidden --no-ignore-vcs 'ExcludeTags:\s*' lib pkg cmd internal .

echo
echo "=== inspect config/filter struct around ExcludeTags mapping ==="
# show relevant parts of lib/config.go
awk 'NR>=540 && NR<=660 {print NR ":" $0}' lib/config.go

Repository: projectdiscovery/nuclei

Length of output: 8066


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== pkg/catalog/loader/loader.go around ExcludeTags assignment from options (~90-150) ==="
awk 'NR>=70 && NR<=160 {print NR ":" $0}' pkg/catalog/loader/loader.go

echo
echo "=== pkg/catalog/loader/loader.go call sites of noteExcludedByTag (~820-900) ==="
# noteExcludedByTag likely used shortly after load filtering loops
awk 'NR>=800 && NR<=920 {print NR ":" $0}' pkg/catalog/loader/loader.go

echo
echo "=== pkg/types/types.go around ExcludeTags wiring (~450-530) ==="
awk 'NR>=450 && NR<=530 {print NR ":" $0}' pkg/types/types.go

Repository: projectdiscovery/nuclei

Length of output: 13223


Fix .nuclei-ignore exclusion stats/log to only count ignore-file tags

noteExcludedByTag (pkg/catalog/loader/loader.go:790-802) increments templates.TemplatesExcludedStats and logs “excluded from default run using .nuclei-ignore” whenever a template matches indexFilter.ExcludeTags, but indexFilter.ExcludeTags is built from store.config.ExcludeTags (store.buildIndexFilter → ExcludeTags: store.config.ExcludeTags), which is populated from options.ExcludeTags (CLI -exclude-tags) and also appended with config.ReadIgnoreFile().Tags (lib/sdk_private.go:140). There’s no check that the matched exclude tag originated from .nuclei-ignore, so CLI -exclude-tags will be misattributed/counted as .nuclei-ignore exclusions.

Separate ignore-file exclude tags from CLI exclude tags in the filter logic (e.g., check intersection only with config.ReadIgnoreFile().Tags) before incrementing/logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/catalog/loader/loader.go` around lines 790 - 802, The noteExcludedByTag
closure currently treats any tag in indexFilter.ExcludeTags as coming from
.nuclei-ignore; change it to only increment templates.TemplatesExcludedStats and
log the “excluded from default run using .nuclei-ignore” message when the
matching exclude tag actually came from the ignore file: obtain the ignore-file
tags (e.g., via config.ReadIgnoreFile().Tags or a new store field like
ignoreFileTags) and replace the slices.ContainsFunc check against
indexFilter.ExcludeTags with a check against that ignore-file tag set (or check
intersection between metadata.HasTag and ignore-file tags) so CLI-provided
exclude tags (store.config.ExcludeTags / options.ExcludeTags) are not counted or
logged as .nuclei-ignore exclusions; keep all other behavior (stats/logging)
unchanged and only gate them on the ignore-file tag match.


typesOpts := store.config.ExecutorOptions.Options
concurrency := typesOpts.TemplateLoadingConcurrency
if concurrency <= 0 {
Expand Down Expand Up @@ -820,6 +841,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
if cachedMetadata, found := store.metadataIndex.Get(templatePath); found {
metadata = cachedMetadata
if !indexFilter.Matches(metadata) {
noteExcludedByTag(templatePath, metadata)
return
}
// NOTE(dwisiswant0): else, tagFilter probably exists (for
Expand All @@ -842,6 +864,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
}

if metadata != nil && !indexFilter.Matches(metadata) {
noteExcludedByTag(templatePath, metadata)
return
}
}
Expand Down
21 changes: 20 additions & 1 deletion pkg/protocols/headless/engine/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -287,8 +288,26 @@ func (p *Page) addInteractshURL(URLs ...string) {
p.InteractshURLs = append(p.InteractshURLs, URLs...)
}

// appendRule records a request/response modification rule. The hijack handler
// reads p.rules from a separate goroutine, so writes must be synchronized.
func (p *Page) appendRule(r rule) {
p.mutex.Lock()
defer p.mutex.Unlock()

p.rules = append(p.rules, r)
}

// rulesSnapshot returns a copy of the current rules for lock-free iteration by
// the hijack handler (which performs network I/O and must not hold the lock).
func (p *Page) rulesSnapshot() []rule {
p.mutex.RLock()
defer p.mutex.RUnlock()

return slices.Clone(p.rules)
}

func (p *Page) hasModificationRules() bool {
for _, rule := range p.rules {
for _, rule := range p.rulesSnapshot() {
if containsAnyModificationActionType(rule.Action) {
return true
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/protocols/headless/engine/page_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func (p *Page) ActionAddHeader(act *Action, out ActionData) error {
return err
}

p.rules = append(p.rules, rule{
p.appendRule(rule{
Action: ActionAddHeader,
Part: part,
Args: args,
Expand Down Expand Up @@ -314,7 +314,7 @@ func (p *Page) ActionSetHeader(act *Action, out ActionData) error {
return err
}

p.rules = append(p.rules, rule{
p.appendRule(rule{
Action: ActionSetHeader,
Part: part,
Args: args,
Expand All @@ -337,7 +337,7 @@ func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error {
return err
}

p.rules = append(p.rules, rule{
p.appendRule(rule{
Action: ActionDeleteHeader,
Part: part,
Args: args,
Expand All @@ -360,7 +360,7 @@ func (p *Page) ActionSetBody(act *Action, out ActionData) error {
return err
}

p.rules = append(p.rules, rule{
p.appendRule(rule{
Action: ActionSetBody,
Part: part,
Args: args,
Expand All @@ -383,7 +383,7 @@ func (p *Page) ActionSetMethod(act *Action, out ActionData) error {
return err
}

p.rules = append(p.rules, rule{
p.appendRule(rule{
Action: ActionSetMethod,
Part: part,
Args: args,
Expand Down
7 changes: 5 additions & 2 deletions pkg/protocols/headless/engine/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack)
return func(ctx *rod.Hijack) {
// usually browsers don't use chunked transfer encoding, so we set the content-length nevertheless
ctx.Request.Req().ContentLength = int64(len(ctx.Request.Body()))
for _, rule := range p.rules {
// snapshot the rules once: ExecuteActions may still be appending rules
// from another goroutine while this hijack handler runs.
rules := p.rulesSnapshot()
for _, rule := range rules {
if rule.Part != "request" {
continue
}
Expand Down Expand Up @@ -61,7 +64,7 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack)
}
}

for _, rule := range p.rules {
for _, rule := range rules {
if rule.Part != "response" {
continue
}
Expand Down
Loading