evadex 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
evadex/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+ try:
3
+ __version__ = version("evadex")
4
+ except PackageNotFoundError:
5
+ __version__ = "0.1.0"
evadex/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from evadex.cli.app import main
2
+ if __name__ == "__main__":
3
+ main()
File without changes
@@ -0,0 +1,49 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, field
3
+ from typing import Optional
4
+ from evadex.core.result import Payload, Variant, ScanResult
5
+
6
+
7
+ class AdapterError(Exception):
8
+ pass
9
+
10
+
11
+ @dataclass
12
+ class AdapterConfig:
13
+ base_url: str = "http://localhost:8080"
14
+ api_key: Optional[str] = None
15
+ timeout: float = 30.0
16
+ extra: dict = field(default_factory=dict)
17
+
18
+ @classmethod
19
+ def from_dict(cls, d: dict) -> "AdapterConfig":
20
+ extra = {k: v for k, v in d.items() if k not in ("base_url", "api_key", "timeout")}
21
+ return cls(
22
+ base_url=d.get("base_url", "http://localhost:8080"),
23
+ api_key=d.get("api_key"),
24
+ timeout=float(d.get("timeout", 30.0)),
25
+ extra=extra,
26
+ )
27
+
28
+
29
+ class BaseAdapter(ABC):
30
+ name: str = "base"
31
+
32
+ def __init__(self, config: "dict | AdapterConfig"):
33
+ if isinstance(config, dict):
34
+ self.config = AdapterConfig.from_dict(config)
35
+ else:
36
+ self.config = config
37
+
38
+ @abstractmethod
39
+ async def submit(self, payload: Payload, variant: Variant) -> ScanResult:
40
+ pass
41
+
42
+ async def setup(self):
43
+ pass
44
+
45
+ async def teardown(self):
46
+ pass
47
+
48
+ async def health_check(self) -> bool:
49
+ return True
File without changes
@@ -0,0 +1,69 @@
1
+ from evadex.adapters.base import BaseAdapter, AdapterConfig
2
+ from evadex.adapters.dlpscan.client import DlpscanClient
3
+ from evadex.adapters.dlpscan.file_builder import FileBuilder
4
+ from evadex.core.registry import register_adapter
5
+ from evadex.core.result import Payload, Variant, ScanResult
6
+
7
+
8
+ @register_adapter("dlpscan")
9
+ class DlpscanAdapter(BaseAdapter):
10
+ name = "dlpscan"
11
+
12
+ def __init__(self, config):
13
+ super().__init__(config)
14
+ self._client = DlpscanClient(
15
+ base_url=self.config.base_url,
16
+ api_key=self.config.api_key,
17
+ timeout=self.config.timeout,
18
+ )
19
+ # Key in response JSON that indicates detection. Configurable via adapter extra config.
20
+ self._detected_key = self.config.extra.get("response_detected_key", "detected")
21
+
22
+ async def setup(self):
23
+ pass
24
+
25
+ async def teardown(self):
26
+ await self._client.close()
27
+
28
+ async def health_check(self) -> bool:
29
+ try:
30
+ await self._client.get_health()
31
+ return True
32
+ except Exception:
33
+ return False
34
+
35
+ async def submit(self, payload: Payload, variant: Variant) -> ScanResult:
36
+ strategy = variant.strategy
37
+ if strategy == "text":
38
+ raw = await self._client.post_text(variant.value)
39
+ else:
40
+ data, mime = FileBuilder.build(variant.value, strategy)
41
+ filename = f"evadex_test.{strategy}"
42
+ raw = await self._client.upload_file(data, filename, mime)
43
+
44
+ detected = self._parse_response(raw)
45
+ return ScanResult(payload=payload, variant=variant, detected=detected, raw_response=raw)
46
+
47
+ def _parse_response(self, raw: dict) -> bool:
48
+ # Try configured key first
49
+ if self._detected_key in raw:
50
+ val = raw[self._detected_key]
51
+ if isinstance(val, bool):
52
+ return val
53
+ if isinstance(val, (int, float)):
54
+ return bool(val)
55
+ if isinstance(val, str):
56
+ return val.lower() in ('true', '1', 'yes', 'detected')
57
+
58
+ # Try common response shapes
59
+ for key in ("detected", "found", "matches", "findings", "alert", "flagged"):
60
+ if key in raw:
61
+ val = raw[key]
62
+ if isinstance(val, bool):
63
+ return val
64
+ if isinstance(val, list):
65
+ return len(val) > 0
66
+ if isinstance(val, (int, float)):
67
+ return bool(val)
68
+
69
+ return False
@@ -0,0 +1,74 @@
1
+ import httpx
2
+ from typing import Optional
3
+ from evadex.adapters.base import AdapterError
4
+
5
+
6
+ class DlpscanClient:
7
+ def __init__(self, base_url: str, api_key: Optional[str] = None, timeout: float = 30.0):
8
+ self.base_url = base_url.rstrip('/')
9
+ headers = {"Content-Type": "application/json"}
10
+ if api_key:
11
+ headers["Authorization"] = f"Bearer {api_key}"
12
+ self._headers = headers
13
+ self._timeout = timeout
14
+ self._client: Optional[httpx.AsyncClient] = None
15
+
16
+ async def __aenter__(self):
17
+ self._client = httpx.AsyncClient(headers=self._headers, timeout=self._timeout)
18
+ return self
19
+
20
+ async def __aexit__(self, *args):
21
+ if self._client:
22
+ await self._client.aclose()
23
+
24
+ async def _get_client(self) -> httpx.AsyncClient:
25
+ if self._client is None:
26
+ self._client = httpx.AsyncClient(timeout=self._timeout)
27
+ return self._client
28
+
29
+ async def post_text(self, text: str) -> dict:
30
+ client = await self._get_client()
31
+ # Exclude Content-Type so httpx sets it correctly for JSON
32
+ headers = {k: v for k, v in self._headers.items() if k != "Content-Type"}
33
+ try:
34
+ resp = await client.post(
35
+ f"{self.base_url}/scan",
36
+ json={"content": text},
37
+ headers=headers,
38
+ )
39
+ resp.raise_for_status()
40
+ return resp.json()
41
+ except httpx.HTTPStatusError as e:
42
+ raise AdapterError(f"HTTP {e.response.status_code}: {e.response.text}") from e
43
+ except httpx.RequestError as e:
44
+ raise AdapterError(f"Request failed: {e}") from e
45
+
46
+ async def upload_file(self, data: bytes, filename: str, mime_type: str) -> dict:
47
+ client = await self._get_client()
48
+ headers = {k: v for k, v in self._headers.items() if k != "Content-Type"}
49
+ try:
50
+ resp = await client.post(
51
+ f"{self.base_url}/scan/file",
52
+ files={"file": (filename, data, mime_type)},
53
+ headers=headers,
54
+ )
55
+ resp.raise_for_status()
56
+ return resp.json()
57
+ except httpx.HTTPStatusError as e:
58
+ raise AdapterError(f"HTTP {e.response.status_code}: {e.response.text}") from e
59
+ except httpx.RequestError as e:
60
+ raise AdapterError(f"Request failed: {e}") from e
61
+
62
+ async def get_health(self) -> dict:
63
+ client = await self._get_client()
64
+ try:
65
+ resp = await client.get(f"{self.base_url}/health")
66
+ resp.raise_for_status()
67
+ return resp.json()
68
+ except Exception as e:
69
+ raise AdapterError(f"Health check failed: {e}") from e
70
+
71
+ async def close(self):
72
+ if self._client:
73
+ await self._client.aclose()
74
+ self._client = None
@@ -0,0 +1,112 @@
1
+ import io
2
+ from typing import Literal
3
+
4
+ try:
5
+ from docx import Document
6
+ HAS_DOCX = True
7
+ except ImportError:
8
+ HAS_DOCX = False
9
+
10
+ try:
11
+ from fpdf import FPDF
12
+ HAS_FPDF = True
13
+ except ImportError:
14
+ HAS_FPDF = False
15
+
16
+ try:
17
+ import openpyxl
18
+ HAS_OPENPYXL = True
19
+ except ImportError:
20
+ HAS_OPENPYXL = False
21
+
22
+
23
+ DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
24
+ PDF_MIME = "application/pdf"
25
+ XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
26
+
27
+ NOISE_SENTENCES = [
28
+ "This document contains confidential business information.",
29
+ "Please handle with care and do not distribute.",
30
+ "For internal use only.",
31
+ "Authorized personnel only.",
32
+ ]
33
+
34
+
35
+ class FileBuilder:
36
+ @staticmethod
37
+ def build(text: str, fmt: Literal["docx", "pdf", "xlsx"]) -> tuple[bytes, str]:
38
+ """Build an in-memory document containing text. Returns (bytes, mime_type).
39
+ Never writes to disk — uses io.BytesIO only.
40
+ """
41
+ if fmt == "docx":
42
+ return FileBuilder._build_docx(text), DOCX_MIME
43
+ elif fmt == "pdf":
44
+ return FileBuilder._build_pdf(text), PDF_MIME
45
+ elif fmt == "xlsx":
46
+ return FileBuilder._build_xlsx(text), XLSX_MIME
47
+ else:
48
+ raise ValueError(f"Unknown format: {fmt!r}")
49
+
50
+ @staticmethod
51
+ def _build_docx(text: str) -> bytes:
52
+ if not HAS_DOCX:
53
+ raise RuntimeError("python-docx is required for DOCX generation")
54
+ doc = Document()
55
+ doc.add_paragraph(NOISE_SENTENCES[0])
56
+ doc.add_paragraph(NOISE_SENTENCES[1])
57
+ doc.add_paragraph(text)
58
+ doc.add_paragraph(NOISE_SENTENCES[2])
59
+ buf = io.BytesIO()
60
+ doc.save(buf)
61
+ return buf.getvalue()
62
+
63
+ @staticmethod
64
+ def _build_pdf(text: str) -> bytes:
65
+ if not HAS_FPDF:
66
+ raise RuntimeError("fpdf2 is required for PDF generation")
67
+ pdf = FPDF()
68
+ pdf.add_page()
69
+ # Try DejaVu (bundled with fpdf2 >= 2.7.6) for full Unicode support.
70
+ # Fall back to Helvetica for ASCII-only content if DejaVu is unavailable.
71
+ try:
72
+ pdf.set_font("DejaVu", size=12)
73
+ except Exception:
74
+ # DejaVu not available — encode text to latin-1 safe subset
75
+ pdf.set_font("Helvetica", size=12)
76
+ text = text.encode("latin-1", errors="replace").decode("latin-1")
77
+
78
+ for sentence in NOISE_SENTENCES[:2]:
79
+ try:
80
+ pdf.cell(0, 10, sentence, new_x="LMARGIN", new_y="NEXT")
81
+ except Exception:
82
+ safe = sentence.encode("latin-1", errors="replace").decode("latin-1")
83
+ pdf.cell(0, 10, safe, new_x="LMARGIN", new_y="NEXT")
84
+
85
+ try:
86
+ pdf.cell(0, 10, text, new_x="LMARGIN", new_y="NEXT")
87
+ except Exception:
88
+ safe = text.encode("latin-1", errors="replace").decode("latin-1")
89
+ pdf.cell(0, 10, safe, new_x="LMARGIN", new_y="NEXT")
90
+
91
+ pdf.cell(0, 10, NOISE_SENTENCES[2], new_x="LMARGIN", new_y="NEXT")
92
+
93
+ buf = io.BytesIO()
94
+ pdf.output(buf)
95
+ return buf.getvalue()
96
+
97
+ @staticmethod
98
+ def _build_xlsx(text: str) -> bytes:
99
+ if not HAS_OPENPYXL:
100
+ raise RuntimeError("openpyxl is required for XLSX generation")
101
+ wb = openpyxl.Workbook()
102
+ ws = wb.active
103
+ ws['A1'] = NOISE_SENTENCES[0]
104
+ ws['A2'] = text
105
+ ws['A3'] = NOISE_SENTENCES[1]
106
+ ws['B1'] = "Document ID"
107
+ ws['B2'] = "12345"
108
+ ws['C1'] = "Classification"
109
+ ws['C2'] = "Confidential"
110
+ buf = io.BytesIO()
111
+ wb.save(buf)
112
+ return buf.getvalue()
File without changes
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+ import json
3
+ import tempfile
4
+ import os
5
+ from evadex.adapters.base import BaseAdapter
6
+ from evadex.adapters.dlpscan.file_builder import FileBuilder
7
+ from evadex.core.registry import register_adapter
8
+ from evadex.core.result import Payload, Variant, ScanResult
9
+
10
+
11
+ @register_adapter("dlpscan-cli")
12
+ class DlpscanCliAdapter(BaseAdapter):
13
+ name = "dlpscan-cli"
14
+
15
+ def __init__(self, config):
16
+ super().__init__(config)
17
+ self._exe = self.config.extra.get("executable", "dlpscan")
18
+
19
+ async def submit(self, payload: Payload, variant: Variant) -> ScanResult:
20
+ strategy = variant.strategy
21
+ loop = asyncio.get_event_loop()
22
+
23
+ if strategy == "text":
24
+ raw = await loop.run_in_executor(None, self._scan_text, variant.value)
25
+ else:
26
+ data, _ = FileBuilder.build(variant.value, strategy)
27
+ raw = await loop.run_in_executor(None, self._scan_bytes, data, strategy)
28
+
29
+ detected = len(raw) > 0
30
+ return ScanResult(payload=payload, variant=variant, detected=detected, raw_response={"matches": raw})
31
+
32
+ def _scan_text(self, text: str) -> list:
33
+ suffix = ".txt"
34
+ return self._run_on_tempfile(text.encode("utf-8"), suffix)
35
+
36
+ def _scan_bytes(self, data: bytes, fmt: str) -> list:
37
+ suffix = f".{fmt}"
38
+ return self._run_on_tempfile(data, suffix)
39
+
40
+ def _run_on_tempfile(self, data: bytes, suffix: str) -> list:
41
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
42
+ f.write(data)
43
+ path = f.name
44
+ try:
45
+ import subprocess
46
+ result = subprocess.run(
47
+ [self._exe, "-f", "json", path],
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=self.config.timeout,
51
+ )
52
+ if result.returncode != 0:
53
+ raise RuntimeError(f"dlpscan exited {result.returncode}: {result.stderr.strip()}")
54
+ return json.loads(result.stdout or "[]")
55
+ finally:
56
+ os.unlink(path)
evadex/cli/__init__.py ADDED
File without changes
evadex/cli/app.py ADDED
@@ -0,0 +1,15 @@
1
+ import click
2
+ from rich.console import Console
3
+ from evadex.cli.commands.scan import scan
4
+
5
+ console = Console()
6
+
7
+
8
+ @click.group()
9
+ @click.version_option(package_name="evadex")
10
+ def main():
11
+ """evadex — DLP evasion test suite."""
12
+ pass
13
+
14
+
15
+ main.add_command(scan)
File without changes
@@ -0,0 +1,114 @@
1
+ import sys
2
+ import click
3
+ from rich.console import Console
4
+ from evadex.core.registry import load_builtins, get_adapter, all_generators, get_generator
5
+ from evadex.core.engine import Engine
6
+ from evadex.core.result import Payload, PayloadCategory
7
+ from evadex.payloads.builtins import get_payloads, detect_category
8
+ from evadex.reporters.json_reporter import JsonReporter
9
+ from evadex.reporters.html_reporter import HtmlReporter
10
+
11
+ err_console = Console(stderr=True)
12
+
13
+ STRATEGY_CHOICES = click.Choice(["text", "docx", "pdf", "xlsx"])
14
+ CATEGORY_CHOICES = click.Choice([c.value for c in PayloadCategory])
15
+
16
+
17
+ @click.command()
18
+ @click.option("--tool", "-t", default="dlpscan", show_default=True, help="DLP adapter to use")
19
+ @click.option("--input", "-i", "input_value", default=None,
20
+ help="Single value to test (if omitted, runs all built-ins)")
21
+ @click.option("--format", "-f", "fmt", type=click.Choice(["json", "html"]),
22
+ default="json", show_default=True)
23
+ @click.option("--output", "-o", default=None, help="Output file path (default: stdout)")
24
+ @click.option("--url", default="http://localhost:8080", show_default=True,
25
+ help="Adapter base URL")
26
+ @click.option("--api-key", default=None, envvar="EVADEX_API_KEY",
27
+ help="API key for adapter")
28
+ @click.option("--timeout", default=30.0, show_default=True, type=float,
29
+ help="Request timeout in seconds")
30
+ @click.option("--strategy", "strategies", multiple=True, type=STRATEGY_CHOICES,
31
+ help="Submission strategies to use (default: all). Repeat for multiple.")
32
+ @click.option("--concurrency", default=5, show_default=True, type=int,
33
+ help="Max concurrent requests")
34
+ @click.option("--category", "categories", multiple=True, type=CATEGORY_CHOICES,
35
+ help="Filter built-in payloads by category. Repeat for multiple.")
36
+ @click.option("--variant-group", "variant_groups", multiple=True,
37
+ help="Limit to specific generator names. Repeat for multiple.")
38
+ def scan(
39
+ tool, input_value, fmt, output, url, api_key, timeout,
40
+ strategies, concurrency, categories, variant_groups,
41
+ ):
42
+ """Run DLP evasion tests."""
43
+ load_builtins()
44
+
45
+ # Resolve strategies
46
+ active_strategies = list(strategies) if strategies else ["text", "docx", "pdf", "xlsx"]
47
+
48
+ # Resolve payloads
49
+ if input_value:
50
+ category = detect_category(input_value)
51
+ payloads = [Payload(
52
+ value=input_value,
53
+ category=category,
54
+ label=f"Custom ({category.value})",
55
+ )]
56
+ else:
57
+ filter_cats = {PayloadCategory(c) for c in categories} if categories else None
58
+ payloads = get_payloads(filter_cats)
59
+
60
+ if not payloads:
61
+ err_console.print("[red]No payloads to test.[/red]")
62
+ sys.exit(1)
63
+
64
+ # Resolve adapter
65
+ config = {"base_url": url, "api_key": api_key, "timeout": timeout}
66
+ try:
67
+ adapter = get_adapter(tool, config)
68
+ except KeyError as e:
69
+ err_console.print(f"[red]{e}[/red]")
70
+ sys.exit(1)
71
+
72
+ # Resolve generators
73
+ if variant_groups:
74
+ try:
75
+ generators = [get_generator(name) for name in variant_groups]
76
+ except KeyError as e:
77
+ err_console.print(f"[red]{e}[/red]")
78
+ sys.exit(1)
79
+ else:
80
+ generators = None # use all registered
81
+
82
+ # Run engine
83
+ err_console.print(
84
+ f"[dim]Running evadex scan against [bold]{tool}[/bold] at {url}...[/dim]"
85
+ )
86
+ engine = Engine(
87
+ adapter=adapter,
88
+ generators=generators,
89
+ concurrency=concurrency,
90
+ strategies=active_strategies,
91
+ )
92
+ results = engine.run(payloads)
93
+
94
+ # Summary
95
+ total = len(results)
96
+ passes = sum(1 for r in results if r.detected)
97
+ fails = total - passes
98
+ err_console.print(
99
+ f"[green]Done.[/green] {total} tests \u2014 "
100
+ f"[green]{passes} detected[/green], [red]{fails} evaded[/red]"
101
+ )
102
+
103
+ # Report
104
+ reporter = HtmlReporter() if fmt == "html" else JsonReporter()
105
+ rendered = reporter.render(results)
106
+
107
+ if output:
108
+ with open(output, "w", encoding="utf-8") as f:
109
+ f.write(rendered)
110
+ err_console.print(f"[dim]Report written to {output}[/dim]")
111
+ else:
112
+ sys.stdout.buffer.write(rendered.encode("utf-8"))
113
+ sys.stdout.buffer.write(b"\n")
114
+ sys.stdout.buffer.flush()
File without changes
evadex/core/engine.py ADDED
@@ -0,0 +1,81 @@
1
+ import asyncio
2
+ import time
3
+ from typing import AsyncIterator
4
+ from evadex.core.result import Payload, ScanResult
5
+ from evadex.adapters.base import BaseAdapter
6
+ from evadex.variants.base import BaseVariantGenerator
7
+ from evadex.core.registry import all_generators
8
+
9
+
10
+ class Engine:
11
+ def __init__(
12
+ self,
13
+ adapter: BaseAdapter,
14
+ generators: list[BaseVariantGenerator] | None = None,
15
+ concurrency: int = 5,
16
+ strategies: list[str] | None = None,
17
+ ):
18
+ self.adapter = adapter
19
+ self.generators = generators # None = use all registered
20
+ self.concurrency = concurrency
21
+ self.strategies = strategies or ["text", "docx", "pdf", "xlsx"]
22
+
23
+ def run(self, payloads: list[Payload]) -> list[ScanResult]:
24
+ return asyncio.run(self._run_async_collect(payloads))
25
+
26
+ async def _run_async_collect(self, payloads: list[Payload]) -> list[ScanResult]:
27
+ results = []
28
+ async for r in self.run_async(payloads):
29
+ results.append(r)
30
+ return results
31
+
32
+ async def run_async(self, payloads: list[Payload]) -> AsyncIterator[ScanResult]:
33
+ generators = self.generators if self.generators is not None else all_generators()
34
+ sem = asyncio.Semaphore(self.concurrency)
35
+ tasks = []
36
+
37
+ for payload in payloads:
38
+ for gen in generators:
39
+ # Check applicable_categories
40
+ if hasattr(gen, 'applicable_categories') and gen.applicable_categories is not None:
41
+ if payload.category not in gen.applicable_categories:
42
+ continue
43
+ for variant in gen.generate(payload.value):
44
+ for strategy in self.strategies:
45
+ tasks.append(self._run_one(sem, payload, variant, strategy))
46
+
47
+ await self.adapter.setup()
48
+ try:
49
+ for coro in asyncio.as_completed(tasks):
50
+ result = await coro
51
+ yield result
52
+ finally:
53
+ await self.adapter.teardown()
54
+
55
+ async def _run_one(
56
+ self, sem: asyncio.Semaphore, payload: Payload, variant, strategy: str
57
+ ) -> ScanResult:
58
+ from evadex.core.result import Variant, ScanResult
59
+
60
+ # Clone variant with strategy
61
+ v = Variant(
62
+ value=variant.value,
63
+ generator=variant.generator,
64
+ technique=variant.technique,
65
+ transform_name=variant.transform_name,
66
+ strategy=strategy,
67
+ )
68
+ async with sem:
69
+ start = time.perf_counter()
70
+ try:
71
+ result = await self.adapter.submit(payload, v)
72
+ result.duration_ms = (time.perf_counter() - start) * 1000
73
+ return result
74
+ except Exception as e:
75
+ return ScanResult(
76
+ payload=payload,
77
+ variant=v,
78
+ detected=False,
79
+ error=str(e),
80
+ duration_ms=(time.perf_counter() - start) * 1000,
81
+ )
@@ -0,0 +1,46 @@
1
+ _GENERATORS: dict = {}
2
+ _ADAPTERS: dict = {}
3
+
4
+
5
+ def register_generator(name: str):
6
+ def decorator(cls):
7
+ _GENERATORS[name] = cls
8
+ return cls
9
+ return decorator
10
+
11
+
12
+ def register_adapter(name: str):
13
+ def decorator(cls):
14
+ _ADAPTERS[name] = cls
15
+ return cls
16
+ return decorator
17
+
18
+
19
+ def get_generator(name: str):
20
+ if name not in _GENERATORS:
21
+ raise KeyError(f"No generator registered: {name!r}")
22
+ return _GENERATORS[name]()
23
+
24
+
25
+ def get_adapter(name: str, config=None):
26
+ if name not in _ADAPTERS:
27
+ raise KeyError(f"No adapter registered: {name!r}. Available: {list(_ADAPTERS)}")
28
+ return _ADAPTERS[name](config or {})
29
+
30
+
31
+ def all_generators():
32
+ return [cls() for cls in _GENERATORS.values()]
33
+
34
+
35
+ def load_builtins():
36
+ # Import all variant modules so their @register_generator decorators fire
37
+ import evadex.variants.unicode_encoding
38
+ import evadex.variants.delimiter
39
+ import evadex.variants.splitting
40
+ import evadex.variants.leetspeak
41
+ import evadex.variants.regional_digits
42
+ import evadex.variants.structural
43
+ import evadex.variants.encoding
44
+ # Import adapters
45
+ import evadex.adapters.dlpscan.adapter
46
+ import evadex.adapters.dlpscan_cli.adapter