Skip to content

Refine: Fix confidence tiers, XSS/SSRF validators, add GraphQL & race condition checks - #5

Merged
ktfth merged 2 commits into
mainfrom
claude/youthful-volta-ChZAV
May 27, 2026
Merged

Refine: Fix confidence tiers, XSS/SSRF validators, add GraphQL & race condition checks#5
ktfth merged 2 commits into
mainfrom
claude/youthful-volta-ChZAV

Conversation

@ktfth

@ktfth ktfth commented May 27, 2026

Copy link
Copy Markdown
Owner

Summary

Implements six improvements identified in the refine pass:

  1. Confidence percentage tier mapping — corrects the formula to properly map evidence weight to TENTATIVE (10–39%), FIRM (50–79%), and CONFIRMED (85–99%) ranges
  2. XSS validator fix — closed </script> tags no longer incorrectly trigger the "inside script block" context check
  3. SSRF validator enhancement — adds support for 307/308 redirect status codes as valid SSRF indicators
  4. GraphQL validator (new) — detects introspection enabled, error verbosity leaks, and injection vulnerabilities
  5. Race condition validator (new) — analyzes parallel request responses to confirm TOCTOU/double-spend vulnerabilities
  6. WAF integration in scanningscan_directories now probes for WAF presence and adapts rate limiting accordingly

Key Changes

src/kambo/models.py

  • Rewrote confidence_pct property to implement proper tier mapping:
    • TENTATIVE (0.0–0.99 weight): 10–39%
    • FIRM (1.0–1.99 weight): 50–79%
    • CONFIRMED (2.0+ weight): 85–99% (capped at 99, never 100)

src/kambo/validation.py

  • validate_xss: Fixed script/comment context detection by counting open/close tags instead of simple substring checks

    • Closed </script> tags no longer block XSS detection
    • Comment detection now checks if the most recent <!-- is actually closed by -->
  • validate_ssrf: Added 307/308 to accepted redirect status codes (permanent/temporary redirects that indicate server-side fetch)

  • validate_graphql (new): Three-vector GraphQL security validator

    • Introspection detection: identifies exposed __schema, __types, directives
    • Error verbosity analysis: detects field names, type info, and suggestions leaked in error messages
    • Injection detection: recognizes syntax errors, parse failures, and database errors from injected payloads
    • Bonus weight for large schemas (>5 named types)
  • validate_race_condition (new): TOCTOU/race condition analyzer

    • Compares parallel request outcomes (status codes, response hashes)
    • Detects when more successes occur than expected (e.g., coupon used twice)
    • Identifies identical vs. varied response bodies (same state applied multiple times vs. concurrent mutation)
    • Baseline comparison for expected vs. actual success counts
    • Proper FP checks for correctly serialized requests

src/kambo/tools/scanning.py

  • scan_directories: Integrated WAF detection and adaptive rate limiting
    • Probes target with a known-good request before fuzzing
    • Uses get_rate_limiter() to analyze probe response for WAF signatures
    • Adapts ffuf threading and delay flags based on WAF detection:
      • WAF detected: 2 threads, 500ms delay
      • Blocked response: 3 threads, 200ms delay
      • Normal: 10 threads, no delay
    • Returns WAF detection status and evasion tips in result

tests/test_refine_improvements.py (new)

  • Comprehensive test suite covering all six improvements:
    • 8 tests for confidence tier boundaries and transitions
    • 5 tests for XSS script/comment context handling
    • 5 tests for SSRF 307/308 acceptance
    • 10 tests for GraphQL introspection, error analysis, and injection
    • 8 tests for race condition detection and FP handling
    • 3 tests for rate limiter integration in scanning module

tests/test_models.py

  • Updated confidence percentage test to reflect new capping behavior (99% max, not 100%)

Implementation Details

  • Confidence formula: Uses piecewise linear scaling within each tier to ensure smooth transitions and no overl

https://claude.ai/code/session_01QrWLmqz6sz4tMVvDWHJqrU

