-
Notifications
You must be signed in to change notification settings - Fork 795
Add comprehensive URL depth filtering system #1353 #1444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Add comprehensive URL depth filtering system #1353 #1444
Conversation
* Add support to collecting and applying cookies while crawling * check for error creating jar * wrap the cookie updating with a mutex * feat: add option to disable unique content filter This commit adds a new command-line flag --disable-unique-filter (-duf) that allows users to disable the duplicate content filtering. This ensures all responses reach the OnResult callback regardless of content duplication. Fixes projectdiscovery#1350 * chore(deps): bump github.com/projectdiscovery/retryablehttp-go (projectdiscovery#1346) Bumps [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) from 1.0.118 to 1.0.119. - [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases) - [Commits](projectdiscovery/retryablehttp-go@v1.0.118...v1.0.119) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/retryablehttp-go dependency-version: 1.0.119 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump golang from 1.24.2-alpine to 1.24.5-alpine (projectdiscovery#1349) Bumps golang from 1.24.2-alpine to 1.24.5-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.24.5-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/projectdiscovery/retryabledns (projectdiscovery#1347) Bumps [github.com/projectdiscovery/retryabledns](https://github.com/projectdiscovery/retryabledns) from 1.0.103 to 1.0.105. - [Release notes](https://github.com/projectdiscovery/retryabledns/releases) - [Commits](projectdiscovery/retryabledns@v1.0.103...v1.0.105) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/retryabledns dependency-version: 1.0.105 dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/projectdiscovery/fastdialer (projectdiscovery#1354) Bumps [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) from 0.4.1 to 0.4.4. - [Release notes](https://github.com/projectdiscovery/fastdialer/releases) - [Commits](projectdiscovery/fastdialer@v0.4.1...v0.4.4) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/fastdialer dependency-version: 0.4.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/projectdiscovery/wappalyzergo (projectdiscovery#1355) Bumps [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) from 0.2.38 to 0.2.40. - [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases) - [Commits](projectdiscovery/wappalyzergo@v0.2.38...v0.2.40) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/wappalyzergo dependency-version: 0.2.40 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/projectdiscovery/dsl from 0.5.0 to 0.5.1 Bumps [github.com/projectdiscovery/dsl](https://github.com/projectdiscovery/dsl) from 0.5.0 to 0.5.1. - [Release notes](https://github.com/projectdiscovery/dsl/releases) - [Commits](projectdiscovery/dsl@v0.5.0...v0.5.1) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/dsl dependency-version: 0.5.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump github.com/projectdiscovery/networkpolicy Bumps [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) from 0.1.18 to 0.1.20. - [Release notes](https://github.com/projectdiscovery/networkpolicy/releases) - [Commits](projectdiscovery/networkpolicy@v0.1.18...v0.1.20) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/networkpolicy dependency-version: 0.1.20 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump github.com/projectdiscovery/wappalyzergo Bumps [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) from 0.2.40 to 0.2.41. - [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases) - [Commits](projectdiscovery/wappalyzergo@v0.2.40...v0.2.41) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/wappalyzergo dependency-version: 0.2.41 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump github.com/projectdiscovery/fastdialer Bumps [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) from 0.4.4 to 0.4.5. - [Release notes](https://github.com/projectdiscovery/fastdialer/releases) - [Commits](projectdiscovery/fastdialer@v0.4.4...v0.4.5) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/fastdialer dependency-version: 0.4.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> * updating docs * refactor as integration test * fix go version * keep only disable test * comments * sync cookiejar * nil pointer * add stale workflow * chore(deps): bump actions/checkout from 4 to 5 (projectdiscovery#1369) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump golang from 1.24.5-alpine to 1.25.0-alpine (projectdiscovery#1373) Bumps golang from 1.24.5-alpine to 1.25.0-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.25.0-alpine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * version update * issue template update * misc update * Update workflow-monitor.yml --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: vbisbest <[email protected]> Co-authored-by: niro <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mzack9999 <[email protected]> Co-authored-by: Doğan Can Bakır <[email protected]>
Implements URL filtering based on structural characteristics: Core Features: - Path depth filtering (-cpd): Filter URLs by number of path segments - Query parameter filtering (-cqp): Filter URLs by query parameter count - Subdomain depth filtering (-csd): Filter URLs by subdomain levels - Flexible operators: >=, <=, ==, >, < for precise control - Range syntax: Support for ranges like '2-5', '1-3' for flexible filtering - Logic operators: --depth-filter-or flag for OR logic between filter types Enhanced User Experience: - Comprehensive error messages with specific guidance and examples - Intuitive CLI integration following existing Katana patterns - Performance optimizations with intelligent caching system - Seamless integration with existing filters (regex, extension, scope) Technical Implementation: - Thread-safe caching for URL component analysis - Efficient parsing and validation with detailed error reporting - Memory-optimized design for large-scale crawling - Comprehensive test coverage including unit, integration, and performance tests Examples: katana -u target.com -cpd '>=2' -cqp '1-3' # URLs with 2+ path depth and 1-3 params katana -u target.com --depth-filter-or -cpd '>=4' -csd '>=1' # Deep paths OR subdomains
WalkthroughIntroduces depth-based URL filtering across path depth, query parameter count, and subdomain depth. Adds new CLI flags and corresponding options, integrates a DepthFilterValidator with caching, validates filters during option parsing, applies filtering in output flow before regex/DSL evaluation, and provides comprehensive tests for parsing, counting, and logic modes (AND/OR). Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant CLI as cmd/katana
participant R as Runner (options)
participant O as Output.New (StandardWriter)
participant F as filters.DepthFilterValidator
U->>CLI: Run with depth flags
CLI->>R: Build Options (CountPathDepth, ...)
R->>F: ValidateAndSuggest(...) per filter
R-->>CLI: Validation result (ok/error)
CLI->>O: Initialize writer with Options
O->>F: NewDepthFilterValidator(..., useOrLogic)
F-->>O: Validator (or error)
sequenceDiagram
autonumber
participant C as Crawler/Emitter
participant O as Output.filterOutput
participant P as net/url
participant F as DepthFilterValidator
participant RX as Regex/DSL Filters
C-->>O: Event(URL)
O->>P: Parse URL
alt parse error
O-->>C: Filter out (true)
else parsed
O->>F: ValidateURL(parsed)
alt fails depth validation
O-->>C: Filter out (true)
else passes
O->>RX: Apply existing filters
RX-->>O: Pass/Fail
O-->>C: Final decision
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (7)
internal/runner/options.go (1)
63-87
: Early depth-filter validation is solid; consider small DRY/UX tweaks
- Factor the three loops via a tiny helper to reduce repetition.
- Optionally strings.TrimSpace before validation for friendlier UX on inputs with stray spaces.
pkg/types/crawler_options.go (1)
40-41
: Avoid double creation/caching of DepthFilterValidatorThis validator is created here and again inside pkg/output/output.go. Prefer a single instance to avoid duplicate parsing and cache memory. Consider passing DepthValidator via output.Options (or constructing only in one place and reusing).
Also applies to: 147-148
pkg/output/output.go (2)
91-104
: Reuse existing validator to avoid duplicate parsing/cachingA DepthFilterValidator is also created in pkg/types/crawler_options.go. Prefer injecting that instance via Options to avoid double work and memory.
373-388
: Optional: add debug log on URL-parse failures before filtering outCurrently parse errors silently drop results. A debug-level log would aid troubleshooting malformed URLs without noisy output.
Example:
if err != nil { gologger.Debug().Msgf("Depth filter: failed to parse URL %q: %v", event.Request.URL, err) return true }pkg/utils/filters/depth_filter.go (3)
466-474
: Avoid deprecated strings.Title usage.strings.Title is deprecated. Use simple first-letter capitalization to avoid new deps.
- return fmt.Errorf("❌ %s filter validation failed:\n%w", - strings.Title(filterType), err) + title := filterType + if len(title) > 0 { + title = strings.ToUpper(title[:1]) + title[1:] + } + return fmt.Errorf("❌ %s filter validation failed:\n%w", title, err)
170-218
: Micro-optimization: precompile regexes.parseDepthFilter recompiles regex each call. Precompile at package level to reduce allocations during option parsing.
- // inside parseDepthFilter - rangeRe := regexp.MustCompile(`^(\d+)-(\d+)$`) + // at package scope + var ( + rangeRe = regexp.MustCompile(`^(\d+)-(\d+)$`) + compRe = regexp.MustCompile(`^(>=|<=|==|>|<)(\d+)$`) + ) ... - re := regexp.MustCompile(`^(>=|<=|==|>|<)(\d+)$`) - matches := re.FindStringSubmatch(filter) + matches := compRe.FindStringSubmatch(filter)
368-376
: Cache eviction is coarse; consider bounded/LRU behavior.Clearing entire maps when urlComponents reaches maxSize can cause cache thrash and keeps filterResults growing independently. Consider:
- Track size of filterResults and bound it similarly.
- Implement simple LRU (list + map) or random eviction instead of full reset.
Also applies to: 398-406
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
cmd/katana/main.go
(1 hunks)internal/runner/options.go
(2 hunks)pkg/output/options.go
(1 hunks)pkg/output/output.go
(5 hunks)pkg/types/crawler_options.go
(5 hunks)pkg/types/options.go
(1 hunks)pkg/utils/filters/depth_filter.go
(1 hunks)pkg/utils/filters/depth_filter_test.go
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
internal/runner/options.go (1)
pkg/utils/filters/depth_filter.go (1)
ValidateAndSuggest
(467-474)
pkg/utils/filters/depth_filter_test.go (1)
pkg/utils/filters/depth_filter.go (2)
DepthFilter
(14-18)NewDepthFilterValidator
(47-90)
pkg/types/crawler_options.go (1)
pkg/utils/filters/depth_filter.go (2)
DepthFilterValidator
(38-44)NewDepthFilterValidator
(47-90)
pkg/output/output.go (2)
pkg/utils/filters/depth_filter.go (2)
DepthFilterValidator
(38-44)NewDepthFilterValidator
(47-90)pkg/navigation/request.go (1)
Request
(12-25)
🔇 Additional comments (4)
pkg/types/crawler_options.go (1)
174-183
: Good: early extension check, depth deferred to outputShort-circuiting on extension improves performance; deferring depth filtering to output keeps discovery intact.
pkg/output/options.go (1)
31-34
: LGTM: clean API for depth filtersFields mirror CLI and keep output decoupled from goflags types.
pkg/types/options.go (1)
45-52
: LGTM: options surface for depth filters is clear and aligned with CLIComments and naming are consistent.
cmd/katana/main.go (1)
177-181
: Options fields and wiring verified All new flags map to Options struct fields and are consumed by the depth‐filter validator in both crawler and output setup.
CountPathDepth: options.CountPathDepth, | ||
CountQueryParams: options.CountQueryParams, | ||
CountSubdomainDepth: options.CountSubdomainDepth, | ||
DepthFilterOrLogic: options.DepthFilterOrLogic, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compile-time type mismatch: convert goflags.StringSlice to []string
output.Options fields are []string, while options.Count* are goflags.StringSlice. Add explicit conversions to avoid build errors.
- CountPathDepth: options.CountPathDepth,
- CountQueryParams: options.CountQueryParams,
- CountSubdomainDepth: options.CountSubdomainDepth,
+ CountPathDepth: []string(options.CountPathDepth),
+ CountQueryParams: []string(options.CountQueryParams),
+ CountSubdomainDepth: []string(options.CountSubdomainDepth),
DepthFilterOrLogic: options.DepthFilterOrLogic,
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
CountPathDepth: options.CountPathDepth, | |
CountQueryParams: options.CountQueryParams, | |
CountSubdomainDepth: options.CountSubdomainDepth, | |
DepthFilterOrLogic: options.DepthFilterOrLogic, | |
} | |
CountPathDepth: []string(options.CountPathDepth), | |
CountQueryParams: []string(options.CountQueryParams), | |
CountSubdomainDepth: []string(options.CountSubdomainDepth), | |
DepthFilterOrLogic: options.DepthFilterOrLogic, |
🤖 Prompt for AI Agents
In pkg/types/crawler_options.go around lines 99 to 103, output.Options fields
(CountPathDepth, CountQueryParams, CountSubdomainDepth, DepthFilterOrLogic) are
typed as []string but the assigned values are goflags.StringSlice, causing a
compile-time type mismatch; fix by converting each goflags.StringSlice to a
[]string before assignment (e.g., call a conversion helper or use
[]string(someStringSlice) / someStringSlice.ToSlice() as appropriate) so each
field receives a plain []string.
depthValidator, err = filters.NewDepthFilterValidator( | ||
options.CountPathDepth, | ||
options.CountQueryParams, | ||
options.CountSubdomainDepth, | ||
options.DepthFilterOrLogic, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also convert goflags.StringSlice when constructing the validator
filters.NewDepthFilterValidator takes []string slices; pass converted values.
- depthValidator, err = filters.NewDepthFilterValidator(
- options.CountPathDepth,
- options.CountQueryParams,
- options.CountSubdomainDepth,
- options.DepthFilterOrLogic,
- )
+ depthValidator, err = filters.NewDepthFilterValidator(
+ []string(options.CountPathDepth),
+ []string(options.CountQueryParams),
+ []string(options.CountSubdomainDepth),
+ options.DepthFilterOrLogic,
+ )
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
depthValidator, err = filters.NewDepthFilterValidator( | |
options.CountPathDepth, | |
options.CountQueryParams, | |
options.CountSubdomainDepth, | |
options.DepthFilterOrLogic, | |
) | |
depthValidator, err = filters.NewDepthFilterValidator( | |
[]string(options.CountPathDepth), | |
[]string(options.CountQueryParams), | |
[]string(options.CountSubdomainDepth), | |
options.DepthFilterOrLogic, | |
) |
🤖 Prompt for AI Agents
In pkg/types/crawler_options.go around lines 128-133, the call to
filters.NewDepthFilterValidator is passing goflags.StringSlice values directly;
convert those to plain []string slices before passing them. Replace the
arguments options.CountPathDepth, options.CountQueryParams and
options.CountSubdomainDepth with their []string equivalents (perform a simple
conversion or extract the underlying slice) so NewDepthFilterValidator receives
[]string types.
func TestCountQueryParams(t *testing.T) { | ||
tests := []struct { | ||
query string | ||
expected int | ||
}{ | ||
{"", 0}, | ||
{"user=admin", 1}, | ||
{"user=admin&pass=secret", 2}, | ||
{"user=admin&pass=secret&role=user", 3}, | ||
{"user=admin&empty&pass=secret", 2}, // empty params ignored | ||
{"=value", 0}, // invalid param ignored | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
countQueryParams should ignore bare keys (test exposes bug).
Case "user=admin&empty&pass=secret" expects 2 by ignoring "empty" (no '='); current implementation counts it as 3. Fix countQueryParams to only count pairs with a non-empty key before '='. See suggested fix in pkg/utils/filters/depth_filter.go Lines 326-344.
🤖 Prompt for AI Agents
In pkg/utils/filters/depth_filter_test.go around lines 110 to 121: the test
shows countQueryParams currently counts bare keys like "empty" in
"user=admin&empty&pass=secret" but should ignore them; update the implementation
in pkg/utils/filters/depth_filter.go (around lines 326-344) so the function
splits on '&', then for each part checks that it contains '=' and that the
substring before '=' is non-empty (trimmed) before incrementing the count;
ensure you skip parts without '=' or with an empty key and add/adjust unit tests
if needed.
// countQueryParams counts the number of query parameters in a query string | ||
func countQueryParams(query string) int { | ||
if query == "" { | ||
return 0 | ||
} | ||
|
||
// Split by & and count valid parameters | ||
params := strings.Split(query, "&") | ||
count := 0 | ||
for _, param := range params { | ||
param = strings.TrimSpace(param) | ||
// Count parameters that have a key (with or without value) | ||
if param != "" && !strings.HasPrefix(param, "=") { | ||
count++ | ||
} | ||
} | ||
|
||
return count | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix: don’t count bare keys as query parameters.
"empty" (no '=') must be ignored. Count only tokens containing '=' with a non-empty key. This aligns with tests and real URL semantics.
Apply this diff:
func countQueryParams(query string) int {
if query == "" {
return 0
}
// Split by & and count valid parameters
params := strings.Split(query, "&")
count := 0
for _, param := range params {
- param = strings.TrimSpace(param)
- // Count parameters that have a key (with or without value)
- if param != "" && !strings.HasPrefix(param, "=") {
- count++
- }
+ param = strings.TrimSpace(param)
+ if param == "" {
+ continue
+ }
+ // Count only key[=value] where key is non-empty
+ if i := strings.IndexByte(param, '='); i > 0 {
+ // key is param[:i]; allow empty value
+ count++
+ }
}
return count
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// countQueryParams counts the number of query parameters in a query string | |
func countQueryParams(query string) int { | |
if query == "" { | |
return 0 | |
} | |
// Split by & and count valid parameters | |
params := strings.Split(query, "&") | |
count := 0 | |
for _, param := range params { | |
param = strings.TrimSpace(param) | |
// Count parameters that have a key (with or without value) | |
if param != "" && !strings.HasPrefix(param, "=") { | |
count++ | |
} | |
} | |
return count | |
} | |
// countQueryParams counts the number of query parameters in a query string | |
func countQueryParams(query string) int { | |
if query == "" { | |
return 0 | |
} | |
// Split by & and count valid parameters | |
params := strings.Split(query, "&") | |
count := 0 | |
for _, param := range params { | |
param = strings.TrimSpace(param) | |
if param == "" { | |
continue | |
} | |
// Count only key[=value] where key is non-empty | |
if i := strings.IndexByte(param, '='); i > 0 { | |
// key is param[:i]; allow empty value | |
count++ | |
} | |
} | |
return count | |
} |
🤖 Prompt for AI Agents
In pkg/utils/filters/depth_filter.go around lines 326 to 344, the current
countQueryParams function incorrectly counts bare keys (tokens without '=') as
parameters; update it to only count tokens that contain an '=' and have a
non-empty key (i.e., the '=' must exist and its index must be > 0). Trim the
token, skip empty tokens, ensure strings.Contains(param, "=") is true, and
verify the substring before '=' is non-empty before incrementing the count.
Implements URL filtering based on structural characteristics:
Core Features:
Enhanced User Experience:
Technical Implementation:
Examples:
katana -u target.com -cpd '>=2' -cqp '1-3' # URLs with 2+ path depth and 1-3 params
katana -u target.com --depth-filter-or -cpd '>=4' -csd '>=1' # Deep paths OR subdomains
#1353
Summary by CodeRabbit