lucidscan 0.5.12__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 (91) hide show
  1. lucidscan/__init__.py +12 -0
  2. lucidscan/bootstrap/__init__.py +26 -0
  3. lucidscan/bootstrap/paths.py +160 -0
  4. lucidscan/bootstrap/platform.py +111 -0
  5. lucidscan/bootstrap/validation.py +76 -0
  6. lucidscan/bootstrap/versions.py +119 -0
  7. lucidscan/cli/__init__.py +50 -0
  8. lucidscan/cli/__main__.py +8 -0
  9. lucidscan/cli/arguments.py +405 -0
  10. lucidscan/cli/commands/__init__.py +64 -0
  11. lucidscan/cli/commands/autoconfigure.py +294 -0
  12. lucidscan/cli/commands/help.py +69 -0
  13. lucidscan/cli/commands/init.py +656 -0
  14. lucidscan/cli/commands/list_scanners.py +59 -0
  15. lucidscan/cli/commands/scan.py +307 -0
  16. lucidscan/cli/commands/serve.py +142 -0
  17. lucidscan/cli/commands/status.py +84 -0
  18. lucidscan/cli/commands/validate.py +105 -0
  19. lucidscan/cli/config_bridge.py +152 -0
  20. lucidscan/cli/exit_codes.py +17 -0
  21. lucidscan/cli/runner.py +284 -0
  22. lucidscan/config/__init__.py +29 -0
  23. lucidscan/config/ignore.py +178 -0
  24. lucidscan/config/loader.py +431 -0
  25. lucidscan/config/models.py +316 -0
  26. lucidscan/config/validation.py +645 -0
  27. lucidscan/core/__init__.py +3 -0
  28. lucidscan/core/domain_runner.py +463 -0
  29. lucidscan/core/git.py +174 -0
  30. lucidscan/core/logging.py +34 -0
  31. lucidscan/core/models.py +207 -0
  32. lucidscan/core/streaming.py +340 -0
  33. lucidscan/core/subprocess_runner.py +164 -0
  34. lucidscan/detection/__init__.py +21 -0
  35. lucidscan/detection/detector.py +154 -0
  36. lucidscan/detection/frameworks.py +270 -0
  37. lucidscan/detection/languages.py +328 -0
  38. lucidscan/detection/tools.py +229 -0
  39. lucidscan/generation/__init__.py +15 -0
  40. lucidscan/generation/config_generator.py +275 -0
  41. lucidscan/generation/package_installer.py +330 -0
  42. lucidscan/mcp/__init__.py +20 -0
  43. lucidscan/mcp/formatter.py +510 -0
  44. lucidscan/mcp/server.py +297 -0
  45. lucidscan/mcp/tools.py +1049 -0
  46. lucidscan/mcp/watcher.py +237 -0
  47. lucidscan/pipeline/__init__.py +17 -0
  48. lucidscan/pipeline/executor.py +187 -0
  49. lucidscan/pipeline/parallel.py +181 -0
  50. lucidscan/plugins/__init__.py +40 -0
  51. lucidscan/plugins/coverage/__init__.py +28 -0
  52. lucidscan/plugins/coverage/base.py +160 -0
  53. lucidscan/plugins/coverage/coverage_py.py +454 -0
  54. lucidscan/plugins/coverage/istanbul.py +411 -0
  55. lucidscan/plugins/discovery.py +107 -0
  56. lucidscan/plugins/enrichers/__init__.py +61 -0
  57. lucidscan/plugins/enrichers/base.py +63 -0
  58. lucidscan/plugins/linters/__init__.py +26 -0
  59. lucidscan/plugins/linters/base.py +125 -0
  60. lucidscan/plugins/linters/biome.py +448 -0
  61. lucidscan/plugins/linters/checkstyle.py +393 -0
  62. lucidscan/plugins/linters/eslint.py +368 -0
  63. lucidscan/plugins/linters/ruff.py +498 -0
  64. lucidscan/plugins/reporters/__init__.py +45 -0
  65. lucidscan/plugins/reporters/base.py +30 -0
  66. lucidscan/plugins/reporters/json_reporter.py +79 -0
  67. lucidscan/plugins/reporters/sarif_reporter.py +303 -0
  68. lucidscan/plugins/reporters/summary_reporter.py +61 -0
  69. lucidscan/plugins/reporters/table_reporter.py +81 -0
  70. lucidscan/plugins/scanners/__init__.py +57 -0
  71. lucidscan/plugins/scanners/base.py +60 -0
  72. lucidscan/plugins/scanners/checkov.py +484 -0
  73. lucidscan/plugins/scanners/opengrep.py +464 -0
  74. lucidscan/plugins/scanners/trivy.py +492 -0
  75. lucidscan/plugins/test_runners/__init__.py +27 -0
  76. lucidscan/plugins/test_runners/base.py +111 -0
  77. lucidscan/plugins/test_runners/jest.py +381 -0
  78. lucidscan/plugins/test_runners/karma.py +481 -0
  79. lucidscan/plugins/test_runners/playwright.py +434 -0
  80. lucidscan/plugins/test_runners/pytest.py +598 -0
  81. lucidscan/plugins/type_checkers/__init__.py +27 -0
  82. lucidscan/plugins/type_checkers/base.py +106 -0
  83. lucidscan/plugins/type_checkers/mypy.py +355 -0
  84. lucidscan/plugins/type_checkers/pyright.py +313 -0
  85. lucidscan/plugins/type_checkers/typescript.py +280 -0
  86. lucidscan-0.5.12.dist-info/METADATA +242 -0
  87. lucidscan-0.5.12.dist-info/RECORD +91 -0
  88. lucidscan-0.5.12.dist-info/WHEEL +5 -0
  89. lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
  90. lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
  91. lucidscan-0.5.12.dist-info/top_level.txt +1 -0