Summary by Sourcery

Refine confidence scoring, enhance existing XSS/SSRF validators, add new GraphQL and race condition validators, and integrate WAF-aware rate limiting into directory scanning, with tests covering all changes.

New Features:

  • Introduce a GraphQL security validator to detect enabled introspection, verbose error leakage, and injection behavior.
  • Add a race condition validator that analyzes parallel request outcomes to confirm TOCTOU and double-spend style vulnerabilities.
  • Expose WAF detection results and adaptive rate limiting behavior from directory scans via the scanning API response.

Bug Fixes:

  • Correct XSS context detection so closed <script> blocks and properly terminated comments no longer suppress valid XSS findings.

Enhancements:

  • Rework confidence percentage mapping to provide tiered, non-overlapping confidence bands that cap confirmed issues below 100%.
  • Extend SSRF validation to treat 307 and 308 redirects as weak but valid indicators of server-side request behavior.
  • Make directory fuzzing scans WAF-aware by probing targets up front and adjusting ffuf threading and delays based on rate limiter analysis.

Tests:

  • Add a dedicated refine improvements test module covering confidence tiers, XSS context handling, SSRF redirect codes, GraphQL validation, race condition analysis, and WAF-aware scanning integration.
  • Update existing confidence percentage tests to reflect the new confirmed-tier capping behavior.

Bugs fixed:
- validate_xss: script context check now counts open/closed <script> tags
  instead of just checking presence — prevents FP when payload is after
  a closed </script> tag
- EvidenceChain.confidence_pct: new tiered formula (TENTATIVE 10-39%,
  FIRM 50-79%, CONFIRMED 85-99%) — old flat formula showed CONFIRMED as 80%
- validate_ssrf: added HTTP 307/308 as valid status codes for SSRF detection

New validators:
- validate_graphql: introspection detection, error verbosity analysis,
  and query injection detection (Did you mean / Cannot query field patterns
  handle raw JSON with escaped quotes via \W+ matching)
- validate_race_condition: TOCTOU detection via parallel response analysis,
  scales weight with extra successes, detects identical bodies

Integration:
- scan_directories: first tool to use rate_limiter — WAF probe before
  fuzzing, adaptive thread count (10 → 2 on WAF, 10 → 3 on block)
  with ffuf -p delay flag when WAF detected

Tests: +41 new tests (613 total, all passing)

https://claude.ai/code/session_01QrWLmqz6sz4tMVvDWHJqrU
@sourcery-ai

sourcery-ai Bot commented May 27, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refines confidence scoring and several validators (XSS, SSRF), introduces new GraphQL and race-condition validators, and integrates WAF-aware rate limiting into directory scanning, with a dedicated regression test suite for all six improvements.

Sequence diagram for WAF-aware scan_directories rate limiting

sequenceDiagram
    participant Scan as scan_directories
    participant Runner as get_runner
    participant Limiter as get_rate_limiter
    participant Ffuf as ffuf

    Scan->>Runner: get_runner()
    Scan->>Limiter: get_rate_limiter()

    Note over Scan: Build probe_cmd with curl to target root
    Scan->>Runner: run(probe_cmd, "scan_directories_waf_probe", target, Phase.SCANNING)
    Runner-->>Scan: probe_result

    Scan->>Limiter: record_request(target, response_body, headers)
    Limiter-->>Scan: probe_analysis

    alt probe_analysis.waf_detected
        Scan->>Scan: threads = 2, extra_flags = "-p 0.5"
    else probe_analysis.is_blocked
        Scan->>Scan: threads = 3, extra_flags = "-p 0.2"
    else normal
        Scan->>Scan: threads = 10, extra_flags = ""
    end

    Note over Scan: Build ffuf cmd with -t threads and extra_flags
    Scan->>Runner: run(cmd, "scan_directories", target, Phase.SCANNING)
    Runner-->>Scan: result
    Scan->>Scan: parse_ffuf(result.raw_output)

    Scan-->>Scan: return {target, scan_type,
    Scan-->>Scan:          waf_detected, waf_name,
    Scan-->>Scan:          rate_adapted, evasion_tips, **parsed}
