prooflayer-runtime 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.
Files changed (45) hide show
  1. prooflayer/__init__.py +50 -0
  2. prooflayer/cli.py +362 -0
  3. prooflayer/config/__init__.py +6 -0
  4. prooflayer/config/allowlist.py +138 -0
  5. prooflayer/config/loader.py +29 -0
  6. prooflayer/detection/__init__.py +21 -0
  7. prooflayer/detection/engine.py +783 -0
  8. prooflayer/detection/models.py +49 -0
  9. prooflayer/detection/normalizer.py +245 -0
  10. prooflayer/detection/rules.py +104 -0
  11. prooflayer/detection/scanner.py +160 -0
  12. prooflayer/detection/scorer.py +65 -0
  13. prooflayer/detection/semantic.py +73 -0
  14. prooflayer/metrics.py +266 -0
  15. prooflayer/reporting/__init__.py +5 -0
  16. prooflayer/reporting/reporter.py +190 -0
  17. prooflayer/response/__init__.py +6 -0
  18. prooflayer/response/actions.py +152 -0
  19. prooflayer/response/killer.py +73 -0
  20. prooflayer/rules/command-injection.yaml +123 -0
  21. prooflayer/rules/data-exfiltration.yaml +83 -0
  22. prooflayer/rules/jailbreaks.yaml +67 -0
  23. prooflayer/rules/prompt-injection.yaml +99 -0
  24. prooflayer/rules/role-manipulation.yaml +60 -0
  25. prooflayer/rules/sql-injection.yaml +51 -0
  26. prooflayer/rules/ssrf-xxe.yaml +51 -0
  27. prooflayer/rules/tool-poisoning.yaml +46 -0
  28. prooflayer/runtime/__init__.py +21 -0
  29. prooflayer/runtime/interceptor.py +91 -0
  30. prooflayer/runtime/mcp_wrapper.py +395 -0
  31. prooflayer/runtime/middleware.py +86 -0
  32. prooflayer/runtime/transport.py +306 -0
  33. prooflayer/runtime/wrapper.py +265 -0
  34. prooflayer/utils/__init__.py +21 -0
  35. prooflayer/utils/encoding.py +87 -0
  36. prooflayer/utils/entropy.py +51 -0
  37. prooflayer/utils/logging.py +86 -0
  38. prooflayer/utils/masking.py +72 -0
  39. prooflayer/version.py +6 -0
  40. prooflayer_runtime-0.1.0.dist-info/METADATA +266 -0
  41. prooflayer_runtime-0.1.0.dist-info/RECORD +45 -0
  42. prooflayer_runtime-0.1.0.dist-info/WHEEL +5 -0
  43. prooflayer_runtime-0.1.0.dist-info/entry_points.txt +2 -0
  44. prooflayer_runtime-0.1.0.dist-info/licenses/LICENSE +4 -0
  45. prooflayer_runtime-0.1.0.dist-info/top_level.txt +1 -0
