#!/usr/bin/env python

"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission

T1 - automatic WAF-bypass tamper selection (lib/utils/wafbypass.py). These cover the pure,
offline pieces: the identYwaf blind-signature decoder (which provocation vectors a known WAF
blocks), the data-ranked / DBMS-filtered / identYwaf-pruned candidate ordering, and the runtime
tamper loader. The end-to-end "adopt a tamper that restores detection" behaviour is exercised by
the --auto-tamper vuln-test case (lib/core/testing.py) against the vulnserver WAF emulator.
"""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()

from lib.utils.wafbypass import candidateTampers, identYwafBlockedVectors, loadTamper


class TestIdentYwafDecoder(unittest.TestCase):
    def test_known_waf_decodes_to_blocked_vectors(self):
        # cloudflare has bundled blind signatures -> a non-trivial set of blocked vector indices,
        # all within range of the 45 provocation vectors
        blocked = identYwafBlockedVectors("cloudflare")
        self.assertTrue(len(blocked) > 5)
        self.assertTrue(all(isinstance(_, int) and 0 <= _ < 45 for _ in blocked))

    def test_unknown_waf_is_empty(self):
        self.assertEqual(identYwafBlockedVectors("definitely-not-a-real-waf"), set())
        self.assertEqual(identYwafBlockedVectors(None), set())


class TestCandidateRanking(unittest.TestCase):
    def test_structural_first(self):
        cands = candidateTampers()
        # the empirically strongest structural substitutions lead, ahead of camouflage. The first
        # three are the ones measured to get through a CRS-class ruleset, so they are trialled first
        self.assertEqual(cands[:3], ["castprefix", "mid2leftright", "odbcbrace"])
        self.assertLess(cands.index("equaltolike"), cands.index("randomcase"))
        self.assertLess(cands.index("between"), cands.index("space2comment"))

    def test_no_dbms_prefiltering(self):
        # DBMS compatibility is verified at runtime (detection re-run through the tamper), not here,
        # so the full candidate set is offered regardless of any guessed back-end DBMS
        cands = candidateTampers()
        self.assertIn("versionedkeywords", cands)
        self.assertIn("space2hash", cands)
        self.assertIn("between", cands)

    def test_identYwaf_prior_prunes_camouflage(self):
        # a WAF whose profile blocks comment-obfuscated vectors should have comment-insertion
        # camouflage pruned (it cannot help there), while structural candidates survive
        base = candidateTampers()
        pruned = candidateTampers(identifiedWafs=["cloudflare"])
        self.assertIn("equaltolike", pruned)
        self.assertNotIn("space2comment", pruned)
        self.assertLessEqual(len(pruned), len(base))


class TestLoadTamper(unittest.TestCase):
    def test_loads_and_applies(self):
        fn = loadTamper("between")
        self.assertTrue(callable(fn))
        self.assertEqual(fn.__name__, "between")
        # the loaded function is the real tamper transform
        self.assertEqual(fn(payload="1 AND A>B"), "1 AND A NOT BETWEEN 0 AND B")

    def test_missing_returns_none_or_raises(self):
        # a non-existent script must not silently yield a bogus callable
        try:
            self.assertIsNone(loadTamper("no_such_tamper_script_xyz"))
        except Exception:
            pass   # an import error is also acceptable; what matters is no fake function


if __name__ == "__main__":
    unittest.main()