Loading

File-Level Changes

Change Details Files
Refined confidence percentage mapping to tiered, piecewise-linear scaling with a non-100% CONFIRMED cap.
  • Replaced linear confidence_pct formula with tier-based mapping for TENTATIVE, FIRM, and CONFIRMED ranges
  • Implemented special-case handling for zero-weight chains to return 0% confidence
  • Updated tests to assert new tier boundaries and ensure CONFIRMED never reaches 100%
src/kambo/models.py
tests/test_models.py
tests/test_refine_improvements.py
Hardened XSS and SSRF validators and added specialized GraphQL validation logic.
  • Reworked validate_xss context detection to count open/close <script> tags and track unclosed HTML comments via last open/close markers
  • Extended validate_ssrf to treat 307/308 as valid redirect indicators and annotate redirect-related signals
  • Introduced validate_graphql with signatures for introspection, verbose error leakage, and injection indicators, including schema-size weighting and FP checks for non-GraphQL or disabled-introspection endpoints
src/kambo/validation.py
tests/test_refine_improvements.py
Added a race-condition validator that analyzes concurrent responses for TOCTOU-style vulnerabilities.
  • Implemented validate_race_condition to evaluate parallel response sets, counting successes vs expected, assessing body hash variance, and comparing against an optional baseline count
  • Added FP checks for all-failure or correctly serialized scenarios and scaled weights based on surplus successes
  • Created tests covering positive detections, FP conditions, identity vs variance signals, and weight scaling
src/kambo/validation.py
tests/test_refine_improvements.py
Integrated WAF-aware rate limiting into directory scanning using the shared rate_limiter singleton.
  • Introduced a preliminary curl-based probe in scan_directories to collect response data before fuzzing
  • Wired probe output through get_rate_limiter().record_request to infer WAF presence, blocking, and recommended pacing
  • Adjusted ffuf command construction to vary thread count and delay flags based on WAF/block status, and surfaced WAF metadata and evasion tips in the scan result structure
  • Added tests to assert rate_limiter integration, singleton behavior, WAF detection heuristics, and record_request’s return shape
src/kambo/tools/scanning.py
tests/test_refine_improvements.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown

Regression validator

Category Count
new 651

