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.
- lucidscan/__init__.py +12 -0
- lucidscan/bootstrap/__init__.py +26 -0
- lucidscan/bootstrap/paths.py +160 -0
- lucidscan/bootstrap/platform.py +111 -0
- lucidscan/bootstrap/validation.py +76 -0
- lucidscan/bootstrap/versions.py +119 -0
- lucidscan/cli/__init__.py +50 -0
- lucidscan/cli/__main__.py +8 -0
- lucidscan/cli/arguments.py +405 -0
- lucidscan/cli/commands/__init__.py +64 -0
- lucidscan/cli/commands/autoconfigure.py +294 -0
- lucidscan/cli/commands/help.py +69 -0
- lucidscan/cli/commands/init.py +656 -0
- lucidscan/cli/commands/list_scanners.py +59 -0
- lucidscan/cli/commands/scan.py +307 -0
- lucidscan/cli/commands/serve.py +142 -0
- lucidscan/cli/commands/status.py +84 -0
- lucidscan/cli/commands/validate.py +105 -0
- lucidscan/cli/config_bridge.py +152 -0
- lucidscan/cli/exit_codes.py +17 -0
- lucidscan/cli/runner.py +284 -0
- lucidscan/config/__init__.py +29 -0
- lucidscan/config/ignore.py +178 -0
- lucidscan/config/loader.py +431 -0
- lucidscan/config/models.py +316 -0
- lucidscan/config/validation.py +645 -0
- lucidscan/core/__init__.py +3 -0
- lucidscan/core/domain_runner.py +463 -0
- lucidscan/core/git.py +174 -0
- lucidscan/core/logging.py +34 -0
- lucidscan/core/models.py +207 -0
- lucidscan/core/streaming.py +340 -0
- lucidscan/core/subprocess_runner.py +164 -0
- lucidscan/detection/__init__.py +21 -0
- lucidscan/detection/detector.py +154 -0
- lucidscan/detection/frameworks.py +270 -0
- lucidscan/detection/languages.py +328 -0
- lucidscan/detection/tools.py +229 -0
- lucidscan/generation/__init__.py +15 -0
- lucidscan/generation/config_generator.py +275 -0
- lucidscan/generation/package_installer.py +330 -0
- lucidscan/mcp/__init__.py +20 -0
- lucidscan/mcp/formatter.py +510 -0
- lucidscan/mcp/server.py +297 -0
- lucidscan/mcp/tools.py +1049 -0
- lucidscan/mcp/watcher.py +237 -0
- lucidscan/pipeline/__init__.py +17 -0
- lucidscan/pipeline/executor.py +187 -0
- lucidscan/pipeline/parallel.py +181 -0
- lucidscan/plugins/__init__.py +40 -0
- lucidscan/plugins/coverage/__init__.py +28 -0
- lucidscan/plugins/coverage/base.py +160 -0
- lucidscan/plugins/coverage/coverage_py.py +454 -0
- lucidscan/plugins/coverage/istanbul.py +411 -0
- lucidscan/plugins/discovery.py +107 -0
- lucidscan/plugins/enrichers/__init__.py +61 -0
- lucidscan/plugins/enrichers/base.py +63 -0
- lucidscan/plugins/linters/__init__.py +26 -0
- lucidscan/plugins/linters/base.py +125 -0
- lucidscan/plugins/linters/biome.py +448 -0
- lucidscan/plugins/linters/checkstyle.py +393 -0
- lucidscan/plugins/linters/eslint.py +368 -0
- lucidscan/plugins/linters/ruff.py +498 -0
- lucidscan/plugins/reporters/__init__.py +45 -0
- lucidscan/plugins/reporters/base.py +30 -0
- lucidscan/plugins/reporters/json_reporter.py +79 -0
- lucidscan/plugins/reporters/sarif_reporter.py +303 -0
- lucidscan/plugins/reporters/summary_reporter.py +61 -0
- lucidscan/plugins/reporters/table_reporter.py +81 -0
- lucidscan/plugins/scanners/__init__.py +57 -0
- lucidscan/plugins/scanners/base.py +60 -0
- lucidscan/plugins/scanners/checkov.py +484 -0
- lucidscan/plugins/scanners/opengrep.py +464 -0
- lucidscan/plugins/scanners/trivy.py +492 -0
- lucidscan/plugins/test_runners/__init__.py +27 -0
- lucidscan/plugins/test_runners/base.py +111 -0
- lucidscan/plugins/test_runners/jest.py +381 -0
- lucidscan/plugins/test_runners/karma.py +481 -0
- lucidscan/plugins/test_runners/playwright.py +434 -0
- lucidscan/plugins/test_runners/pytest.py +598 -0
- lucidscan/plugins/type_checkers/__init__.py +27 -0
- lucidscan/plugins/type_checkers/base.py +106 -0
- lucidscan/plugins/type_checkers/mypy.py +355 -0
- lucidscan/plugins/type_checkers/pyright.py +313 -0
- lucidscan/plugins/type_checkers/typescript.py +280 -0
- lucidscan-0.5.12.dist-info/METADATA +242 -0
- lucidscan-0.5.12.dist-info/RECORD +91 -0
- lucidscan-0.5.12.dist-info/WHEEL +5 -0
- lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
- lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
- lucidscan-0.5.12.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""AI instruction formatter for MCP tools.
|
|
2
|
+
|
|
3
|
+
Transforms UnifiedIssue objects into rich, AI-friendly fix instructions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, asdict, field
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from lucidscan.core.models import ScanDomain, Severity, ToolDomain, UnifiedIssue
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FixInstruction:
|
|
16
|
+
"""Rich fix instruction for AI agents."""
|
|
17
|
+
|
|
18
|
+
priority: int # 1 (highest) to 5 (lowest)
|
|
19
|
+
action: str # FIX_SECURITY_VULNERABILITY, FIX_TYPE_ERROR, etc.
|
|
20
|
+
summary: str # One-line summary
|
|
21
|
+
file: str
|
|
22
|
+
line: int
|
|
23
|
+
column: Optional[int] = None
|
|
24
|
+
problem: str = "" # Detailed problem description
|
|
25
|
+
fix_steps: List[str] = field(default_factory=list) # Ordered steps to fix
|
|
26
|
+
suggested_fix: Optional[str] = None # Suggested code replacement
|
|
27
|
+
current_code: Optional[str] = None # Current code snippet
|
|
28
|
+
documentation_url: Optional[str] = None
|
|
29
|
+
related_issues: List[str] = field(default_factory=list) # Related issue IDs
|
|
30
|
+
issue_id: str = "" # Original issue ID for reference
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InstructionFormatter:
|
|
34
|
+
"""Transforms UnifiedIssue to AI-friendly instructions."""
|
|
35
|
+
|
|
36
|
+
SEVERITY_PRIORITY = {
|
|
37
|
+
Severity.CRITICAL: 1,
|
|
38
|
+
Severity.HIGH: 2,
|
|
39
|
+
Severity.MEDIUM: 3,
|
|
40
|
+
Severity.LOW: 4,
|
|
41
|
+
Severity.INFO: 5,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Map both ScanDomain and ToolDomain to action prefixes
|
|
45
|
+
DOMAIN_ACTION_PREFIX = {
|
|
46
|
+
# ScanDomain values
|
|
47
|
+
ScanDomain.SCA: "FIX_DEPENDENCY_",
|
|
48
|
+
ScanDomain.SAST: "FIX_SECURITY_",
|
|
49
|
+
ScanDomain.IAC: "FIX_INFRASTRUCTURE_",
|
|
50
|
+
ScanDomain.CONTAINER: "FIX_CONTAINER_",
|
|
51
|
+
# ToolDomain values
|
|
52
|
+
ToolDomain.LINTING: "FIX_LINTING_",
|
|
53
|
+
ToolDomain.TYPE_CHECKING: "FIX_TYPE_",
|
|
54
|
+
ToolDomain.SECURITY: "FIX_SECURITY_",
|
|
55
|
+
ToolDomain.TESTING: "FIX_TEST_",
|
|
56
|
+
ToolDomain.COVERAGE: "IMPROVE_COVERAGE_",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def format_scan_result(
|
|
60
|
+
self,
|
|
61
|
+
issues: List[UnifiedIssue],
|
|
62
|
+
checked_domains: Optional[List[str]] = None,
|
|
63
|
+
) -> Dict[str, Any]:
|
|
64
|
+
"""Format scan result as AI instructions.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
issues: List of unified issues from scan.
|
|
68
|
+
checked_domains: List of domain names that were checked.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary with structured AI instructions including:
|
|
72
|
+
- total_issues: Count of all issues
|
|
73
|
+
- blocking: Whether there are high-priority issues
|
|
74
|
+
- summary: Human-readable summary
|
|
75
|
+
- severity_counts: Issues by severity level
|
|
76
|
+
- domain_status: Pass/fail status for each checked domain
|
|
77
|
+
- issues_by_domain: Issues grouped by domain
|
|
78
|
+
- instructions: Sorted list of fix instructions
|
|
79
|
+
- recommended_action: Suggested next step
|
|
80
|
+
"""
|
|
81
|
+
instructions = [self._issue_to_instruction(issue) for issue in issues]
|
|
82
|
+
|
|
83
|
+
# Sort by priority
|
|
84
|
+
instructions.sort(key=lambda x: x.priority)
|
|
85
|
+
|
|
86
|
+
# Count by severity
|
|
87
|
+
severity_counts: dict[str, int] = {}
|
|
88
|
+
for issue in issues:
|
|
89
|
+
sev_name = issue.severity.value if issue.severity else "unknown"
|
|
90
|
+
severity_counts[sev_name] = severity_counts.get(sev_name, 0) + 1
|
|
91
|
+
|
|
92
|
+
# Group issues by domain
|
|
93
|
+
issues_by_domain: Dict[str, List[Dict[str, Any]]] = {}
|
|
94
|
+
for issue in issues:
|
|
95
|
+
domain_name = issue.domain.value if issue.domain else "unknown"
|
|
96
|
+
if domain_name not in issues_by_domain:
|
|
97
|
+
issues_by_domain[domain_name] = []
|
|
98
|
+
issues_by_domain[domain_name].append(
|
|
99
|
+
self._issue_to_brief(issue)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Build domain status (pass/fail for each checked domain)
|
|
103
|
+
domain_status: Dict[str, Dict[str, Any]] = {}
|
|
104
|
+
if checked_domains:
|
|
105
|
+
for domain in checked_domains:
|
|
106
|
+
domain_issues = issues_by_domain.get(domain, [])
|
|
107
|
+
issue_count = len(domain_issues)
|
|
108
|
+
fixable_count = sum(1 for i in domain_issues if i.get("fixable", False))
|
|
109
|
+
|
|
110
|
+
if issue_count == 0:
|
|
111
|
+
status = "pass"
|
|
112
|
+
status_display = "Pass"
|
|
113
|
+
else:
|
|
114
|
+
status = "fail"
|
|
115
|
+
if fixable_count > 0:
|
|
116
|
+
status_display = f"{issue_count} issues ({fixable_count} auto-fixable)"
|
|
117
|
+
else:
|
|
118
|
+
status_display = f"{issue_count} issues"
|
|
119
|
+
|
|
120
|
+
domain_status[domain] = {
|
|
121
|
+
"status": status,
|
|
122
|
+
"display": status_display,
|
|
123
|
+
"issue_count": issue_count,
|
|
124
|
+
"fixable_count": fixable_count,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Generate recommended action
|
|
128
|
+
recommended_action = self._generate_recommended_action(
|
|
129
|
+
issues, severity_counts, domain_status
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"total_issues": len(issues),
|
|
134
|
+
"blocking": any(i.priority <= 2 for i in instructions),
|
|
135
|
+
"summary": self._generate_summary(issues, severity_counts),
|
|
136
|
+
"severity_counts": severity_counts,
|
|
137
|
+
"domain_status": domain_status,
|
|
138
|
+
"issues_by_domain": issues_by_domain,
|
|
139
|
+
"instructions": [asdict(i) for i in instructions],
|
|
140
|
+
"recommended_action": recommended_action,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def format_single_issue(
|
|
144
|
+
self,
|
|
145
|
+
issue: UnifiedIssue,
|
|
146
|
+
detailed: bool = False,
|
|
147
|
+
) -> Dict[str, Any]:
|
|
148
|
+
"""Format a single issue for AI consumption.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
issue: The issue to format.
|
|
152
|
+
detailed: Whether to include extra detail.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dictionary with issue details and fix instructions.
|
|
156
|
+
"""
|
|
157
|
+
instruction = self._issue_to_instruction(issue, detailed=detailed)
|
|
158
|
+
return asdict(instruction)
|
|
159
|
+
|
|
160
|
+
def _issue_to_instruction(
|
|
161
|
+
self,
|
|
162
|
+
issue: UnifiedIssue,
|
|
163
|
+
detailed: bool = False,
|
|
164
|
+
) -> FixInstruction:
|
|
165
|
+
"""Convert UnifiedIssue to FixInstruction.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
issue: The unified issue.
|
|
169
|
+
detailed: Whether to include extra detail.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
FixInstruction instance.
|
|
173
|
+
"""
|
|
174
|
+
file_path = str(issue.file_path) if issue.file_path else ""
|
|
175
|
+
|
|
176
|
+
return FixInstruction(
|
|
177
|
+
priority=self.SEVERITY_PRIORITY.get(issue.severity, 3),
|
|
178
|
+
action=self._generate_action(issue),
|
|
179
|
+
summary=self._generate_summary_line(issue),
|
|
180
|
+
file=file_path,
|
|
181
|
+
line=issue.line_start or 0,
|
|
182
|
+
column=issue.column_start,
|
|
183
|
+
problem=issue.description or "",
|
|
184
|
+
fix_steps=self._generate_fix_steps(issue, detailed),
|
|
185
|
+
suggested_fix=self._generate_suggested_fix(issue),
|
|
186
|
+
current_code=issue.code_snippet,
|
|
187
|
+
documentation_url=issue.documentation_url,
|
|
188
|
+
related_issues=[],
|
|
189
|
+
issue_id=issue.id,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _generate_action(self, issue: UnifiedIssue) -> str:
|
|
193
|
+
"""Generate action type from issue.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
issue: The unified issue.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Action string like FIX_SECURITY_VULNERABILITY.
|
|
200
|
+
"""
|
|
201
|
+
prefix = self.DOMAIN_ACTION_PREFIX.get(issue.domain, "FIX_")
|
|
202
|
+
title_lower = issue.title.lower() if issue.title else ""
|
|
203
|
+
domain = issue.domain
|
|
204
|
+
|
|
205
|
+
# Specific action types based on issue characteristics
|
|
206
|
+
# Handle both ScanDomain and ToolDomain
|
|
207
|
+
if domain in (ScanDomain.SAST, ToolDomain.SECURITY):
|
|
208
|
+
if "hardcoded" in title_lower or "secret" in title_lower:
|
|
209
|
+
return f"{prefix}HARDCODED_SECRET"
|
|
210
|
+
elif "injection" in title_lower:
|
|
211
|
+
return f"{prefix}INJECTION"
|
|
212
|
+
elif "xss" in title_lower:
|
|
213
|
+
return f"{prefix}XSS"
|
|
214
|
+
return f"{prefix}VULNERABILITY"
|
|
215
|
+
|
|
216
|
+
if domain == ScanDomain.SCA:
|
|
217
|
+
return f"{prefix}VULNERABILITY"
|
|
218
|
+
|
|
219
|
+
if domain == ScanDomain.IAC:
|
|
220
|
+
if "exposed" in title_lower or "public" in title_lower:
|
|
221
|
+
return f"{prefix}EXPOSURE"
|
|
222
|
+
return f"{prefix}MISCONFIGURATION"
|
|
223
|
+
|
|
224
|
+
if domain == ScanDomain.CONTAINER:
|
|
225
|
+
return f"{prefix}VULNERABILITY"
|
|
226
|
+
|
|
227
|
+
if domain == ToolDomain.LINTING:
|
|
228
|
+
return f"{prefix}ERROR"
|
|
229
|
+
|
|
230
|
+
if domain == ToolDomain.TYPE_CHECKING:
|
|
231
|
+
return f"{prefix}ERROR"
|
|
232
|
+
|
|
233
|
+
if domain == ToolDomain.TESTING:
|
|
234
|
+
return f"{prefix}FAILURE"
|
|
235
|
+
|
|
236
|
+
if domain == ToolDomain.COVERAGE:
|
|
237
|
+
return f"{prefix}GAP"
|
|
238
|
+
|
|
239
|
+
return "FIX_ISSUE"
|
|
240
|
+
|
|
241
|
+
def _generate_summary_line(self, issue: UnifiedIssue) -> str:
|
|
242
|
+
"""Generate one-line summary for issue.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
issue: The unified issue.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Summary string.
|
|
249
|
+
"""
|
|
250
|
+
file_part = ""
|
|
251
|
+
if issue.file_path:
|
|
252
|
+
file_name = issue.file_path.name if hasattr(issue.file_path, "name") else str(issue.file_path).split("/")[-1]
|
|
253
|
+
if issue.line_start:
|
|
254
|
+
file_part = f" in {file_name}:{issue.line_start}"
|
|
255
|
+
else:
|
|
256
|
+
file_part = f" in {file_name}"
|
|
257
|
+
|
|
258
|
+
return f"{issue.title}{file_part}"
|
|
259
|
+
|
|
260
|
+
def _generate_summary(
|
|
261
|
+
self,
|
|
262
|
+
issues: List[UnifiedIssue],
|
|
263
|
+
severity_counts: Dict[str, int],
|
|
264
|
+
) -> str:
|
|
265
|
+
"""Generate overall summary string.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
issues: List of issues.
|
|
269
|
+
severity_counts: Count by severity.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Summary string.
|
|
273
|
+
"""
|
|
274
|
+
if not issues:
|
|
275
|
+
return "No issues found"
|
|
276
|
+
|
|
277
|
+
parts = []
|
|
278
|
+
for sev in ["critical", "high", "medium", "low", "info"]:
|
|
279
|
+
count = severity_counts.get(sev, 0)
|
|
280
|
+
if count > 0:
|
|
281
|
+
parts.append(f"{count} {sev}")
|
|
282
|
+
|
|
283
|
+
return f"{len(issues)} issues found: {', '.join(parts)}"
|
|
284
|
+
|
|
285
|
+
def _generate_fix_steps(
|
|
286
|
+
self,
|
|
287
|
+
issue: UnifiedIssue,
|
|
288
|
+
detailed: bool = False,
|
|
289
|
+
) -> List[str]:
|
|
290
|
+
"""Generate fix steps from issue context.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
issue: The unified issue.
|
|
294
|
+
detailed: Whether to include extra detail.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of fix steps.
|
|
298
|
+
"""
|
|
299
|
+
steps = []
|
|
300
|
+
|
|
301
|
+
# Use recommendation if available
|
|
302
|
+
if issue.recommendation:
|
|
303
|
+
steps.append(issue.recommendation)
|
|
304
|
+
|
|
305
|
+
# Add AI explanation if enriched
|
|
306
|
+
ai_explanation = issue.metadata.get("ai_explanation")
|
|
307
|
+
if ai_explanation:
|
|
308
|
+
steps.extend(self._parse_ai_explanation(ai_explanation))
|
|
309
|
+
|
|
310
|
+
# Generate generic steps based on domain if no specific steps
|
|
311
|
+
if not steps:
|
|
312
|
+
steps = self._generate_generic_steps(issue)
|
|
313
|
+
|
|
314
|
+
return steps
|
|
315
|
+
|
|
316
|
+
def _parse_ai_explanation(self, explanation: str) -> List[str]:
|
|
317
|
+
"""Parse AI explanation into steps.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
explanation: AI-generated explanation text.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
List of steps extracted from explanation.
|
|
324
|
+
"""
|
|
325
|
+
if not explanation:
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
# Split on numbered items or bullet points
|
|
329
|
+
lines = explanation.strip().split("\n")
|
|
330
|
+
steps = []
|
|
331
|
+
|
|
332
|
+
for line in lines:
|
|
333
|
+
line = line.strip()
|
|
334
|
+
# Skip empty lines
|
|
335
|
+
if not line:
|
|
336
|
+
continue
|
|
337
|
+
# Remove leading numbers/bullets
|
|
338
|
+
if line[0].isdigit() and "." in line[:3]:
|
|
339
|
+
line = line.split(".", 1)[1].strip()
|
|
340
|
+
elif line.startswith("-") or line.startswith("*"):
|
|
341
|
+
line = line[1:].strip()
|
|
342
|
+
|
|
343
|
+
if line and len(line) > 5:
|
|
344
|
+
steps.append(line)
|
|
345
|
+
|
|
346
|
+
return steps[:5] # Limit to 5 steps
|
|
347
|
+
|
|
348
|
+
def _generate_generic_steps(self, issue: UnifiedIssue) -> List[str]:
|
|
349
|
+
"""Generate generic fix steps based on domain.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
issue: The unified issue.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List of generic fix steps.
|
|
356
|
+
"""
|
|
357
|
+
file_ref = f"{issue.file_path}:{issue.line_start}" if issue.file_path and issue.line_start else str(issue.file_path or "the file")
|
|
358
|
+
domain = issue.domain
|
|
359
|
+
|
|
360
|
+
# Handle both ScanDomain and ToolDomain
|
|
361
|
+
if domain in (ScanDomain.SAST, ToolDomain.SECURITY):
|
|
362
|
+
return [
|
|
363
|
+
f"Review the security issue at {file_ref}",
|
|
364
|
+
"Apply the recommended fix from the scanner",
|
|
365
|
+
"Verify the fix doesn't break functionality",
|
|
366
|
+
"Consider adding tests to prevent regression",
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
if domain == ScanDomain.SCA:
|
|
370
|
+
return [
|
|
371
|
+
f"Update the vulnerable dependency mentioned in {issue.title}",
|
|
372
|
+
"Run tests to ensure compatibility with new version",
|
|
373
|
+
"Check for breaking changes in the changelog",
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
if domain == ScanDomain.IAC:
|
|
377
|
+
return [
|
|
378
|
+
f"Review the infrastructure issue at {file_ref}",
|
|
379
|
+
"Apply security best practices for the resource",
|
|
380
|
+
"Test the changes in a non-production environment",
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
if domain == ScanDomain.CONTAINER:
|
|
384
|
+
return [
|
|
385
|
+
f"Review the container vulnerability at {file_ref}",
|
|
386
|
+
"Update the base image or vulnerable packages",
|
|
387
|
+
"Rebuild and test the container",
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
if domain == ToolDomain.LINTING:
|
|
391
|
+
return [
|
|
392
|
+
f"Fix the linting issue at {file_ref}",
|
|
393
|
+
"Consider running 'lucidscan scan --linting --fix' for auto-fix",
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
if domain == ToolDomain.TYPE_CHECKING:
|
|
397
|
+
return [
|
|
398
|
+
f"Fix the type error at {file_ref}",
|
|
399
|
+
"Ensure type annotations are correct and complete",
|
|
400
|
+
"Check for None values that need handling",
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
if domain == ToolDomain.TESTING:
|
|
404
|
+
return [
|
|
405
|
+
f"Review the failing test at {file_ref}",
|
|
406
|
+
"Determine if the test or the code needs to be fixed",
|
|
407
|
+
"Run the test in isolation to verify the fix",
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
if domain == ToolDomain.COVERAGE:
|
|
411
|
+
return [
|
|
412
|
+
f"Add tests to cover the uncovered code at {file_ref}",
|
|
413
|
+
"Focus on critical paths and edge cases",
|
|
414
|
+
"Verify coverage threshold is met after adding tests",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
return [f"Address the issue at {file_ref}"]
|
|
418
|
+
|
|
419
|
+
def _generate_suggested_fix(self, issue: UnifiedIssue) -> Optional[str]:
|
|
420
|
+
"""Generate suggested fix code if available.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
issue: The unified issue.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Suggested fix code or None.
|
|
427
|
+
"""
|
|
428
|
+
# Use the issue's suggested_fix field directly
|
|
429
|
+
if issue.suggested_fix:
|
|
430
|
+
return issue.suggested_fix
|
|
431
|
+
|
|
432
|
+
# For linting issues, check metadata for auto_fix
|
|
433
|
+
if issue.domain == ToolDomain.LINTING:
|
|
434
|
+
auto_fix = issue.metadata.get("auto_fix")
|
|
435
|
+
if auto_fix:
|
|
436
|
+
return auto_fix
|
|
437
|
+
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
def _issue_to_brief(self, issue: UnifiedIssue) -> Dict[str, Any]:
|
|
441
|
+
"""Convert issue to brief format for domain grouping.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
issue: The unified issue.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Brief issue dictionary.
|
|
448
|
+
"""
|
|
449
|
+
file_path = str(issue.file_path) if issue.file_path else ""
|
|
450
|
+
location = file_path
|
|
451
|
+
if issue.line_start:
|
|
452
|
+
location = f"{file_path}:{issue.line_start}"
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"id": issue.id,
|
|
456
|
+
"location": location,
|
|
457
|
+
"severity": issue.severity.value if issue.severity else "unknown",
|
|
458
|
+
"title": issue.title or "",
|
|
459
|
+
"fixable": issue.fixable,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
def _generate_recommended_action(
|
|
463
|
+
self,
|
|
464
|
+
issues: List[UnifiedIssue],
|
|
465
|
+
severity_counts: Dict[str, int],
|
|
466
|
+
domain_status: Dict[str, Dict[str, Any]],
|
|
467
|
+
) -> str:
|
|
468
|
+
"""Generate recommended next action based on scan results.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
issues: List of issues found.
|
|
472
|
+
severity_counts: Count of issues by severity.
|
|
473
|
+
domain_status: Status of each checked domain.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Recommended action string.
|
|
477
|
+
"""
|
|
478
|
+
if not issues:
|
|
479
|
+
return "All checks passed. Ready to proceed."
|
|
480
|
+
|
|
481
|
+
# Count fixable issues
|
|
482
|
+
fixable_count = sum(1 for i in issues if i.fixable)
|
|
483
|
+
linting_fixable = sum(
|
|
484
|
+
1 for i in issues if i.domain == ToolDomain.LINTING and i.fixable
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Check for critical/high severity issues
|
|
488
|
+
critical_count = severity_counts.get("critical", 0)
|
|
489
|
+
high_count = severity_counts.get("high", 0)
|
|
490
|
+
|
|
491
|
+
if critical_count > 0:
|
|
492
|
+
return f"Fix {critical_count} critical issue(s) immediately before proceeding."
|
|
493
|
+
|
|
494
|
+
if high_count > 0:
|
|
495
|
+
return f"Address {high_count} high-severity issue(s) before committing."
|
|
496
|
+
|
|
497
|
+
if linting_fixable > 0:
|
|
498
|
+
return f"Run `scan(fix=true)` to auto-fix {linting_fixable} linting issue(s), then address remaining issues manually."
|
|
499
|
+
|
|
500
|
+
if fixable_count > 0:
|
|
501
|
+
return f"Run `scan(fix=true)` to auto-fix {fixable_count} issue(s)."
|
|
502
|
+
|
|
503
|
+
# Type errors or other issues
|
|
504
|
+
type_issues = sum(
|
|
505
|
+
1 for i in issues if i.domain == ToolDomain.TYPE_CHECKING
|
|
506
|
+
)
|
|
507
|
+
if type_issues > 0:
|
|
508
|
+
return f"Fix {type_issues} type error(s) by updating type annotations or handling None values."
|
|
509
|
+
|
|
510
|
+
return f"Review and fix {len(issues)} issue(s), then re-scan to verify."
|