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,106 @@
1
+ """Base class for type checker plugins.
2
+
3
+ All type checker plugins inherit from TypeCheckerPlugin and implement the check() method.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+
13
+ from lucidscan.core.models import ScanContext, UnifiedIssue, ToolDomain
14
+
15
+
16
+ @dataclass
17
+ class TypeCheckResult:
18
+ """Result statistics from type checking."""
19
+
20
+ errors: int = 0
21
+ warnings: int = 0
22
+ notes: int = 0
23
+ files_checked: int = 0
24
+ details: List[str] = field(default_factory=list)
25
+
26
+
27
+ class TypeCheckerPlugin(ABC):
28
+ """Abstract base class for type checker plugins.
29
+
30
+ Type checker plugins provide static type checking functionality.
31
+ Each plugin wraps a specific type checking tool (mypy, pyright, tsc, etc.).
32
+ """
33
+
34
+ def __init__(self, project_root: Optional[Path] = None, **kwargs) -> None:
35
+ """Initialize the type checker plugin.
36
+
37
+ Args:
38
+ project_root: Optional project root for tool installation.
39
+ **kwargs: Additional arguments for subclasses.
40
+ """
41
+ self._project_root = project_root
42
+
43
+ @property
44
+ @abstractmethod
45
+ def name(self) -> str:
46
+ """Unique plugin identifier (e.g., 'mypy', 'pyright').
47
+
48
+ Returns:
49
+ Plugin name string.
50
+ """
51
+
52
+ @property
53
+ @abstractmethod
54
+ def languages(self) -> List[str]:
55
+ """Languages this type checker supports.
56
+
57
+ Returns:
58
+ List of language names (e.g., ['python'], ['typescript']).
59
+ """
60
+
61
+ @property
62
+ def domain(self) -> ToolDomain:
63
+ """Tool domain (always TYPE_CHECKING for type checkers).
64
+
65
+ Returns:
66
+ ToolDomain.TYPE_CHECKING
67
+ """
68
+ return ToolDomain.TYPE_CHECKING
69
+
70
+ @property
71
+ def supports_strict_mode(self) -> bool:
72
+ """Whether this type checker supports strict mode.
73
+
74
+ Returns:
75
+ True if the type checker has a strict mode.
76
+ """
77
+ return False
78
+
79
+ @abstractmethod
80
+ def get_version(self) -> str:
81
+ """Get the version of the underlying type checking tool.
82
+
83
+ Returns:
84
+ Version string.
85
+ """
86
+
87
+ @abstractmethod
88
+ def ensure_binary(self) -> Path:
89
+ """Ensure the type checking tool is installed.
90
+
91
+ Downloads or installs the tool if not present.
92
+
93
+ Returns:
94
+ Path to the tool binary.
95
+ """
96
+
97
+ @abstractmethod
98
+ def check(self, context: ScanContext) -> List[UnifiedIssue]:
99
+ """Run type checking on the specified paths.
100
+
101
+ Args:
102
+ context: Scan context with paths and configuration.
103
+
104
+ Returns:
105
+ List of UnifiedIssue objects for each type error.
106
+ """
@@ -0,0 +1,355 @@
1
+ """mypy type checker plugin.
2
+
3
+ mypy is a static type checker for Python.
4
+ https://mypy-lang.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.type_checkers.base import TypeCheckerPlugin
25
+
26
+ LOGGER = get_logger(__name__)
27
+
28
+
29
+ def _glob_to_regex(pattern: str) -> str:
30
+ """Convert a gitignore-style glob pattern to a regex for mypy.
31
+
32
+ mypy's --exclude flag expects Python regex patterns, not glob patterns.
33
+ This function converts common glob patterns to equivalent regex.
34
+
35
+ Args:
36
+ pattern: Gitignore-style glob pattern (e.g., '**/.venv/**', '*.pyc').
37
+
38
+ Returns:
39
+ Regex pattern suitable for mypy --exclude.
40
+ """
41
+ import re
42
+
43
+ # Handle common directory patterns like **/.venv/** or .venv/
44
+ # Extract the core directory/file name and create a simple regex
45
+ if pattern.startswith("**/") and pattern.endswith("/**"):
46
+ # Pattern like **/.venv/** - match directory anywhere in path
47
+ core = pattern[3:-3] # Remove **/ and /**
48
+ # Escape regex special chars and create pattern
49
+ escaped = re.escape(core)
50
+ return f"(^|/){escaped}(/|$)"
51
+
52
+ if pattern.startswith("**/"):
53
+ # Pattern like **/foo - match at end of any path
54
+ core = pattern[3:]
55
+ escaped = re.escape(core)
56
+ return f"(^|/){escaped}$"
57
+
58
+ if pattern.endswith("/**"):
59
+ # Pattern like foo/** - match directory at start
60
+ core = pattern[:-3]
61
+ escaped = re.escape(core)
62
+ return f"^{escaped}(/|$)"
63
+
64
+ if pattern.endswith("/"):
65
+ # Directory pattern like .venv/
66
+ core = pattern[:-1]
67
+ escaped = re.escape(core)
68
+ return f"(^|/){escaped}(/|$)"
69
+
70
+ # Handle wildcard patterns
71
+ # Escape all regex special chars first
72
+ escaped = re.escape(pattern)
73
+ # Convert glob wildcards to regex
74
+ # ** matches any path components
75
+ escaped = escaped.replace(r"\*\*", ".*")
76
+ # * matches anything except /
77
+ escaped = escaped.replace(r"\*", "[^/]*")
78
+ # ? matches single char except /
79
+ escaped = escaped.replace(r"\?", "[^/]")
80
+
81
+ return escaped
82
+
83
+
84
+ # mypy severity mapping
85
+ # mypy outputs: error, warning, note
86
+ SEVERITY_MAP = {
87
+ "error": Severity.HIGH,
88
+ "warning": Severity.MEDIUM,
89
+ "note": Severity.LOW,
90
+ }
91
+
92
+
93
+ class MypyChecker(TypeCheckerPlugin):
94
+ """mypy type checker plugin for Python code analysis."""
95
+
96
+ def __init__(self, project_root: Optional[Path] = None):
97
+ """Initialize MypyChecker.
98
+
99
+ Args:
100
+ project_root: Optional project root for finding mypy installation.
101
+ """
102
+ self._project_root = project_root
103
+
104
+ @property
105
+ def name(self) -> str:
106
+ """Plugin identifier."""
107
+ return "mypy"
108
+
109
+ @property
110
+ def languages(self) -> List[str]:
111
+ """Supported languages."""
112
+ return ["python"]
113
+
114
+ @property
115
+ def supports_strict_mode(self) -> bool:
116
+ """mypy supports strict mode."""
117
+ return True
118
+
119
+ def get_version(self) -> str:
120
+ """Get mypy version.
121
+
122
+ Returns:
123
+ Version string or 'unknown' if unable to determine.
124
+ """
125
+ try:
126
+ binary = self.ensure_binary()
127
+ result = subprocess.run(
128
+ [str(binary), "--version"],
129
+ capture_output=True,
130
+ text=True,
131
+ encoding="utf-8",
132
+ errors="replace",
133
+ timeout=30,
134
+ )
135
+ # Output is like "mypy 1.8.0 (compiled: yes)"
136
+ if result.returncode == 0:
137
+ parts = result.stdout.strip().split()
138
+ if len(parts) >= 2:
139
+ return parts[1]
140
+ except Exception:
141
+ pass
142
+ return "unknown"
143
+
144
+ def ensure_binary(self) -> Path:
145
+ """Ensure mypy is available.
146
+
147
+ Checks for mypy in:
148
+ 1. Project's .venv/bin/mypy
149
+ 2. System PATH
150
+
151
+ Returns:
152
+ Path to mypy binary.
153
+
154
+ Raises:
155
+ FileNotFoundError: If mypy is not installed.
156
+ """
157
+ # Check project venv first
158
+ if self._project_root:
159
+ venv_mypy = self._project_root / ".venv" / "bin" / "mypy"
160
+ if venv_mypy.exists():
161
+ return venv_mypy
162
+
163
+ # Check system PATH
164
+ mypy_path = shutil.which("mypy")
165
+ if mypy_path:
166
+ return Path(mypy_path)
167
+
168
+ raise FileNotFoundError(
169
+ "mypy is not installed. Install it with: pip install mypy"
170
+ )
171
+
172
+ def check(self, context: ScanContext) -> List[UnifiedIssue]:
173
+ """Run mypy type checking.
174
+
175
+ Args:
176
+ context: Scan context with paths and configuration.
177
+
178
+ Returns:
179
+ List of type checking issues.
180
+ """
181
+ try:
182
+ binary = self.ensure_binary()
183
+ except FileNotFoundError as e:
184
+ LOGGER.warning(str(e))
185
+ return []
186
+
187
+ # Build command
188
+ cmd = [
189
+ str(binary),
190
+ "--output", "json",
191
+ "--no-error-summary",
192
+ ]
193
+
194
+ # Check for strict mode in config
195
+ if context.config and hasattr(context.config, "type_checking"):
196
+ type_config = context.config.type_checking
197
+ if hasattr(type_config, "strict") and type_config.strict:
198
+ cmd.append("--strict")
199
+
200
+ # Check for mypy config file
201
+ mypy_ini = context.project_root / "mypy.ini"
202
+ setup_cfg = context.project_root / "setup.cfg"
203
+ pyproject = context.project_root / "pyproject.toml"
204
+
205
+ if mypy_ini.exists():
206
+ cmd.extend(["--config-file", str(mypy_ini)])
207
+ elif setup_cfg.exists():
208
+ cmd.extend(["--config-file", str(setup_cfg)])
209
+ elif pyproject.exists():
210
+ cmd.extend(["--config-file", str(pyproject)])
211
+
212
+ # Add paths to check
213
+ paths = [str(p) for p in context.paths] if context.paths else ["."]
214
+ cmd.extend(paths)
215
+
216
+ # Add exclude patterns (convert glob patterns to regex for mypy)
217
+ exclude_patterns = context.get_exclude_patterns()
218
+ for pattern in exclude_patterns:
219
+ regex_pattern = _glob_to_regex(pattern)
220
+ cmd.extend(["--exclude", regex_pattern])
221
+
222
+ LOGGER.debug(f"Running: {' '.join(cmd)}")
223
+
224
+ try:
225
+ result = run_with_streaming(
226
+ cmd=cmd,
227
+ cwd=context.project_root,
228
+ tool_name="mypy",
229
+ stream_handler=context.stream_handler,
230
+ timeout=180,
231
+ )
232
+ except subprocess.TimeoutExpired:
233
+ LOGGER.warning("mypy timed out after 180 seconds")
234
+ return []
235
+ except Exception as e:
236
+ LOGGER.error(f"Failed to run mypy: {e}")
237
+ return []
238
+
239
+ # Parse output
240
+ issues = self._parse_output(result.stdout, context.project_root)
241
+
242
+ LOGGER.info(f"mypy found {len(issues)} issues")
243
+ return issues
244
+
245
+ def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
246
+ """Parse mypy JSON output.
247
+
248
+ Args:
249
+ output: JSON output from mypy (one JSON object per line).
250
+ project_root: Project root directory.
251
+
252
+ Returns:
253
+ List of UnifiedIssue objects.
254
+ """
255
+ if not output.strip():
256
+ return []
257
+
258
+ issues = []
259
+ for line in output.strip().split("\n"):
260
+ if not line.strip():
261
+ continue
262
+
263
+ try:
264
+ error = json.loads(line)
265
+ issue = self._error_to_issue(error, project_root)
266
+ if issue:
267
+ issues.append(issue)
268
+ except json.JSONDecodeError:
269
+ # Skip non-JSON lines (e.g., summary messages)
270
+ LOGGER.debug(f"Skipping non-JSON line: {line}")
271
+ continue
272
+
273
+ return issues
274
+
275
+ def _error_to_issue(
276
+ self,
277
+ error: Dict[str, Any],
278
+ project_root: Path,
279
+ ) -> Optional[UnifiedIssue]:
280
+ """Convert mypy error to UnifiedIssue.
281
+
282
+ Args:
283
+ error: mypy error dict from JSON output.
284
+ project_root: Project root directory.
285
+
286
+ Returns:
287
+ UnifiedIssue or None.
288
+ """
289
+ try:
290
+ severity_str = error.get("severity", "error")
291
+ message = error.get("message", "")
292
+ file = error.get("file", "")
293
+ line = error.get("line")
294
+ column = error.get("column")
295
+ code = error.get("code", "")
296
+
297
+ # Get severity
298
+ severity = SEVERITY_MAP.get(severity_str, Severity.MEDIUM)
299
+
300
+ # Build file path
301
+ file_path = Path(file)
302
+ if not file_path.is_absolute():
303
+ file_path = project_root / file_path
304
+
305
+ # Generate deterministic ID
306
+ issue_id = self._generate_issue_id(code, file, line, column, message)
307
+
308
+ # Build title
309
+ title = f"[{code}] {message}" if code else message
310
+
311
+ return UnifiedIssue(
312
+ id=issue_id,
313
+ domain=ToolDomain.TYPE_CHECKING,
314
+ source_tool="mypy",
315
+ severity=severity,
316
+ rule_id=code or "unknown",
317
+ title=title,
318
+ description=message,
319
+ documentation_url=f"https://mypy.readthedocs.io/en/stable/error_code_list.html#{code}" if code else None,
320
+ file_path=file_path,
321
+ line_start=line,
322
+ line_end=line,
323
+ column_start=column,
324
+ fixable=False,
325
+ metadata={
326
+ "severity_raw": severity_str,
327
+ },
328
+ )
329
+ except Exception as e:
330
+ LOGGER.warning(f"Failed to parse mypy error: {e}")
331
+ return None
332
+
333
+ def _generate_issue_id(
334
+ self,
335
+ code: str,
336
+ file: str,
337
+ line: Optional[int],
338
+ column: Optional[int],
339
+ message: str,
340
+ ) -> str:
341
+ """Generate deterministic issue ID.
342
+
343
+ Args:
344
+ code: Error code.
345
+ file: File path.
346
+ line: Line number.
347
+ column: Column number.
348
+ message: Error message.
349
+
350
+ Returns:
351
+ Unique issue ID.
352
+ """
353
+ content = f"{code}:{file}:{line or 0}:{column or 0}:{message}"
354
+ hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
355
+ return f"mypy-{code}-{hash_val}" if code else f"mypy-{hash_val}"