No regressions or failures detected.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/kambo/validation.py" line_range="1499-1501" />
<code_context>
+# validate_graphql — GraphQL Introspection & Injection
+# ---------------------------------------------------------------------------
+
+_GRAPHQL_INTROSPECTION_SIGNATURES: list[str] = [
+    r'"__schema"\s*:',                    # introspection response
+    r'"__types"\s*:',                     # type listing
+    r'"queryType"\s*:.*"name"',           # schema root types
+    r'"mutationType"\s*:',                # mutation support exposed
</code_context>
<issue_to_address>
**issue (bug_risk):** The '__types' introspection signature looks incorrect and may never match real GraphQL responses.

GraphQL introspection responses expose types under `__schema.types`, e.g. `{"data": {"__schema": {"types": [...]}}}`. The current `"__types"` pattern doesn’t match real responses, so this signature likely never fires. Consider matching `"types"` under `"__schema"` (or a more specific path) instead to detect introspection reliably.
</issue_to_address>

### Comment 2
<location path="tests/test_refine_improvements.py" line_range="339-348" />
<code_context>
+class TestScanDirectoriesRateLimiterIntegration:
</code_context>
<issue_to_address>
**suggestion (testing):** The scan_directories/WAF integration tests only verify imports and helpers, not the adaptive threading behavior of scan_directories itself

Since `scan_directories` now adapts `ffuf` threads/delay and returns WAF metadata, the tests should exercise it directly rather than just its collaborators. Please add an async integration-style test that monkeypatches `get_runner` and `get_rate_limiter` to return fakes:
- Fake rate limiter: return different `probe_analysis` dicts (e.g., `{"waf_detected": "Cloudflare"}`, `{ "is_blocked": True }`, and a normal case) and assert the constructed `ffuf` command includes the expected `-t` and `-p` values.
- Fake runner: return canned `probe_result` and `scan` output and assert the `scan_directories` result contains `waf_detected`, `waf_name`, `rate_adapted`, and `evasion_tips` with correct values.
This will validate the end-to-end WAF detection and rate adaptation behavior.

Suggested implementation:

```python
class TestScanDirectoriesRateLimiterIntegration:
    """Verify rate_limiter integration and adaptive behavior in scan_directories."""

    def test_rate_limiter_imported_in_scanning_module(self) -> None:
        import kambo.tools.scanning as scanning_module
        import kambo.rate_limiter as rl_module  # noqa: F401
        # The module should reference the rate limiter
        import inspect

        source = inspect.getsource(scanning_module)
        assert "get_rate_limiter" in source, "get_rate_limiter not used in scanning.py"
        assert "rate_limiter" in source.lower(), "rate_limiter not imported in scanning.py"

    async def test_scan_directories_waf_rate_adaptation(self, monkeypatch, tmp_path) -> None:
        """
        Integration-style test for scan_directories WAF detection and rate adaptation.

        We monkeypatch get_rate_limiter and get_runner to inject fakes, then assert:
        - ffuf command contains expected -t (threads) and -p (delay) based on probe_analysis
        - the returned scan_directories result contains WAF-related metadata
        """

        import kambo.tools.scanning as scanning_module

        captured_commands = []

        class FakeRunner:
            def __init__(self, probe_output: str, scan_output: str) -> None:
                self.probe_output = probe_output
                self.scan_output = scan_output

            async def run(self, cmd, *args, **kwargs):
                # record the command so we can assert on -t and -p
                captured_commands.append(list(cmd))
                # emulate the structure scan_directories expects from the runner
                return {
                    "probe_result": self.probe_output,
                    "scan_result": self.scan_output,
                }

        class FakeRateLimiter:
            def __init__(self, probe_analysis):
                self.probe_analysis = probe_analysis

            async def analyze_probe(self, *_args, **_kwargs):
                # emulate whatever scan_directories uses from the rate limiter
                return self.probe_analysis

        # Different WAF scenarios to exercise:
        scenarios = [
            {
                "probe_analysis": {"waf_detected": "Cloudflare", "threads": 5, "delay": 0.3},
                "expected_waf_name": "Cloudflare",
                "expected_rate_adapted": True,
            },
            {
                "probe_analysis": {"is_blocked": True, "threads": 1, "delay": 1.0},
                "expected_waf_name": None,
                "expected_rate_adapted": True,
            },
            {
                "probe_analysis": {"threads": 25, "delay": 0.0},
                "expected_waf_name": None,
                "expected_rate_adapted": False,
            },
        ]

        async def run_scenario(scenario):
            captured_commands.clear()

            def fake_get_rate_limiter(*_args, **_kwargs):
                # scan_directories should call this factory to get a rate limiter instance
                return FakeRateLimiter(probe_analysis=scenario["probe_analysis"])

            def fake_get_runner(*_args, **_kwargs):
                # scan_directories should call this factory to get a runner
                return FakeRunner(
                    probe_output="fake_probe_output",
                    scan_output="fake_scan_output",
                )

            monkeypatch.setattr(scanning_module, "get_rate_limiter", fake_get_rate_limiter)
            monkeypatch.setattr(scanning_module, "get_runner", fake_get_runner)

            # Minimal realistic call into scan_directories
            # (tmp_path provides a concrete wordlist path)
            wordlist = tmp_path / "words.txt"
            wordlist.write_text("admin\nlogin\n")

            result = await scanning_module.scan_directories(
                url="https://example.com",
                wordlist_path=str(wordlist),
            )

            # 1) Assert we actually invoked ffuf with a command that includes -t and -p
            assert captured_commands, "Runner was not invoked by scan_directories"
            ffuf_cmd = captured_commands[-1]
            assert "-t" in ffuf_cmd, "ffuf command missing -t (threads) flag"
            assert "-p" in ffuf_cmd, "ffuf command missing -p (delay) flag"

            t_index = ffuf_cmd.index("-t") + 1
            p_index = ffuf_cmd.index("-p") + 1

            # Note: we only assert presence/consistency, not exact numeric values,
            # because the mapping logic belongs to scan_directories.
            assert ffuf_cmd[t_index], "threads value after -t must not be empty"
            assert ffuf_cmd[p_index], "delay value after -p must not be empty"

            # 2) Assert WAF / rate-limiter metadata is surfaced in the result
            assert "waf_detected" in result
            assert "waf_name" in result
            assert "rate_adapted" in result
            assert "evasion_tips" in result

            expected_waf_name = scenario["expected_waf_name"]
            if expected_waf_name:
                assert result["waf_detected"] is True
                assert result["waf_name"] == expected_waf_name
            else:
                # for non-Cloudflare scenarios we just assert the key exists;
                # the implementation may use None/False/"" to indicate "no WAF"
                assert result["waf_detected"] in (False, None)

            if scenario["expected_rate_adapted"]:
                assert result["rate_adapted"] is True
                assert isinstance(result["evasion_tips"], (list, tuple))
            else:
                assert result["rate_adapted"] in (False, None)

        # Run all scenarios
        for scenario in scenarios:
            await run_scenario(scenario)

```

The fake `FakeRunner` and `FakeRateLimiter` in this test assume:
- `scan_directories` imports `get_runner` and `get_rate_limiter` from `kambo.tools.scanning` and calls them as factories.
- The object returned by `get_runner` exposes an async `run(cmd, *args, **kwargs)` that returns a dict with `"probe_result"` and `"scan_result"` keys.
- The object returned by `get_rate_limiter` exposes an async `analyze_probe(...)` method that returns the `probe_analysis` dict used to derive threads/delay and WAF metadata.
- `scan_directories` is an async function taking at least `url` and `wordlist_path` parameters and returning a dict with `"waf_detected"`, `"waf_name"`, `"rate_adapted"`, and `"evasion_tips"` keys.

If your actual `scan_directories` / rate-limiter / runner APIs differ, you should:
1. Adjust `FakeRunner.run`’s signature and returned structure to match what `scan_directories` expects.
2. Adjust `FakeRateLimiter`’s method name and signature to align with how the real rate limiter is used (e.g., `analyze`, `analyze_result`, etc.).
3. Update the monkeypatch targets (`scanning_module.get_runner`, `scanning_module.get_rate_limiter`) if those helpers live under different names or modules.
4. If `scan_directories` is synchronous, remove `async`/`await` usage and `pytest.mark.asyncio` (if present around the test) and call it directly instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/kambo/validation.py Outdated
Comment thread tests/test_refine_improvements.py Outdated
Comment on lines +339 to +348
class TestScanDirectoriesRateLimiterIntegration:
"""Verify rate_limiter is imported and used in scan_directories."""

def test_rate_limiter_imported_in_scanning_module(self) -> None:
import kambo.tools.scanning as scanning_module
import kambo.rate_limiter as rl_module
# The module should reference the rate limiter
import inspect
source = inspect.getsource(scanning_module)
assert "get_rate_limiter" in source, "get_rate_limiter not used in scanning.py"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): The scan_directories/WAF integration tests only verify imports and helpers, not the adaptive threading behavior of scan_directories itself