prooflayer/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ ProofLayer Runtime Security
3
+ ============================
4
+
5
+ Runtime prompt injection firewall for MCP servers.
6
+ Detects malicious prompts, kills compromised servers, generates security reports.
7
+
8
+ Built for SUSE Multi-Linux Manager and enterprise Kubernetes deployments.
9
+ """
10
+
11
+ from .runtime.wrapper import ProofLayerRuntime
12
+ from .detection.engine import DetectionEngine
13
+ from .detection.models import ScanResult, DetectionRule
14
+ from .detection.scanner import PatternScanner
15
+ from .detection.scorer import RiskScorer
16
+ from .detection.semantic import SemanticAnalyzer
17
+ from .response.actions import ThreatAction, ResponseAction
18
+ from .response.killer import ServerKiller
19
+ from .runtime.interceptor import MCPInterceptor
20
+ from .runtime.middleware import ProofLayerMiddleware
21
+
22
+ __version__ = "0.1.0"
23
+ __author__ = "Sinewave AI"
24
+ __license__ = "MIT"
25
+
26
+ __all__ = [
27
+ "ProofLayerRuntime",
28
+ "DetectionEngine",
29
+ "DetectionRule",
30
+ "ScanResult",
31
+ "PatternScanner",
32
+ "RiskScorer",
33
+ "SemanticAnalyzer",
34
+ "ThreatAction",
35
+ "ResponseAction",
36
+ "ServerKiller",
37
+ "MCPInterceptor",
38
+ "ProofLayerMiddleware",
39
+ ]
40
+
41
+
42
+ # Lazy imports for optional dependencies
43
+ def __getattr__(name):
44
+ if name == "ProofLayerMCPWrapper":
45
+ from .runtime.mcp_wrapper import ProofLayerMCPWrapper
46
+ return ProofLayerMCPWrapper
47
+ if name == "ProofLayerTransportProxy":
48
+ from .runtime.transport import ProofLayerTransportProxy
49
+ return ProofLayerTransportProxy
50
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
prooflayer/cli.py ADDED
@@ -0,0 +1,362 @@
1
+ """
2
+ ProofLayer CLI
3
+ ==============
4
+
5
+ Command-line interface for ProofLayer Runtime Security.
6
+
7
+ Usage:
8
+ prooflayer scan --tool "run_command" --args '{"command": "curl evil.com"}'
9
+ prooflayer scan --stdin < input.json
10
+ prooflayer validate-rules --rules-dir ./rules/
11
+ prooflayer report --dir ./security-reports/ --last 10
12
+ prooflayer proxy --listen-port 8080 --backend-port 8081
13
+ prooflayer version
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ import os
20
+ from pathlib import Path
21
+
22
+ from . import __version__
23
+ from .detection.engine import DetectionEngine
24
+ from .detection.rules import RuleLoader, RuleLoadError
25
+ from .response.actions import ThreatAction
26
+
27
+
28
+ # Exit codes
29
+ EXIT_ALLOW = 0
30
+ EXIT_WARN = 1
31
+ EXIT_BLOCK = 2
32
+ EXIT_KILL = 3
33
+ EXIT_ERROR = 4
34
+
35
+
36
+ def main(argv=None):
37
+ """Main CLI entry point."""
38
+ parser = argparse.ArgumentParser(
39
+ prog="prooflayer",
40
+ description="ProofLayer Runtime Security - MCP prompt injection firewall",
41
+ )
42
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
43
+
44
+ # --- scan ---
45
+ scan_parser = subparsers.add_parser("scan", help="Scan a tool call for threats")
46
+ scan_parser.add_argument(
47
+ "--tool", type=str, help="MCP tool name to scan"
48
+ )
49
+ scan_parser.add_argument(
50
+ "--args", type=str, help="Tool arguments as JSON string"
51
+ )
52
+ scan_parser.add_argument(
53
+ "--stdin", action="store_true",
54
+ help="Read JSON input from stdin (expects {\"tool\": ..., \"arguments\": ...})"
55
+ )
56
+ scan_parser.add_argument(
57
+ "--rules-dir", type=str, default=None,
58
+ help="Custom rules directory"
59
+ )
60
+ scan_parser.add_argument(
61
+ "--json", action="store_true", dest="output_json",
62
+ help="Output result as JSON"
63
+ )
64
+
65
+ # --- validate-rules ---
66
+ validate_parser = subparsers.add_parser(
67
+ "validate-rules", help="Validate YAML rule files"
68
+ )
69
+ validate_parser.add_argument(
70
+ "--rules-dir", type=str, required=True,
71
+ help="Directory containing YAML rule files"
72
+ )
73
+
74
+ # --- report ---
75
+ report_parser = subparsers.add_parser(
76
+ "report", help="View and summarize security reports"
77
+ )
78
+ report_parser.add_argument(
79
+ "--dir", type=str, default="./security-reports",
80
+ help="Directory containing security reports"
81
+ )
82
+ report_parser.add_argument(
83
+ "--last", type=int, default=10,
84
+ help="Number of recent reports to show"
85
+ )
86
+ report_parser.add_argument(
87
+ "--json", action="store_true", dest="output_json",
88
+ help="Output as JSON"
89
+ )
90
+
91
+ # --- proxy ---
92
+ proxy_parser = subparsers.add_parser(
93
+ "proxy", help="Start HTTP proxy for MCP server security scanning"
94
+ )
95
+ proxy_parser.add_argument(
96
+ "--listen-port", type=int, default=8080,
97
+ help="Port to listen on (default: 8080)"
98
+ )
99
+ proxy_parser.add_argument(
100
+ "--backend-port", type=int, default=8081,
101
+ help="Port where the MCP server is running (default: 8081)"
102
+ )
103
+ proxy_parser.add_argument(
104
+ "--backend-host", type=str, default="127.0.0.1",
105
+ help="Backend host (default: 127.0.0.1)"
106
+ )
107
+ proxy_parser.add_argument(
108
+ "--config", type=str, default=None,
109
+ help="Path to prooflayer.yaml config file"
110
+ )
111
+ proxy_parser.add_argument(
112
+ "--rules-dir", type=str, default=None,
113
+ help="Custom rules directory"
114
+ )
115
+ proxy_parser.add_argument(
116
+ "--report-dir", type=str, default="./security-reports",
117
+ help="Directory for security reports"
118
+ )
119
+
120
+ # --- version ---
121
+ subparsers.add_parser("version", help="Show version information")
122
+
123
+ args = parser.parse_args(argv)
124
+
125
+ if not args.command:
126
+ parser.print_help()
127
+ return EXIT_ERROR
128
+
129
+ if args.command == "scan":
130
+ return cmd_scan(args)
131
+ elif args.command == "validate-rules":
132
+ return cmd_validate_rules(args)
133
+ elif args.command == "report":
134
+ return cmd_report(args)
135
+ elif args.command == "proxy":
136
+ return cmd_proxy(args)
137
+ elif args.command == "version":
138
+ return cmd_version(args)
139
+
140
+ parser.print_help()
141
+ return EXIT_ERROR
142
+
143
+
144
+ def cmd_scan(args):
145
+ """Handle the 'scan' subcommand."""
146
+ tool_name = None
147
+ arguments = {}
148
+
149
+ if args.stdin:
150
+ try:
151
+ raw = sys.stdin.read()
152
+ data = json.loads(raw)
153
+ tool_name = data.get("tool")
154
+ arguments = data.get("arguments", {})
155
+ except json.JSONDecodeError as e:
156
+ print(f"ERROR: Invalid JSON from stdin: {e}", file=sys.stderr)
157
+ return EXIT_ERROR
158
+ else:
159
+ tool_name = args.tool
160
+ if args.args:
161
+ try:
162
+ arguments = json.loads(args.args)
163
+ except json.JSONDecodeError as e:
164
+ print(f"ERROR: Invalid JSON for --args: {e}", file=sys.stderr)
165
+ return EXIT_ERROR
166
+
167
+ if not tool_name:
168
+ print("ERROR: --tool is required (or use --stdin)", file=sys.stderr)
169
+ return EXIT_ERROR
170
+
171
+ try:
172
+ engine = DetectionEngine(rules_dir=args.rules_dir)
173
+ except RuleLoadError as e:
174
+ print(f"ERROR: Failed to load rules: {e}", file=sys.stderr)
175
+ return EXIT_ERROR
176
+
177
+ risk_score, matched_rules = engine.scan(tool_name=tool_name, arguments=arguments)
178
+
179
+ # Determine action from score
180
+ if risk_score <= 29:
181
+ action = ThreatAction.ALLOW
182
+ elif risk_score <= 69:
183
+ action = ThreatAction.WARN
184
+ elif risk_score <= 89:
185
+ action = ThreatAction.BLOCK
186
+ else:
187
+ action = ThreatAction.KILL
188
+
189
+ if getattr(args, "output_json", False):
190
+ result = {
191
+ "tool": tool_name,
192
+ "arguments": arguments,
193
+ "risk_score": risk_score,
194
+ "action": action.value,
195
+ "rules_matched": [
196
+ {
197
+ "id": r.id,
198
+ "severity": r.severity,
199
+ "message": r.message,
200
+ "category": r.category,
201
+ "score": r.score,
202
+ }
203
+ for r in matched_rules
204
+ ],
205
+ }
206
+ print(json.dumps(result, indent=2))
207
+ else:
208
+ rule_ids = ", ".join(r.id for r in matched_rules) if matched_rules else "none"
209
+ print(f"RISK: {risk_score}/100 | ACTION: {action.value} | Rules matched: {rule_ids}")
210
+
211
+ if matched_rules:
212
+ print()
213
+ for r in matched_rules:
214
+ print(f" [{r.severity.upper()}] {r.id}: {r.message} (+{r.score})")
215
+
216
+ # Map action to exit code
217
+ exit_codes = {
218
+ ThreatAction.ALLOW: EXIT_ALLOW,
219
+ ThreatAction.WARN: EXIT_WARN,
220
+ ThreatAction.BLOCK: EXIT_BLOCK,
221
+ ThreatAction.KILL: EXIT_KILL,
222
+ }
223
+ return exit_codes.get(action, EXIT_ERROR)
224
+
225
+
226
+ def cmd_validate_rules(args):
227
+ """Handle the 'validate-rules' subcommand."""
228
+ rules_dir = args.rules_dir
229
+ rules_path = Path(rules_dir)
230
+
231
+ if not rules_path.exists():
232
+ print(f"ERROR: Rules directory not found: {rules_dir}", file=sys.stderr)
233
+ return EXIT_ERROR
234
+
235
+ if not rules_path.is_dir():
236
+ print(f"ERROR: Not a directory: {rules_dir}", file=sys.stderr)
237
+ return EXIT_ERROR
238
+
239
+ yaml_files = list(rules_path.glob("*.yaml"))
240
+ if not yaml_files:
241
+ print(f"ERROR: No YAML files found in {rules_dir}", file=sys.stderr)
242
+ return EXIT_ERROR
243
+
244
+ total_rules = 0
245
+ errors = []
246
+
247
+ for yaml_file in sorted(yaml_files):
248
+ try:
249
+ rules = RuleLoader.load_from_file(str(yaml_file))
250
+ total_rules += len(rules)
251
+
252
+ # Check for compilation failures
253
+ for rule in rules:
254
+ if rule.compiled_pattern is None:
255
+ errors.append(f" {yaml_file.name}: rule '{rule.id}' pattern failed to compile")
256
+
257
+ print(f" {yaml_file.name}: {len(rules)} rules OK")
258
+
259
+ except Exception as e:
260
+ errors.append(f" {yaml_file.name}: {e}")
261
+
262
+ print()
263
+ if errors:
264
+ print(f"Loaded {total_rules} rules from {len(yaml_files)} files with ERRORS:")
265
+ for err in errors:
266
+ print(err)
267
+ return EXIT_ERROR
268
+ else:
269
+ print(f"Loaded {total_rules} rules from {len(yaml_files)} files. All patterns compile successfully.")
270
+ return EXIT_ALLOW
271
+
272
+
273
+ def cmd_report(args):
274
+ """Handle the 'report' subcommand."""
275
+ report_dir = Path(args.dir)
276
+
277
+ if not report_dir.exists():
278
+ print(f"ERROR: Report directory not found: {args.dir}", file=sys.stderr)
279
+ return EXIT_ERROR
280
+
281
+ # Find JSON report files
282
+ report_files = sorted(report_dir.glob("prooflayer-*.json"), reverse=True)
283
+
284
+ if not report_files:
285
+ print(f"No reports found in {args.dir}")
286
+ return EXIT_ALLOW
287
+
288
+ # Limit to --last N
289
+ report_files = report_files[: args.last]
290
+
291
+ reports = []
292
+ for rf in report_files:
293
+ try:
294
+ with open(rf, "r") as f:
295
+ report = json.load(f)
296
+ reports.append(report)
297
+ except (json.JSONDecodeError, OSError) as e:
298
+ print(f"WARNING: Skipping {rf.name}: {e}", file=sys.stderr)
299
+
300
+ if getattr(args, "output_json", False):
301
+ print(json.dumps(reports, indent=2))
302
+ else:
303
+ # Table format
304
+ print(f"{'TIMESTAMP':<28} {'TOOL':<20} {'SCORE':>5} {'ACTION':<8} {'RULES MATCHED'}")
305
+ print("-" * 95)
306
+
307
+ for report in reports:
308
+ threat = report.get("threat", {})
309
+ detection = report.get("detection", {})
310
+ timestamp = report.get("timestamp", "N/A")
311
+ tool = threat.get("tool", "N/A")
312
+ score = threat.get("risk_score", "N/A")
313
+ action = threat.get("action", "N/A")
314
+ rules = [r.get("id", "?") for r in detection.get("rules_matched", [])]
315
+ rules_str = ", ".join(rules) if rules else "none"
316
+
317
+ print(f"{timestamp:<28} {tool:<20} {score:>5} {action:<8} {rules_str}")
318
+
319
+ print()
320
+ print(f"Showing {len(reports)} of {len(list(report_dir.glob('prooflayer-*.json')))} reports")
321
+
322
+ return EXIT_ALLOW
323
+
324
+
325
+ def cmd_proxy(args):
326
+ """Handle the 'proxy' subcommand."""
327
+ from .runtime.transport import ProofLayerTransportProxy
328
+
329
+ try:
330
+ proxy = ProofLayerTransportProxy(
331
+ listen_port=args.listen_port,
332
+ backend_port=args.backend_port,
333
+ backend_host=args.backend_host,
334
+ rules_dir=args.rules_dir,
335
+ report_dir=args.report_dir,
336
+ )
337
+ except Exception as e:
338
+ print(f"ERROR: Failed to start proxy: {e}", file=sys.stderr)
339
+ return EXIT_ERROR
340
+
341
+ print(f"ProofLayer proxy listening on :{args.listen_port}")
342
+ print(f"Forwarding to {args.backend_host}:{args.backend_port}")
343
+ print(f"Security reports: {args.report_dir}")
344
+
345
+ try:
346
+ proxy.start()
347
+ except KeyboardInterrupt:
348
+ print("\nShutting down...")
349
+ proxy.stop()
350
+
351
+ return EXIT_ALLOW
352
+
353
+
354
+ def cmd_version(args):
355
+ """Handle the 'version' subcommand."""
356
+ print(f"ProofLayer Runtime Security v{__version__}")
357
+ print(f"Python {sys.version}")
358
+ return EXIT_ALLOW
359
+
360
+
361
+ if __name__ == "__main__":
362
+ sys.exit(main())
@@ -0,0 +1,6 @@
1
+ """Configuration loading and management."""
2
+
3
+ from .loader import ConfigLoader
4
+ from .allowlist import Allowlist
5
+
6
+ __all__ = ["ConfigLoader", "Allowlist"]
@@ -0,0 +1,138 @@
1
+ """
2
+ Allowlist
3
+ =========
4
+
5
+ Allowlisting mechanism for known-safe tool calls so legitimate
6
+ operations are not flagged by the detection engine.
7
+ """
8
+
9
+ import fnmatch
10
+ import logging
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Allowlist:
17
+ """
18
+ Manages allowlist rules loaded from YAML configuration.
19
+
20
+ Supports allowlisting by:
21
+ - Exact tool name
22
+ - Tool name glob pattern
23
+ - Argument value patterns (glob)
24
+ - Combined tool + argument bypass rules
25
+ """
26
+
27
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
28
+ """
29
+ Initialize allowlist from configuration dictionary.
30
+
31
+ Args:
32
+ config: The ``allowlist`` section from the YAML config.
33
+ Expected keys: tools, tool_patterns, argument_allowlist,
34
+ bypass_rules.
35
+ """
36
+ config = config or {}
37
+
38
+ self.tools: List[str] = config.get("tools", [])
39
+ self.tool_patterns: List[str] = config.get("tool_patterns", [])
40
+ self.argument_allowlist: Dict[str, List[str]] = config.get(
41
+ "argument_allowlist", {}
42
+ )
43
+ self.bypass_rules: List[Dict[str, Any]] = config.get("bypass_rules", [])
44
+
45
+ logger.info(
46
+ "Allowlist loaded: %d tools, %d patterns, %d argument rules, %d bypass rules",
47
+ len(self.tools),
48
+ len(self.tool_patterns),
49
+ len(self.argument_allowlist),
50
+ len(self.bypass_rules),
51
+ )
52
+
53
+ def is_allowed(
54
+ self, tool_name: str, arguments: Optional[Dict[str, Any]] = None
55
+ ) -> bool:
56
+ """
57
+ Check whether a tool call matches any allowlist rule.
58
+
59
+ Args:
60
+ tool_name: Name of the MCP tool being called.
61
+ arguments: Tool call arguments (may be None).
62
+
63
+ Returns:
64
+ True if the call is allowlisted and should skip detection.
65
+ """
66
+ arguments = arguments or {}
67
+
68
+ # 1. Exact tool name match
69
+ if tool_name in self.tools:
70
+ logger.info("Allowlisted by exact tool name: %s", tool_name)
71
+ return True
72
+
73
+ # 2. Tool name glob pattern match
74
+ for pattern in self.tool_patterns:
75
+ if fnmatch.fnmatch(tool_name, pattern):
76
+ logger.info(
77
+ "Allowlisted by tool pattern '%s': %s", pattern, tool_name
78
+ )
79
+ return True
80
+
81
+ # 3. Argument value pattern match — all specified argument keys must
82
+ # have a value matching at least one of the allowed patterns.
83
+ if self.argument_allowlist and arguments:
84
+ if self._arguments_match(arguments):
85
+ logger.info(
86
+ "Allowlisted by argument values: %s %s", tool_name, arguments
87
+ )
88
+ return True
89
+
90
+ # 4. Combined bypass rules
91
+ for rule in self.bypass_rules:
92
+ if self._bypass_rule_matches(rule, tool_name, arguments):
93
+ logger.info(
94
+ "Allowlisted by bypass rule for tool '%s': %s",
95
+ rule.get("tool"),
96
+ tool_name,
97
+ )
98
+ return True
99
+
100
+ return False
101
+
102
+ # ------------------------------------------------------------------
103
+ # Internal helpers
104
+ # ------------------------------------------------------------------
105
+
106
+ def _arguments_match(self, arguments: Dict[str, Any]) -> bool:
107
+ """Check if all specified argument keys match their allowed patterns."""
108
+ matched_any = False
109
+ for key, patterns in self.argument_allowlist.items():
110
+ value = arguments.get(key)
111
+ if value is None:
112
+ continue
113
+ value_str = str(value)
114
+ if any(fnmatch.fnmatch(value_str, p) for p in patterns):
115
+ matched_any = True
116
+ else:
117
+ # If the key is present but does not match, fail.
118
+ return False
119
+ return matched_any
120
+
121
+ def _bypass_rule_matches(
122
+ self,
123
+ rule: Dict[str, Any],
124
+ tool_name: str,
125
+ arguments: Dict[str, Any],
126
+ ) -> bool:
127
+ """Check if a combined bypass rule matches."""
128
+ rule_tool = rule.get("tool")
129
+ if rule_tool and rule_tool != tool_name:
130
+ return False
131
+
132
+ rule_args = rule.get("arguments", {})
133
+ for key, expected_value in rule_args.items():
134
+ actual = arguments.get(key)
135
+ if actual is None or str(actual) != str(expected_value):
136
+ return False
137
+
138
+ return True
@@ -0,0 +1,29 @@
1
+ """Configuration file loader."""
2
+
3
+ import yaml # type: ignore[import-untyped]
4
+ from typing import Dict, Any
5
+ from pathlib import Path
6
+
7
+
8
+ class ConfigLoader:
9
+ """Load ProofLayer configuration from YAML files."""
10
+
11
+ @staticmethod
12
+ def load(config_path: str) -> Dict[str, Any]:
13
+ """
14
+ Load configuration from YAML file.
15
+
16
+ Args:
17
+ config_path: Path to YAML config file
18
+
19
+ Returns:
20
+ Configuration dictionary
21
+ """
22
+ path = Path(config_path)
23
+ if not path.exists():
24
+ raise FileNotFoundError(f"Config file not found: {config_path}")
25
+
26
+ with open(path, "r") as f:
27
+ config = yaml.safe_load(f)
28
+
29
+ return config or {}
@@ -0,0 +1,21 @@
1
+ """Threat detection engine."""
2
+
3
+ from .models import DetectionRule, ScanResult
4
+ from .engine import DetectionEngine
5
+ from .rules import RuleLoadError
6
+ from .normalizer import normalize_text, flatten_arguments
7
+ from .scanner import PatternScanner
8
+ from .scorer import RiskScorer
9
+ from .semantic import SemanticAnalyzer
10
+
11
+ __all__ = [
12
+ "DetectionEngine",
13
+ "DetectionRule",
14
+ "ScanResult",
15
+ "RuleLoadError",
16
+ "PatternScanner",
17
+ "RiskScorer",
18
+ "SemanticAnalyzer",
19
+ "normalize_text",
20
+ "flatten_arguments",
21
+ ]