diff --git a/cmd/nuclei/main.go b/cmd/nuclei/main.go index 188d3fba92..1c4fbc5608 100644 --- a/cmd/nuclei/main.go +++ b/cmd/nuclei/main.go @@ -435,6 +435,7 @@ on extensive configurability, massive extensibility and ease of use.`) ) flagSet.CreateGroup("headless", "Headless", + flagSet.BoolVarP(&options.DisableTechStackFiltering, "disable-tech-filter", "dtf", false, "disable automatic template filtering based on Server response header tech detection"), flagSet.BoolVar(&options.Headless, "headless", false, "enable templates that require headless browser support (root user on Linux will disable sandbox)"), flagSet.IntVar(&options.PageTimeout, "page-timeout", 20, "seconds to wait for each page in headless mode"), flagSet.BoolVarP(&options.ShowBrowser, "show-browser", "sb", false, "show the browser on the screen when running templates with headless mode"), diff --git a/pkg/core/engine.go b/pkg/core/engine.go index 0a412b6fcc..c0ae2be969 100644 --- a/pkg/core/engine.go +++ b/pkg/core/engine.go @@ -5,6 +5,7 @@ import ( "github.com/projectdiscovery/nuclei/v3/pkg/output" "github.com/projectdiscovery/nuclei/v3/pkg/protocols" "github.com/projectdiscovery/nuclei/v3/pkg/types" + "github.com/projectdiscovery/nuclei/v3/pkg/core/hosttechcache" ) // Engine is an executer for running Nuclei Templates/Workflows. @@ -21,6 +22,7 @@ type Engine struct { executerOpts *protocols.ExecutorOptions Callback func(*output.ResultEvent) // Executed on results Logger *gologger.Logger + HostTechCache *hosttechcache.HostTechCache } // New returns a new Engine instance @@ -30,6 +32,9 @@ func New(options *types.Options) *Engine { Logger: options.Logger, } engine.workPool = engine.GetWorkPool() + if !options.DisableTechStackFiltering { + engine.HostTechCache = hosttechcache.NewHostTechCache() + } return engine } @@ -64,4 +69,4 @@ func (e *Engine) WorkPool() *WorkPool { // resize check point - nop if there are no changes e.workPool.RefreshWithConfig(e.GetWorkPoolConfig()) return e.workPool -} +} \ No newline at end of file diff --git a/pkg/core/execute_options.go b/pkg/core/execute_options.go index df1fe14358..9689abdf74 100644 --- a/pkg/core/execute_options.go +++ b/pkg/core/execute_options.go @@ -175,4 +175,4 @@ func getRequestCount(templates []*templates.Template) int { count += template.TotalRequests } return count -} +} \ No newline at end of file diff --git a/pkg/core/executors.go b/pkg/core/executors.go index aeb85ddfe6..105d7633b5 100644 --- a/pkg/core/executors.go +++ b/pkg/core/executors.go @@ -5,7 +5,9 @@ import ( "sync" "sync/atomic" "time" - + "net/http" + "strings" + "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v3/pkg/input/provider" "github.com/projectdiscovery/nuclei/v3/pkg/output" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs" @@ -229,6 +231,82 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates ctxArgs.MetaInput = value scanCtx := scan.NewScanContext(ctx, ctxArgs) + // CRITICAL: Check if HostTechCache exists + if e.HostTechCache == nil { + gologger.Warning().Msgf("[tech-filter] ERROR: HostTechCache is NIL! Tech filtering is disabled.") + } else { + gologger.Warning().Msgf("[tech-filter] HostTechCache is initialized!") + } + + // --- Tech-stack probe: ONE-TIME per host --- + if e.HostTechCache != nil { + // Only probe if we have NO hint for this host yet + if !e.HostTechCache.HasHint(value.Input) { + gologger.Warning().Msgf("[tech-filter] PROBE: Attempting probe for host '%s'", value.Input) + + // Send a lightweight HEAD request to detect Server header + client := &http.Client{ + Timeout: 5 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // Don't follow redirects + }, + } + + // Build full URL if needed + targetURL := value.Input + if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") { + targetURL = "http://" + targetURL + } + + gologger.Warning().Msgf("[tech-filter] PROBE: Sending HEAD to '%s'", targetURL) + + resp, err := client.Head(targetURL) + if err == nil && resp != nil { + defer resp.Body.Close() + if serverHdr := resp.Header.Get("Server"); serverHdr != "" { + gologger.Warning().Msgf("[tech-filter] PROBE SUCCESS: Found Server header '%s' for host '%s'", + serverHdr, value.Input) + e.HostTechCache.RecordServerHeader(value.Input, serverHdr) + } else { + gologger.Warning().Msgf("[tech-filter] PROBE: No Server header for host '%s'", value.Input) + e.HostTechCache.RecordNoServerHeader(value.Input) + } + } else { + gologger.Warning().Msgf("[tech-filter] PROBE FAILED for host '%s': %v", value.Input, err) + e.HostTechCache.RecordNoServerHeader(value.Input) + } + } else { + gologger.Warning().Msgf("[tech-filter] PROBE: Already have hint for host '%s', skipping probe", value.Input) + } + } + // --- end probe --- + + // --- Tech-stack based template filtering --- + gologger.Warning().Msgf("[tech-filter] CHECK: Checking if should skip template '%s' for host '%s'", template.ID, value.Input) + + if e.HostTechCache != nil { + tags := template.Info.Tags.ToSlice() + gologger.Warning().Msgf("[tech-filter] Template '%s' has tags: %v", template.ID, tags) + + if e.HostTechCache.ShouldSkipTemplate(value.Input, tags) { + serverHdr := "" + if e.HostTechCache.HasHint(value.Input) { + serverHdr = e.HostTechCache.GetServerHeader(value.Input) + } + + gologger.Warning().Msgf( + "[tech-filter] SKIPPED template '%s' for host '%s' (server='%s', no matching tags)", + template.ID, + value.Input, + serverHdr, + ) + return false, nil + } else { + gologger.Warning().Msgf("[tech-filter] ALLOW: Template '%s' passed filter for host '%s'", template.ID, value.Input) + } + } + // --- end tech-stack filtering --- + switch template.Type() { case types.WorkflowProtocol: return e.executeWorkflow(scanCtx, template.CompiledWorkflow), nil @@ -245,4 +323,4 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates } return template.Executer.Execute(scanCtx) } -} +} \ No newline at end of file diff --git a/pkg/core/hosttechcache/hosttechcache.go b/pkg/core/hosttechcache/hosttechcache.go new file mode 100644 index 0000000000..7f2ed4932d --- /dev/null +++ b/pkg/core/hosttechcache/hosttechcache.go @@ -0,0 +1,128 @@ +package hosttechcache + +import ( + "strings" + "sync" + "github.com/projectdiscovery/gologger" +) + +// TechHint represents a detected technology on a host that can be used +// to filter templates before execution. +type TechHint struct { + // ServerHeader stores the original Server header value for logging purposes + ServerHeader string + // Tags is the set of template tags that are REQUIRED for this host. + // A template is skipped unless it contains at least one of these tags, + // or the set is empty (meaning: no filtering). + Tags map[string]struct{} +} + +// HostTechCache stores per-host technology hints derived from early HTTP +// responses (e.g. the Server: header). It is safe for concurrent use. +type HostTechCache struct { + mu sync.RWMutex + hints map[string]*TechHint // keyed by normalised host (scheme+host) +} + +// NewHostTechCache returns an initialised HostTechCache. +func NewHostTechCache() *HostTechCache { + return &HostTechCache{hints: make(map[string]*TechHint)} +} + +// RecordServerHeader inspects a raw Server header value and, if it contains +// a known technology keyword, records a tag requirement for that host. +// +// Currently understood keywords → required tag: +// +// "apache" → "apache" +// +// The mapping is intentionally simple and lowercase-compared so that +// "Apache/2.4.51 (Unix)" and "apache" both resolve to the same hint. +func (c *HostTechCache) RecordServerHeader(host, serverHeader string) { + lower := strings.ToLower(serverHeader) + + var requiredTags []string + if strings.Contains(lower, "apache") { + requiredTags = append(requiredTags, "apache") + } + + c.mu.Lock() + defer c.mu.Unlock() + + if len(requiredTags) == 0 { + if _, exists := c.hints[host]; exists { + gologger.Debug().Msgf("[tech-filter] CLEARED hint for host '%s' (unrecognised Server header: '%s')", + host, serverHeader) + } + delete(c.hints, host) + return + } + + gologger.Debug().Msgf("[tech-filter] RECORDED hint for host '%s' — Server: '%s' → required tags: %v", + host, serverHeader, requiredTags) + + hint := &TechHint{ + ServerHeader: serverHeader, + Tags: make(map[string]struct{}, len(requiredTags)), + } + + for _, t := range requiredTags { + hint.Tags[t] = struct{}{} + } + c.hints[host] = hint +} + +// ShouldSkipTemplate returns true when the cache has a hint for the given host +// AND the template's tags contain none of the required tags. +// +// If there is no hint for the host the function always returns false (no skip). +func (c *HostTechCache) ShouldSkipTemplate(host string, templateTags []string) bool { + c.mu.RLock() + hint, ok := c.hints[host] + c.mu.RUnlock() + + if !ok || len(hint.Tags) == 0 { + return false // no information → don't skip + } + + for _, tag := range templateTags { + if _, required := hint.Tags[strings.ToLower(tag)]; required { + return false // template has at least one matching tag → keep it + } + } + return true // no matching tag found → skip +} + +// HasHint returns true if any hint (including "no recognised tech") has been +// recorded for this host, so we don't probe the same host twice. +func (c *HostTechCache) HasHint(host string) bool { + c.mu.RLock() + _, ok := c.hints[host] + c.mu.RUnlock() + return ok +} + + +// RecordNoServerHeader marks that we checked a host but found no Server header +func (c *HostTechCache) RecordNoServerHeader(host string) { + c.mu.Lock() + defer c.mu.Unlock() + // Create an empty TechHint to indicate we checked but found nothing + c.hints[host] = &TechHint{ + ServerHeader: "", + Tags: make(map[string]struct{}), + } + gologger.Debug().Msgf("[tech-filter] RECORDED no Server header for host '%s'", host) +} + +// GetServerHeader returns the detected server header for a host +func (c *HostTechCache) GetServerHeader(host string) string { + c.mu.RLock() + defer c.mu.RUnlock() + + hint, exists := c.hints[host] + if !exists || hint == nil { + return "" + } + return hint.ServerHeader +} diff --git a/pkg/core/hosttechcache/hosttestcache_test.go b/pkg/core/hosttechcache/hosttestcache_test.go new file mode 100644 index 0000000000..414da97e54 --- /dev/null +++ b/pkg/core/hosttechcache/hosttestcache_test.go @@ -0,0 +1,286 @@ +package hosttechcache + +import ( + "fmt" + "sync" + "testing" +) + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +// mustNotSkip fails the test if ShouldSkipTemplate returns true. +func mustNotSkip(t *testing.T, c *HostTechCache, host string, tags []string, msg string) { + t.Helper() + if c.ShouldSkipTemplate(host, tags) { + t.Errorf("UNEXPECTED SKIP — %s (host=%q tags=%v)", msg, host, tags) + } +} + +// mustSkip fails the test if ShouldSkipTemplate returns false. +func mustSkip(t *testing.T, c *HostTechCache, host string, tags []string, msg string) { + t.Helper() + if !c.ShouldSkipTemplate(host, tags) { + t.Errorf("EXPECTED SKIP — %s (host=%q tags=%v)", msg, host, tags) + } +} + +// --------------------------------------------------------------------------- +// 1. Basic Apache detection +// --------------------------------------------------------------------------- + +func TestHostTechCache_ApacheExactMatch(t *testing.T) { + c := NewHostTechCache() + c.RecordServerHeader("http://example.com", "Apache") + + mustSkip(t, c, "http://example.com", []string{"nginx"}, "non-apache tag should be skipped") + mustNotSkip(t, c, "http://example.com", []string{"apache"}, "apache tag must not be skipped") +} + +func TestHostTechCache_ApacheVersionString(t *testing.T) { + // Real-world Server header: "Apache/2.4.51 (Unix) OpenSSL/1.1.1l" + c := NewHostTechCache() + c.RecordServerHeader("https://target.io", "Apache/2.4.51 (Unix) OpenSSL/1.1.1l") + + mustSkip(t, c, "https://target.io", []string{"iis", "xss"}, "non-apache template should be skipped") + mustNotSkip(t, c, "https://target.io", []string{"apache", "cve"}, "template tagged apache+cve must run") +} + +func TestHostTechCache_ApacheCaseInsensitive(t *testing.T) { + variants := []string{ + "APACHE/2.4", + "Apache/2.4", + "apache/2.4", + "aPaChE", + } + for _, hdr := range variants { + c := NewHostTechCache() + c.RecordServerHeader("http://host.test", hdr) + + mustSkip(t, c, "http://host.test", []string{"nginx"}, fmt.Sprintf("header %q: non-apache should skip", hdr)) + mustNotSkip(t, c, "http://host.test", []string{"apache"}, fmt.Sprintf("header %q: apache tag must not skip", hdr)) + } +} + +// --------------------------------------------------------------------------- +// 2. No hint → never skip +// --------------------------------------------------------------------------- + +func TestHostTechCache_UnknownHostNeverSkipped(t *testing.T) { + c := NewHostTechCache() + // Nothing recorded for "http://unknown.host" + mustNotSkip(t, c, "http://unknown.host", []string{"xss", "sqli"}, "host with no hint must never be skipped") +} + +func TestHostTechCache_EmptyServerHeader(t *testing.T) { + c := NewHostTechCache() + c.RecordServerHeader("http://silent.host", "") + + mustNotSkip(t, c, "http://silent.host", []string{"xss"}, "empty Server header must not create a hint") +} + +func TestHostTechCache_UnrecognisedServerHeader(t *testing.T) { + c := NewHostTechCache() + c.RecordServerHeader("http://custom.host", "MyPrivateServer/3.0") + + mustNotSkip(t, c, "http://custom.host", []string{"xss", "sqli"}, "unknown server value must not trigger filtering") +} + +// --------------------------------------------------------------------------- +// 3. Tag matching logic +// --------------------------------------------------------------------------- + +func TestHostTechCache_TemplateWithNoTags(t *testing.T) { + // A template with zero tags should be skipped once Apache is detected — + // it carries no evidence of Apache relevance. + c := NewHostTechCache() + c.RecordServerHeader("http://example.com", "Apache/2.2") + + mustSkip(t, c, "http://example.com", []string{}, "tagless template should be skipped on Apache host") + mustSkip(t, c, "http://example.com", nil, "nil-tag template should be skipped on Apache host") +} + +func TestHostTechCache_MultiTagTemplateOneMatches(t *testing.T) { + // Template tagged ["rce", "apache", "cve-2021"] — should NOT be skipped. + c := NewHostTechCache() + c.RecordServerHeader("http://example.com", "Apache/2.4") + + mustNotSkip(t, c, "http://example.com", []string{"rce", "apache", "cve-2021"}, + "template with apache among multiple tags must run") +} + +func TestHostTechCache_MultiTagTemplateNoneMatch(t *testing.T) { + // Template tagged ["rce", "nginx", "cve-2023"] — should be skipped. + c := NewHostTechCache() + c.RecordServerHeader("http://example.com", "Apache/2.4") + + mustSkip(t, c, "http://example.com", []string{"rce", "nginx", "cve-2023"}, + "template without apache tag must be skipped on Apache host") +} + +func TestHostTechCache_TagComparisonIsCaseInsensitive(t *testing.T) { + c := NewHostTechCache() + c.RecordServerHeader("http://example.com", "Apache/2.4") + + // Template tags written in various cases should all match. + for _, tag := range []string{"Apache", "APACHE", "aPaChE"} { + mustNotSkip(t, c, "http://example.com", []string{tag}, + fmt.Sprintf("tag %q must match the apache hint case-insensitively", tag)) + } +} + +// --------------------------------------------------------------------------- +// 4. Host isolation (different hosts don't bleed into each other) +// --------------------------------------------------------------------------- + +func TestHostTechCache_PerHostIsolation(t *testing.T) { + c := NewHostTechCache() + c.RecordServerHeader("http://apache-host.com", "Apache/2.4") + // nginx-host.com deliberately has no hint recorded. + + // apache-host: non-apache template skipped + mustSkip(t, c, "http://apache-host.com", []string{"nginx"}, "apache-host: nginx template should skip") + + // nginx-host: same template must NOT be skipped (no hint) + mustNotSkip(t, c, "http://nginx-host.com", []string{"nginx"}, "nginx-host: nginx template must run (no hint)") + + // apache-host: apache template not skipped + mustNotSkip(t, c, "http://apache-host.com", []string{"apache"}, "apache-host: apache template must run") +} + +func TestHostTechCache_OverwriteHint(t *testing.T) { + // If a second response arrives with a different Server header for the same + // host (e.g. redirect to a different backend), the newer hint wins. + c := NewHostTechCache() + c.RecordServerHeader("http://example.com", "Apache/2.4") + c.RecordServerHeader("http://example.com", "MyPrivateServer/1.0") // overwrites — no recognised tech + + // After the overwrite the hint for example.com should be gone. + mustNotSkip(t, c, "http://example.com", []string{"nginx"}, + "after overwrite with unknown server, no filtering should apply") +} + +// --------------------------------------------------------------------------- +// 5. Concurrency safety +// --------------------------------------------------------------------------- + +func TestHostTechCache_ConcurrentWrites(t *testing.T) { + c := NewHostTechCache() + + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines) + + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + host := fmt.Sprintf("http://host-%d.example.com", i) + c.RecordServerHeader(host, "Apache/2.4") + }(i) + } + wg.Wait() + + // Every host should now have the apache hint. + for i := 0; i < goroutines; i++ { + host := fmt.Sprintf("http://host-%d.example.com", i) + mustSkip(t, c, host, []string{"nginx"}, fmt.Sprintf("host %s: non-apache should skip after concurrent write", host)) + mustNotSkip(t, c, host, []string{"apache"}, fmt.Sprintf("host %s: apache should not skip after concurrent write", host)) + } +} + +func TestHostTechCache_ConcurrentReadsAndWrites(t *testing.T) { + c := NewHostTechCache() + + const writers = 20 + const readers = 40 + + var wg sync.WaitGroup + wg.Add(writers + readers) + + // Writers record Apache for even-numbered hosts. + for i := 0; i < writers; i++ { + go func(i int) { + defer wg.Done() + host := fmt.Sprintf("http://host-%d.test", i*2) // even hosts + c.RecordServerHeader(host, "Apache/2.4") + }(i) + } + + // Readers call ShouldSkipTemplate concurrently; we just verify no panic/race. + for i := 0; i < readers; i++ { + go func(i int) { + defer wg.Done() + host := fmt.Sprintf("http://host-%d.test", i) + // Result may be true or false depending on scheduling; we don't assert + // here — the race detector will catch any data races. + _ = c.ShouldSkipTemplate(host, []string{"apache"}) + }(i) + } + + wg.Wait() +} + +// --------------------------------------------------------------------------- +// 6. NewHostTechCache constructor +// --------------------------------------------------------------------------- + +func TestNewHostTechCache_InitialisedEmpty(t *testing.T) { + c := NewHostTechCache() + if c == nil { + t.Fatal("NewHostTechCache returned nil") + } + // Freshly created cache should never skip anything. + mustNotSkip(t, c, "http://any.host", []string{"apache"}, "fresh cache must never skip") +} + +// --------------------------------------------------------------------------- +// 7. Edge / boundary cases +// --------------------------------------------------------------------------- + +func TestHostTechCache_WhitespaceServerHeader(t *testing.T) { + c := NewHostTechCache() + c.RecordServerHeader("http://ws.host", " ") + + mustNotSkip(t, c, "http://ws.host", []string{"xss"}, "whitespace-only Server header must not trigger filtering") +} + +func TestHostTechCache_ServerHeaderContainsApacheAsSubstring(t *testing.T) { + // "NotApache/1.0" still contains the substring "apache" in lowercase; + // the current implementation detects it. This test documents that behaviour + // explicitly so a future change to tighten the match is a conscious decision. + c := NewHostTechCache() + c.RecordServerHeader("http://sub.host", "NotApache/1.0") + + mustSkip(t, c, "http://sub.host", []string{"nginx"}, + "'NotApache' contains 'apache' substring — current behaviour skips non-apache templates") + mustNotSkip(t, c, "http://sub.host", []string{"apache"}, + "'NotApache' contains 'apache' substring — apache-tagged template must still run") +} + +func TestHostTechCache_MultipleHostsIndependent(t *testing.T) { + c := NewHostTechCache() + + hosts := map[string]string{ + "http://alpha.test": "Apache/2.4", + "http://beta.test": "nginx/1.18", // unrecognised → no hint + "http://gamma.test": "", // empty → no hint + "http://delta.test": "Apache/1.3.42", + } + + for host, header := range hosts { + c.RecordServerHeader(host, header) + } + + // Alpha and delta → apache hints recorded. + for _, host := range []string{"http://alpha.test", "http://delta.test"} { + mustSkip(t, c, host, []string{"nginx"}, host+": nginx should skip (apache host)") + mustNotSkip(t, c, host, []string{"apache"}, host+": apache should not skip") + } + + // Beta and gamma → no hints; nothing skipped. + for _, host := range []string{"http://beta.test", "http://gamma.test"} { + mustNotSkip(t, c, host, []string{"nginx"}, host+": nginx must not skip (no hint)") + mustNotSkip(t, c, host, []string{"apache"}, host+": apache must not skip (no hint)") + } +} \ No newline at end of file diff --git a/pkg/protocols/http/request.go b/pkg/protocols/http/request.go index 9698b4ade5..ad43d6b5ae 100644 --- a/pkg/protocols/http/request.go +++ b/pkg/protocols/http/request.go @@ -922,6 +922,15 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ } } + if resp != nil { + if serverHdr := resp.Header.Get("Server"); serverHdr != "" { + if tc := request.options.HostTechCache; tc != nil { + tc.RecordServerHeader(input.MetaInput.Input, serverHdr) + } + } + } + + gologger.Verbose().Msgf("[%s] Sent HTTP request to %s", request.options.TemplateID, formedURL) request.options.Output.Request(request.options.TemplatePath, formedURL, request.Type().String(), err) diff --git a/pkg/protocols/protocols.go b/pkg/protocols/protocols.go index 69a8b7bea3..0230ed6094 100644 --- a/pkg/protocols/protocols.go +++ b/pkg/protocols/protocols.go @@ -39,6 +39,7 @@ import ( templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types" "github.com/projectdiscovery/nuclei/v3/pkg/types" unitutils "github.com/projectdiscovery/utils/unit" + "github.com/projectdiscovery/nuclei/v3/pkg/core/hosttechcache" ) var ( @@ -89,6 +90,7 @@ type ExecutorOptions struct { Interactsh *interactsh.Client // HostErrorsCache is an optional cache for handling host errors HostErrorsCache hosterrorscache.CacheInterface + HostTechCache *hosttechcache.HostTechCache // Stop execution once first match is found (Assigned while parsing templates) // Note: this is different from Options.StopAtFirstMatch (Assigned from CLI option) StopAtFirstMatch bool @@ -277,6 +279,7 @@ func (e *ExecutorOptions) Copy() *ExecutorOptions { Browser: e.Browser, Interactsh: e.Interactsh, HostErrorsCache: e.HostErrorsCache, + HostTechCache: e.HostTechCache, StopAtFirstMatch: e.StopAtFirstMatch, Variables: e.Variables, Constants: e.Constants, @@ -472,4 +475,5 @@ func (e *ExecutorOptions) ApplyNewEngineOptions(n *ExecutorOptions) { e.GlobalMatchers = n.GlobalMatchers e.Logger = n.Logger e.CustomFastdialer = n.CustomFastdialer -} + e.HostTechCache = n.HostTechCache +} \ No newline at end of file diff --git a/pkg/tmplexec/exec.go b/pkg/tmplexec/exec.go index acf72a3c0d..04d5b1de64 100644 --- a/pkg/tmplexec/exec.go +++ b/pkg/tmplexec/exec.go @@ -271,6 +271,26 @@ func getErrorCause(err error) string { // ExecuteWithResults executes the protocol requests and returns results instead of writing them. func (e *TemplateExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) { + gologger.Debug().Msgf("trying ExecuteWithResults - go logger") + ctx.LogWarning("trying ExecuteWithResults - ctx logger") + e.options.Logger.Warning().Msgf("trying ExecuteWithResults - e looger") + // --- Tech-stack based template filtering --- + if tc := e.options.HostTechCache; tc != nil { + tags := e.options.TemplateInfo.Tags.ToSlice() + host := ctx.Input.MetaInput.Input + if tc.ShouldSkipTemplate(host, tags) { + gologger.Debug().Msgf("[tech-filter] SKIPPED template '%s' (tags: %v) for host '%s' — no matching tech hint", + e.options.TemplateID, tags, host) + e.options.Logger.Warning().Msgf("tryting skip-success") + return nil, nil + } else { + e.options.Logger.Warning().Msgf("tryting skip-success not needed") + gologger.Debug().Msgf("[tech-filter] ALLOWED template '%s' (tags: %v) for host '%s'", + e.options.TemplateID, tags, host) + } + } + // --- end filtering --- + var errx error if e.options.Flow != "" { flowexec, err := flow.NewFlowExecutor(e.requests, ctx, e.options, e.results, e.program) diff --git a/pkg/types/types.go b/pkg/types/types.go index 08ed924ffb..450b4fc676 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -132,6 +132,9 @@ type Options struct { TrackError goflags.StringSlice // NoHostErrors disables host skipping after maximum number of errors NoHostErrors bool + // DisableTechStackFiltering disables the automatic per-host template + // filtering based on HTTP response headers (e.g. Server: Apache). + DisableTechStackFiltering bool `yaml:"disable-tech-filter,omitempty"` // BulkSize is the of targets analyzed in parallel for each template BulkSize int // TemplateThreads is the number of templates executed in parallel