Since scan_directories now adapts ffuf threads/delay and returns WAF metadata, the tests should exercise it directly rather than just its collaborators. Please add an async integration-style test that monkeypatches get_runner and get_rate_limiter to return fakes:

  • Fake rate limiter: return different probe_analysis dicts (e.g., {"waf_detected": "Cloudflare"}, { "is_blocked": True }, and a normal case) and assert the constructed ffuf command includes the expected -t and -p values.
  • Fake runner: return canned probe_result and scan output and assert the scan_directories result contains waf_detected, waf_name, rate_adapted, and evasion_tips with correct values.
    This will validate the end-to-end WAF detection and rate adaptation behavior.

Suggested implementation:

class TestScanDirectoriesRateLimiterIntegration:
    """Verify rate_limiter integration and adaptive behavior in scan_directories."""

    def test_rate_limiter_imported_in_scanning_module(self) -> None:
        import kambo.tools.scanning as scanning_module
        import kambo.rate_limiter as rl_module  # noqa: F401
        # The module should reference the rate limiter
        import inspect

        source = inspect.getsource(scanning_module)
        assert "get_rate_limiter" in source, "get_rate_limiter not used in scanning.py"
        assert "rate_limiter" in source.lower(), "rate_limiter not imported in scanning.py"

    async def test_scan_directories_waf_rate_adaptation(self, monkeypatch, tmp_path) -> None:
        """
        Integration-style test for scan_directories WAF detection and rate adaptation.

        We monkeypatch get_rate_limiter and get_runner to inject fakes, then assert:
        - ffuf command contains expected -t (threads) and -p (delay) based on probe_analysis
        - the returned scan_directories result contains WAF-related metadata
        """

        import kambo.tools.scanning as scanning_module

        captured_commands = []

        class FakeRunner:
            def __init__(self, probe_output: str, scan_output: str) -> None:
                self.probe_output = probe_output
                self.scan_output = scan_output

            async def run(self, cmd, *args, **kwargs):
                # record the command so we can assert on -t and -p
                captured_commands.append(list(cmd))
                # emulate the structure scan_directories expects from the runner
                return {
                    "probe_result": self.probe_output,
                    "scan_result": self.scan_output,
                }

        class FakeRateLimiter:
            def __init__(self, probe_analysis):
                self.probe_analysis = probe_analysis

            async def analyze_probe(self, *_args, **_kwargs):
                # emulate whatever scan_directories uses from the rate limiter
                return self.probe_analysis

        # Different WAF scenarios to exercise:
        scenarios = [
            {
                "probe_analysis": {"waf_detected": "Cloudflare", "threads": 5, "delay": 0.3},
                "expected_waf_name": "Cloudflare",
                "expected_rate_adapted": True,
            },
            {
                "probe_analysis": {"is_blocked": True, "threads": 1, "delay": 1.0},
                "expected_waf_name": None,
                "expected_rate_adapted": True,
            },
            {
                "probe_analysis": {"threads": 25, "delay": 0.0},
                "expected_waf_name": None,
                "expected_rate_adapted": False,
            },
        ]

        async def run_scenario(scenario):
            captured_commands.clear()

            def fake_get_rate_limiter(*_args, **_kwargs):
                # scan_directories should call this factory to get a rate limiter instance
                return FakeRateLimiter(probe_analysis=scenario["probe_analysis"])

            def fake_get_runner(*_args, **_kwargs):
                # scan_directories should call this factory to get a runner
                return FakeRunner(
                    probe_output="fake_probe_output",
                    scan_output="fake_scan_output",
                )

            monkeypatch.setattr(scanning_module, "get_rate_limiter", fake_get_rate_limiter)
            monkeypatch.setattr(scanning_module, "get_runner", fake_get_runner)

            # Minimal realistic call into scan_directories
            # (tmp_path provides a concrete wordlist path)
            wordlist = tmp_path / "words.txt"
            wordlist.write_text("admin\nlogin\n")

            result = await scanning_module.scan_directories(
                url="https://example.com",
                wordlist_path=str(wordlist),
            )

            # 1) Assert we actually invoked ffuf with a command that includes -t and -p
            assert captured_commands, "Runner was not invoked by scan_directories"
            ffuf_cmd = captured_commands[-1]
            assert "-t" in ffuf_cmd, "ffuf command missing -t (threads) flag"
            assert "-p" in ffuf_cmd, "ffuf command missing -p (delay) flag"

            t_index = ffuf_cmd.index("-t") + 1
            p_index = ffuf_cmd.index("-p") + 1

            # Note: we only assert presence/consistency, not exact numeric values,
            # because the mapping logic belongs to scan_directories.
            assert ffuf_cmd[t_index], "threads value after -t must not be empty"
            assert ffuf_cmd[p_index], "delay value after -p must not be empty"

            # 2) Assert WAF / rate-limiter metadata is surfaced in the result
            assert "waf_detected" in result
            assert "waf_name" in result
            assert "rate_adapted" in result
            assert "evasion_tips" in result

            expected_waf_name = scenario["expected_waf_name"]
            if expected_waf_name:
                assert result["waf_detected"] is True
                assert result["waf_name"] == expected_waf_name
            else:
                # for non-Cloudflare scenarios we just assert the key exists;
                # the implementation may use None/False/"" to indicate "no WAF"
                assert result["waf_detected"] in (False, None)

            if scenario["expected_rate_adapted"]:
                assert result["rate_adapted"] is True
                assert isinstance(result["evasion_tips"], (list, tuple))
            else:
                assert result["rate_adapted"] in (False, None)

        # Run all scenarios
        for scenario in scenarios:
            await run_scenario(scenario)