@@ -0,0 +1,303 @@
1
+ """SARIF reporter plugin for IDE integration.
2
+
3
+ Outputs scan results in SARIF 2.1.0 format, compatible with:
4
+ - GitHub Security tab (Code Scanning)
5
+ - VS Code SARIF Viewer extension
6
+ - Other SARIF-compatible tools
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Any, Dict, IO, List, Optional
13
+
14
+ from lucidscan.plugins.reporters.base import ReporterPlugin
15
+ from lucidscan.core.models import ScanResult, Severity, UnifiedIssue
16
+
17
+ # SARIF schema URL
18
+ SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
19
+ SARIF_VERSION = "2.1.0"
20
+
21
+ # Project information
22
+ LUCIDSCAN_INFO_URI = "https://github.com/voldeq/lucidscan"
23
+
24
+ # Severity mapping to SARIF security-severity (CVSS-aligned 0.0-10.0)
25
+ # and level (error, warning, note)
26
+ SEVERITY_MAP: Dict[Severity, Dict[str, Any]] = {
27
+ Severity.CRITICAL: {"security-severity": "9.5", "level": "error"},
28
+ Severity.HIGH: {"security-severity": "7.5", "level": "error"},
29
+ Severity.MEDIUM: {"security-severity": "5.5", "level": "warning"},
30
+ Severity.LOW: {"security-severity": "2.5", "level": "warning"},
31
+ Severity.INFO: {"security-severity": "0.0", "level": "note"},
32
+ }
33
+
34
+
35
+ class SARIFReporter(ReporterPlugin):
36
+ """Reporter plugin for SARIF 2.1.0 output format.
37
+
38
+ Produces SARIF JSON suitable for upload to GitHub Code Scanning
39
+ or viewing in VS Code with the SARIF Viewer extension.
40
+ """
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ return "sarif"
45
+
46
+ def report(self, result: ScanResult, output: IO[str]) -> None:
47
+ """Format and write the scan result as SARIF JSON.
48
+
49
+ Args:
50
+ result: The aggregated scan result to format.
51
+ output: Output stream to write the formatted result.
52
+ """
53
+ sarif_doc = self._build_sarif(result)
54
+ json.dump(sarif_doc, output, indent=2)
55
+ output.write("\n")
56
+
57
+ def _build_sarif(self, result: ScanResult) -> Dict[str, Any]:
58
+ """Build the complete SARIF document.
59
+
60
+ Args:
61
+ result: Scan result containing issues and metadata.
62
+
63
+ Returns:
64
+ SARIF document as a dictionary.
65
+ """
66
+ # Get lucidscan version from metadata or use default
67
+ version = "unknown"
68
+ if result.metadata:
69
+ version = result.metadata.lucidscan_version
70
+
71
+ # Collect unique rules from all issues
72
+ rules = self._collect_rules(result.issues)
73
+
74
+ # Convert issues to SARIF results
75
+ results = [
76
+ self._issue_to_result(issue)
77
+ for issue in result.issues
78
+ ]
79
+
80
+ return {
81
+ "$schema": SARIF_SCHEMA,
82
+ "version": SARIF_VERSION,
83
+ "runs": [
84
+ {
85
+ "tool": {
86
+ "driver": {
87
+ "name": "lucidscan",
88
+ "version": version,
89
+ "informationUri": LUCIDSCAN_INFO_URI,
90
+ "rules": rules,
91
+ }
92
+ },
93
+ "results": results,
94
+ }
95
+ ],
96
+ }
97
+
98
+ def _collect_rules(self, issues: List[UnifiedIssue]) -> List[Dict[str, Any]]:
99
+ """Collect unique rules from issues.
100
+
101
+ Each unique rule ID becomes a SARIF rule definition.
102
+
103
+ Args:
104
+ issues: List of unified issues to extract rules from.
105
+
106
+ Returns:
107
+ List of SARIF rule definitions.
108
+ """
109
+ rules_dict: Dict[str, Dict[str, Any]] = {}
110
+
111
+ for issue in issues:
112
+ rule_id = self._get_rule_id(issue)
113
+
114
+ # Skip if we already have this rule
115
+ if rule_id in rules_dict:
116
+ continue
117
+
118
+ # Build rule definition
119
+ rule = self._build_rule(issue, rule_id)
120
+ rules_dict[rule_id] = rule
121
+
122
+ return list(rules_dict.values())
123
+
124
+ def _build_rule(self, issue: UnifiedIssue, rule_id: str) -> Dict[str, Any]:
125
+ """Build a SARIF rule definition from an issue.
126
+
127
+ Args:
128
+ issue: The issue to build a rule from.
129
+ rule_id: The rule identifier.
130
+
131
+ Returns:
132
+ SARIF rule definition.
133
+ """
134
+ severity_info = SEVERITY_MAP.get(issue.severity, SEVERITY_MAP[Severity.MEDIUM])
135
+
136
+ rule: Dict[str, Any] = {
137
+ "id": rule_id,
138
+ "shortDescription": {
139
+ "text": self._truncate(issue.title, 1024),
140
+ },
141
+ "defaultConfiguration": {
142
+ "level": severity_info["level"],
143
+ },
144
+ "properties": {
145
+ "security-severity": severity_info["security-severity"],
146
+ },
147
+ }
148
+
149
+ # Add full description if we have more detail
150
+ if issue.description and issue.description != issue.title:
151
+ rule["fullDescription"] = {
152
+ "text": issue.description,
153
+ }
154
+
155
+ # Add help URI from documentation_url or recommendation
156
+ if issue.documentation_url:
157
+ rule["helpUri"] = issue.documentation_url
158
+ elif issue.recommendation:
159
+ # Check if recommendation contains a URL
160
+ if issue.recommendation.startswith("http"):
161
+ rule["helpUri"] = issue.recommendation
162
+ elif "See: " in issue.recommendation:
163
+ # Extract URL from "See: <url>" format
164
+ url = issue.recommendation.replace("See: ", "").strip()
165
+ if url.startswith("http"):
166
+ rule["helpUri"] = url
167
+
168
+ # Add CWE/OWASP tags from metadata if available
169
+ tool_metadata = issue.metadata
170
+ if tool_metadata:
171
+ tags = []
172
+ if "cwe" in tool_metadata:
173
+ cwe_ids = tool_metadata["cwe"]
174
+ if isinstance(cwe_ids, list):
175
+ tags.extend(cwe_ids)
176
+ elif cwe_ids:
177
+ tags.append(cwe_ids)
178
+ if "owasp" in tool_metadata:
179
+ owasp_ids = tool_metadata["owasp"]
180
+ if isinstance(owasp_ids, list):
181
+ tags.extend(owasp_ids)
182
+ elif owasp_ids:
183
+ tags.append(owasp_ids)
184
+ if tags:
185
+ rule["properties"]["tags"] = tags
186
+
187
+ return rule
188
+
189
+ def _issue_to_result(self, issue: UnifiedIssue) -> Dict[str, Any]:
190
+ """Convert a UnifiedIssue to a SARIF result.
191
+
192
+ Args:
193
+ issue: The issue to convert.
194
+
195
+ Returns:
196
+ SARIF result object.
197
+ """
198
+ rule_id = self._get_rule_id(issue)
199
+ severity_info = SEVERITY_MAP.get(issue.severity, SEVERITY_MAP[Severity.MEDIUM])
200
+
201
+ result: Dict[str, Any] = {
202
+ "ruleId": rule_id,
203
+ "message": {
204
+ "text": issue.description or issue.title,
205
+ },
206
+ "level": severity_info["level"],
207
+ "fingerprints": {
208
+ "v1": issue.id,
209
+ },
210
+ }
211
+
212
+ # Add location if we have file information
213
+ location = self._build_location(issue)
214
+ if location:
215
+ result["locations"] = [location]
216
+
217
+ return result
218
+
219
+ def _build_location(self, issue: UnifiedIssue) -> Optional[Dict[str, Any]]:
220
+ """Build a SARIF physical location from issue file info.
221
+
222
+ Args:
223
+ issue: The issue containing file location info.
224
+
225
+ Returns:
226
+ SARIF location object or None if no file info.
227
+ """
228
+ if not issue.file_path:
229
+ return None
230
+
231
+ # Use relative path for SARIF (strip leading slash if present)
232
+ file_uri = str(issue.file_path)
233
+ if file_uri.startswith("/"):
234
+ # Try to make it relative - just use the path as-is for now
235
+ # In practice, the path should already be relative or project-relative
236
+ pass
237
+
238
+ location: Dict[str, Any] = {
239
+ "physicalLocation": {
240
+ "artifactLocation": {
241
+ "uri": file_uri,
242
+ },
243
+ }
244
+ }
245
+
246
+ # Add region if we have line information
247
+ if issue.line_start is not None:
248
+ region: Dict[str, Any] = {
249
+ "startLine": issue.line_start,
250
+ }
251
+
252
+ if issue.line_end is not None:
253
+ region["endLine"] = issue.line_end
254
+
255
+ location["physicalLocation"]["region"] = region
256
+
257
+ return location
258
+
259
+ def _get_rule_id(self, issue: UnifiedIssue) -> str:
260
+ """Extract or generate a rule ID for an issue.
261
+
262
+ Uses the issue's rule_id field, falling back to scanner-specific
263
+ identifiers in metadata if needed.
264
+
265
+ Args:
266
+ issue: The issue to get a rule ID from.
267
+
268
+ Returns:
269
+ Rule identifier string.
270
+ """
271
+ # Use the new rule_id field if available
272
+ if issue.rule_id:
273
+ return issue.rule_id
274
+
275
+ # Fallback to metadata for older scanner output formats
276
+ metadata = issue.metadata
277
+
278
+ # Try scanner-specific IDs from metadata
279
+ if "vulnerability_id" in metadata:
280
+ return metadata["vulnerability_id"]
281
+ if "rule_id" in metadata:
282
+ return metadata["rule_id"]
283
+ if "check_id" in metadata:
284
+ return metadata["check_id"]
285
+
286
+ # Fallback: construct from source tool and issue title
287
+ # Use a simplified version of the title as rule ID
288
+ title_slug = issue.title.split(":")[0].strip()
289
+ return f"{issue.source_tool}/{title_slug}"
290
+
291
+ def _truncate(self, text: str, max_length: int) -> str:
292
+ """Truncate text to max length with ellipsis.
293
+
294
+ Args:
295
+ text: Text to truncate.
296
+ max_length: Maximum allowed length.
297
+
298
+ Returns:
299
+ Truncated text.
300
+ """
301
+ if len(text) <= max_length:
302
+ return text
303
+ return text[: max_length - 3] + "..."
@@ -0,0 +1,61 @@
1
+ """Summary reporter plugin for lucidscan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import IO, List
6
+
7
+ from lucidscan.core.models import ScanResult
8
+ from lucidscan.plugins.reporters.base import ReporterPlugin
9
+
10
+
11
+ class SummaryReporter(ReporterPlugin):
12
+ """Reporter plugin that outputs a brief scan summary.
13
+
14
+ Produces a concise summary with:
15
+ - Total issue count
16
+ - Breakdown by severity
17
+ - Breakdown by scanner domain
18
+ - Scan duration and project info
19
+ """
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ return "summary"
24
+
25
+ def report(self, result: ScanResult, output: IO[str]) -> None:
26
+ """Format scan result as a summary and write to output.
27
+
28
+ Args:
29
+ result: The scan result to format.
30
+ output: Output stream to write to.
31
+ """
32
+ lines = self._format_summary(result)
33
+ output.write("\n".join(lines))
34
+ output.write("\n")
35
+
36
+ def _format_summary(self, result: ScanResult) -> List[str]:
37
+ """Format scan result as a brief summary."""
38
+ lines: List[str] = []
39
+
40
+ if result.summary:
41
+ lines.append(f"Total issues: {result.summary.total}")
42
+
43
+ if result.summary.by_severity:
44
+ lines.append("\nBy severity:")
45
+ for sev in ["critical", "high", "medium", "low", "info"]:
46
+ count = result.summary.by_severity.get(sev, 0)
47
+ if count > 0:
48
+ lines.append(f" {sev.upper()}: {count}")
49
+
50
+ if result.summary.by_scanner:
51
+ lines.append("\nBy scanner domain:")
52
+ for scanner, count in result.summary.by_scanner.items():
53
+ lines.append(f" {scanner.upper()}: {count}")
54
+ else:
55
+ lines.append("No summary available.")
56
+
57
+ if result.metadata:
58
+ lines.append(f"\nScan duration: {result.metadata.duration_ms}ms")
59
+ lines.append(f"Project: {result.metadata.project_root}")
60
+
61
+ return lines
@@ -0,0 +1,81 @@
1
+ """Table reporter plugin for lucidscan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import IO, List
6
+
7
+ from lucidscan.core.models import ScanResult, Severity
8
+ from lucidscan.plugins.reporters.base import ReporterPlugin
9
+
10
+
11
+ class TableReporter(ReporterPlugin):
12
+ """Reporter plugin that outputs scan results as a human-readable table.
13
+
14
+ Produces a formatted table suitable for terminal display with:
15
+ - Severity-sorted issues
16
+ - Truncated fields for readability
17
+ - Summary statistics at the bottom
18
+ """
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return "table"
23
+
24
+ def report(self, result: ScanResult, output: IO[str]) -> None:
25
+ """Format scan result as a table and write to output.
26
+
27
+ Args:
28
+ result: The scan result to format.
29
+ output: Output stream to write to.
30
+ """
31
+ lines = self._format_table(result)
32
+ output.write("\n".join(lines))
33
+ output.write("\n")
34
+
35
+ def _format_table(self, result: ScanResult) -> List[str]:
36
+ """Format scan result as a human-readable table."""
37
+ lines: List[str] = []
38
+
39
+ if not result.issues:
40
+ lines.append("No issues found.")
41
+ return lines
42
+
43
+ # Header
44
+ lines.append(f"{'SEVERITY':<10} {'ID':<20} {'DEPENDENCY':<40} {'TITLE'}")
45
+ lines.append("-" * 100)
46
+
47
+ # Sort by severity
48
+ severity_order = {
49
+ Severity.CRITICAL: 0,
50
+ Severity.HIGH: 1,
51
+ Severity.MEDIUM: 2,
52
+ Severity.LOW: 3,
53
+ Severity.INFO: 4,
54
+ }
55
+
56
+ sorted_issues = sorted(
57
+ result.issues, key=lambda x: severity_order.get(x.severity, 5)
58
+ )
59
+
60
+ for issue in sorted_issues:
61
+ sev = issue.severity.value.upper()
62
+ # Use rule_id first, then check metadata for vulnerability_id, fallback to issue id
63
+ rule_id = issue.rule_id or issue.metadata.get("vulnerability_id", issue.id)
64
+ rule_id = rule_id[:20]
65
+ dep = (issue.dependency or "")[:40]
66
+ title = issue.title[:60] if len(issue.title) > 60 else issue.title
67
+ lines.append(f"{sev:<10} {rule_id:<20} {dep:<40} {title}")
68
+
69
+ # Summary
70
+ lines.append("")
71
+ lines.append("-" * 100)
72
+ if result.summary:
73
+ lines.append(f"Total: {result.summary.total} issues")
74
+ sev_parts = [
75
+ f"{sev}: {count}"
76
+ for sev, count in result.summary.by_severity.items()
77
+ ]
78
+ if sev_parts:
79
+ lines.append(f"By severity: {', '.join(sev_parts)}")
80
+
81
+ return lines
@@ -0,0 +1,57 @@
1
+ """Scanner plugins for integrating external security tools.
2
+
3
+ Plugins are discovered via Python entry points (lucidscan.scanners group).
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Dict, Optional, Type
8
+
9
+ from lucidscan.plugins.scanners.base import ScannerPlugin
10
+ from lucidscan.plugins.scanners.trivy import TrivyScanner
11
+ from lucidscan.plugins.scanners.opengrep import OpenGrepScanner
12
+ from lucidscan.plugins.scanners.checkov import CheckovScanner
13
+ from lucidscan.plugins import SCANNER_ENTRY_POINT_GROUP
14
+ from lucidscan.plugins.discovery import discover_plugins, get_plugin, list_available_plugins as _list_plugins
15
+
16
+
17
+ def discover_scanner_plugins() -> Dict[str, Type[ScannerPlugin]]:
18
+ """Discover all installed scanner plugins via entry points."""
19
+ return discover_plugins(SCANNER_ENTRY_POINT_GROUP, ScannerPlugin)
20
+
21
+
22
+ def get_scanner_plugin(
23
+ name: str,
24
+ project_root: Optional[Path] = None,
25
+ ) -> ScannerPlugin | None:
26
+ """Get an instantiated scanner plugin by name.
27
+
28
+ Args:
29
+ name: Scanner plugin name (e.g., 'trivy').
30
+ project_root: Optional project root for tool installation.
31
+ If provided, tools are installed to {project_root}/.lucidscan/
32
+
33
+ Returns:
34
+ Instantiated scanner plugin or None if not found.
35
+ """
36
+ kwargs = {}
37
+ if project_root:
38
+ kwargs["project_root"] = project_root
39
+ return get_plugin(SCANNER_ENTRY_POINT_GROUP, name, ScannerPlugin, **kwargs)
40
+
41
+
42
+ def list_available_scanners() -> list[str]:
43
+ """List names of all available scanner plugins."""
44
+ return _list_plugins(SCANNER_ENTRY_POINT_GROUP)
45
+
46
+
47
+ __all__ = [
48
+ "ScannerPlugin",
49
+ "TrivyScanner",
50
+ "OpenGrepScanner",
51
+ "CheckovScanner",
52
+ "discover_scanner_plugins",
53
+ "get_scanner_plugin",
54
+ "list_available_scanners",
55
+ ]
56
+
57
+
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from lucidscan.core.models import ScanContext, ScanDomain, UnifiedIssue
8
+
9
+
10
+ class ScannerPlugin(ABC):
11
+ """Base class for all scanner plugins.
12
+
13
+ Each scanner plugin wraps an underlying security tool and exposes it
14
+ through a common interface. Plugins are self-contained and manage
15
+ their own binary lifecycle.
16
+ """
17
+
18
+ def __init__(self, project_root: Optional[Path] = None, **kwargs) -> None:
19
+ """Initialize the scanner plugin.
20
+
21
+ Args:
22
+ project_root: Optional project root for tool installation.
23
+ **kwargs: Additional arguments for subclasses.
24
+ """
25
+ self.project_root = project_root
26
+
27
+ @property
28
+ @abstractmethod
29
+ def name(self) -> str:
30
+ """Plugin identifier (e.g., 'trivy', 'opengrep')."""
31
+
32
+ @property
33
+ @abstractmethod
34
+ def domains(self) -> List[ScanDomain]:
35
+ """Scan domains this plugin supports (SCA, SAST, IAC, CONTAINER)."""
36
+
37
+ @abstractmethod
38
+ def ensure_binary(self) -> Path:
39
+ """Ensure the scanner binary is available, downloading if needed.
40
+
41
+ Returns:
42
+ Path to the scanner binary.
43
+ """
44
+
45
+ @abstractmethod
46
+ def get_version(self) -> str:
47
+ """Return the version of the underlying scanner."""
48
+
49
+ @abstractmethod
50
+ def scan(self, context: ScanContext) -> List[UnifiedIssue]:
51
+ """Execute scan and return normalized issues.
52
+
53
+ Args:
54
+ context: Scan context containing target paths and configuration.
55
+
56
+ Returns:
57
+ List of unified issues found during the scan.
58
+ """
59
+
60
+