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,368 @@
1
+ """ESLint linter plugin.
2
+
3
+ ESLint is a pluggable linting utility for JavaScript and TypeScript.
4
+ https://eslint.org/
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import shutil
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from lucidscan.core.logging import get_logger
17
+ from lucidscan.core.models import (
18
+ ScanContext,
19
+ Severity,
20
+ ToolDomain,
21
+ UnifiedIssue,
22
+ )
23
+ from lucidscan.core.subprocess_runner import run_with_streaming
24
+ from lucidscan.plugins.linters.base import FixResult, LinterPlugin
25
+
26
+ LOGGER = get_logger(__name__)
27
+
28
+ # ESLint severity mapping
29
+ # ESLint uses: 1=warning, 2=error
30
+ SEVERITY_MAP = {
31
+ 2: Severity.HIGH, # error
32
+ 1: Severity.MEDIUM, # warning
33
+ }
34
+
35
+
36
+ class ESLintLinter(LinterPlugin):
37
+ """ESLint linter plugin for JavaScript/TypeScript code analysis."""
38
+
39
+ def __init__(self, project_root: Optional[Path] = None):
40
+ """Initialize ESLintLinter.
41
+
42
+ Args:
43
+ project_root: Optional project root for finding ESLint installation.
44
+ """
45
+ self._project_root = project_root
46
+
47
+ @property
48
+ def name(self) -> str:
49
+ """Plugin identifier."""
50
+ return "eslint"
51
+
52
+ @property
53
+ def languages(self) -> List[str]:
54
+ """Supported languages."""
55
+ return ["javascript", "typescript"]
56
+
57
+ @property
58
+ def supports_fix(self) -> bool:
59
+ """ESLint supports auto-fix."""
60
+ return True
61
+
62
+ def get_version(self) -> str:
63
+ """Get ESLint version.
64
+
65
+ Returns:
66
+ Version string or 'unknown' if unable to determine.
67
+ """
68
+ try:
69
+ binary = self.ensure_binary()
70
+ result = subprocess.run(
71
+ [str(binary), "--version"],
72
+ capture_output=True,
73
+ text=True,
74
+ encoding="utf-8",
75
+ errors="replace",
76
+ timeout=30,
77
+ )
78
+ # Output is like "v8.56.0"
79
+ if result.returncode == 0:
80
+ return result.stdout.strip().lstrip("v")
81
+ except Exception:
82
+ pass
83
+ return "unknown"
84
+
85
+ def ensure_binary(self) -> Path:
86
+ """Ensure ESLint is available.
87
+
88
+ Checks for ESLint in:
89
+ 1. Project's node_modules/.bin/eslint
90
+ 2. System PATH (globally installed)
91
+
92
+ Returns:
93
+ Path to ESLint binary.
94
+
95
+ Raises:
96
+ FileNotFoundError: If ESLint is not installed.
97
+ """
98
+ # Check project node_modules first
99
+ if self._project_root:
100
+ node_eslint = self._project_root / "node_modules" / ".bin" / "eslint"
101
+ if node_eslint.exists():
102
+ return node_eslint
103
+
104
+ # Check system PATH
105
+ eslint_path = shutil.which("eslint")
106
+ if eslint_path:
107
+ return Path(eslint_path)
108
+
109
+ raise FileNotFoundError(
110
+ "ESLint is not installed. Install it with:\n"
111
+ " npm install eslint --save-dev\n"
112
+ " OR\n"
113
+ " npm install -g eslint"
114
+ )
115
+
116
+ def lint(self, context: ScanContext) -> List[UnifiedIssue]:
117
+ """Run ESLint linting.
118
+
119
+ Args:
120
+ context: Scan context with paths and configuration.
121
+
122
+ Returns:
123
+ List of linting issues.
124
+ """
125
+ try:
126
+ binary = self.ensure_binary()
127
+ except FileNotFoundError as e:
128
+ LOGGER.warning(str(e))
129
+ return []
130
+
131
+ # Build command
132
+ cmd = [
133
+ str(binary),
134
+ "--format", "json",
135
+ ]
136
+
137
+ # Add paths to check - default to src if exists, otherwise current dir
138
+ if context.paths:
139
+ paths = [str(p) for p in context.paths]
140
+ else:
141
+ src_dir = context.project_root / "src"
142
+ if src_dir.exists():
143
+ paths = [str(src_dir)]
144
+ else:
145
+ paths = ["."]
146
+
147
+ cmd.extend(paths)
148
+
149
+ # Add ignore patterns
150
+ exclude_patterns = context.get_exclude_patterns()
151
+ for pattern in exclude_patterns:
152
+ cmd.extend(["--ignore-pattern", pattern])
153
+
154
+ LOGGER.debug(f"Running: {' '.join(cmd)}")
155
+
156
+ try:
157
+ result = run_with_streaming(
158
+ cmd=cmd,
159
+ cwd=context.project_root,
160
+ tool_name="eslint",
161
+ stream_handler=context.stream_handler,
162
+ timeout=120,
163
+ )
164
+ except subprocess.TimeoutExpired:
165
+ LOGGER.warning("ESLint lint timed out after 120 seconds")
166
+ return []
167
+ except Exception as e:
168
+ LOGGER.error(f"Failed to run ESLint: {e}")
169
+ return []
170
+
171
+ # Parse output
172
+ issues = self._parse_output(result.stdout, context.project_root)
173
+
174
+ LOGGER.info(f"ESLint found {len(issues)} issues")
175
+ return issues
176
+
177
+ def fix(self, context: ScanContext) -> FixResult:
178
+ """Apply ESLint auto-fixes.
179
+
180
+ Args:
181
+ context: Scan context with paths and configuration.
182
+
183
+ Returns:
184
+ FixResult with statistics.
185
+ """
186
+ try:
187
+ binary = self.ensure_binary()
188
+ except FileNotFoundError as e:
189
+ LOGGER.warning(str(e))
190
+ return FixResult()
191
+
192
+ # Run without fix to count issues first
193
+ pre_issues = self.lint(context)
194
+
195
+ # Build fix command
196
+ cmd = [
197
+ str(binary),
198
+ "--fix",
199
+ "--format", "json",
200
+ ]
201
+
202
+ if context.paths:
203
+ paths = [str(p) for p in context.paths]
204
+ else:
205
+ src_dir = context.project_root / "src"
206
+ if src_dir.exists():
207
+ paths = [str(src_dir)]
208
+ else:
209
+ paths = ["."]
210
+
211
+ cmd.extend(paths)
212
+
213
+ exclude_patterns = context.get_exclude_patterns()
214
+ for pattern in exclude_patterns:
215
+ cmd.extend(["--ignore-pattern", pattern])
216
+
217
+ LOGGER.debug(f"Running: {' '.join(cmd)}")
218
+
219
+ try:
220
+ result = run_with_streaming(
221
+ cmd=cmd,
222
+ cwd=context.project_root,
223
+ tool_name="eslint-fix",
224
+ stream_handler=context.stream_handler,
225
+ timeout=120,
226
+ )
227
+ except subprocess.TimeoutExpired:
228
+ LOGGER.warning("ESLint fix timed out after 120 seconds")
229
+ return FixResult()
230
+ except Exception as e:
231
+ LOGGER.error(f"Failed to run ESLint fix: {e}")
232
+ return FixResult()
233
+
234
+ # Parse remaining issues
235
+ post_issues = self._parse_output(result.stdout, context.project_root)
236
+
237
+ # Calculate stats
238
+ files_modified = len(set(
239
+ str(issue.file_path)
240
+ for issue in pre_issues
241
+ if issue not in post_issues
242
+ ))
243
+
244
+ return FixResult(
245
+ files_modified=files_modified,
246
+ issues_fixed=len(pre_issues) - len(post_issues),
247
+ issues_remaining=len(post_issues),
248
+ )
249
+
250
+ def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
251
+ """Parse ESLint JSON output.
252
+
253
+ Args:
254
+ output: JSON output from ESLint.
255
+ project_root: Project root directory.
256
+
257
+ Returns:
258
+ List of UnifiedIssue objects.
259
+ """
260
+ if not output.strip():
261
+ return []
262
+
263
+ try:
264
+ results = json.loads(output)
265
+ except json.JSONDecodeError:
266
+ LOGGER.warning("Failed to parse ESLint output as JSON")
267
+ return []
268
+
269
+ issues = []
270
+ for file_result in results:
271
+ file_path = file_result.get("filePath", "")
272
+ messages = file_result.get("messages", [])
273
+
274
+ for message in messages:
275
+ issue = self._message_to_issue(message, file_path, project_root)
276
+ if issue:
277
+ issues.append(issue)
278
+
279
+ return issues
280
+
281
+ def _message_to_issue(
282
+ self,
283
+ message: Dict[str, Any],
284
+ file_path: str,
285
+ project_root: Path,
286
+ ) -> Optional[UnifiedIssue]:
287
+ """Convert ESLint message to UnifiedIssue.
288
+
289
+ Args:
290
+ message: ESLint message dict.
291
+ file_path: File path from ESLint.
292
+ project_root: Project root directory.
293
+
294
+ Returns:
295
+ UnifiedIssue or None.
296
+ """
297
+ try:
298
+ severity_int = message.get("severity", 2)
299
+ rule_id = message.get("ruleId", "")
300
+ msg = message.get("message", "")
301
+ line = message.get("line")
302
+ column = message.get("column")
303
+ end_line = message.get("endLine")
304
+
305
+ # Get severity
306
+ severity = SEVERITY_MAP.get(severity_int, Severity.MEDIUM)
307
+
308
+ # Build file path
309
+ path = Path(file_path)
310
+ if not path.is_absolute():
311
+ path = project_root / path
312
+
313
+ # Generate deterministic ID
314
+ issue_id = self._generate_issue_id(rule_id, file_path, line, column, msg)
315
+
316
+ # Build title
317
+ title = f"[{rule_id}] {msg}" if rule_id else msg
318
+
319
+ # Check if fixable
320
+ fixable = message.get("fix") is not None
321
+
322
+ # Extract end column
323
+ end_column = message.get("endColumn")
324
+
325
+ return UnifiedIssue(
326
+ id=issue_id,
327
+ domain=ToolDomain.LINTING,
328
+ source_tool="eslint",
329
+ severity=severity,
330
+ rule_id=rule_id or "unknown",
331
+ title=title,
332
+ description=msg,
333
+ documentation_url=f"https://eslint.org/docs/rules/{rule_id}" if rule_id else None,
334
+ file_path=path,
335
+ line_start=line,
336
+ line_end=end_line or line,
337
+ column_start=column,
338
+ column_end=end_column,
339
+ fixable=fixable,
340
+ metadata={},
341
+ )
342
+ except Exception as e:
343
+ LOGGER.warning(f"Failed to parse ESLint message: {e}")
344
+ return None
345
+
346
+ def _generate_issue_id(
347
+ self,
348
+ rule: str,
349
+ file: str,
350
+ line: Optional[int],
351
+ column: Optional[int],
352
+ message: str,
353
+ ) -> str:
354
+ """Generate deterministic issue ID.
355
+
356
+ Args:
357
+ rule: Rule ID.
358
+ file: File path.
359
+ line: Line number.
360
+ column: Column number.
361
+ message: Error message.
362
+
363
+ Returns:
364
+ Unique issue ID.
365
+ """
366
+ content = f"{rule}:{file}:{line or 0}:{column or 0}:{message}"
367
+ hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
368
+ return f"eslint-{rule}-{hash_val}" if rule else f"eslint-{hash_val}"