The fake FakeRunner and FakeRateLimiter in this test assume:

  • scan_directories imports get_runner and get_rate_limiter from kambo.tools.scanning and calls them as factories.
  • The object returned by get_runner exposes an async run(cmd, *args, **kwargs) that returns a dict with "probe_result" and "scan_result" keys.
  • The object returned by get_rate_limiter exposes an async analyze_probe(...) method that returns the probe_analysis dict used to derive threads/delay and WAF metadata.
  • scan_directories is an async function taking at least url and wordlist_path parameters and returning a dict with "waf_detected", "waf_name", "rate_adapted", and "evasion_tips" keys.

If your actual scan_directories / rate-limiter / runner APIs differ, you should:

  1. Adjust FakeRunner.run’s signature and returned structure to match what scan_directories expects.
  2. Adjust FakeRateLimiter’s method name and signature to align with how the real rate limiter is used (e.g., analyze, analyze_result, etc.).
  3. Update the monkeypatch targets (scanning_module.get_runner, scanning_module.get_rate_limiter) if those helpers live under different names or modules.
  4. If scan_directories is synchronous, remove async/await usage and pytest.mark.asyncio (if present around the test) and call it directly instead.

…n tests

Comment 1 — validate_graphql: fix wrong '__types' introspection signature
  The pattern r'"__types"\s*:' never matched real GraphQL responses.
  Real introspection responses expose types under __schema.types as:
    {"data": {"__schema": {"types": [...]}}}
  Fixed to r'"types"\s*:\s*\[' which reliably matches the types
  array present in every introspection response.

Comment 2 — scan_directories: replace import-only checks with monkeypatched
  integration tests that exercise the actual adaptive behavior:
  - _fake_runner: stubs runner.run() — first call returns probe, second scan
  - _fake_limiter: stubs record_request() to return controlled probe_analysis
  - test_waf_detected_reduces_threads_to_2: Cloudflare → -t 2 -p 0.5
  - test_blocked_without_waf_reduces_threads_to_3: blocked → -t 3 -p 0.2
  - test_no_waf_uses_full_threads: clean → -t 10, no -p flag
  - test_result_always_contains_waf_metadata_keys: dict shape contract
  - test_probe_runs_before_ffuf: curl probe fires first, ffuf second

614 tests passing.

https://claude.ai/code/session_01QrWLmqz6sz4tMVvDWHJqrU
@ktfth
ktfth merged commit 4777e6a into main May 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants