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,393 @@
|
|
|
1
|
+
"""Checkstyle linter plugin.
|
|
2
|
+
|
|
3
|
+
Checkstyle is a development tool to help programmers write Java code
|
|
4
|
+
that adheres to a coding standard.
|
|
5
|
+
https://checkstyle.org/
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import xml.etree.ElementTree as ET
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
from lucidscan.bootstrap.paths import LucidscanPaths
|
|
18
|
+
from lucidscan.bootstrap.versions import get_tool_version
|
|
19
|
+
from lucidscan.core.logging import get_logger
|
|
20
|
+
from lucidscan.core.models import (
|
|
21
|
+
ScanContext,
|
|
22
|
+
Severity,
|
|
23
|
+
ToolDomain,
|
|
24
|
+
UnifiedIssue,
|
|
25
|
+
)
|
|
26
|
+
from lucidscan.core.subprocess_runner import run_with_streaming
|
|
27
|
+
from lucidscan.plugins.linters.base import LinterPlugin
|
|
28
|
+
|
|
29
|
+
LOGGER = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
# Default version from pyproject.toml [tool.lucidscan.tools]
|
|
32
|
+
DEFAULT_VERSION = get_tool_version("checkstyle")
|
|
33
|
+
|
|
34
|
+
# Checkstyle severity mapping
|
|
35
|
+
SEVERITY_MAP = {
|
|
36
|
+
"error": Severity.HIGH,
|
|
37
|
+
"warning": Severity.MEDIUM,
|
|
38
|
+
"info": Severity.LOW,
|
|
39
|
+
"ignore": Severity.INFO,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CheckstyleLinter(LinterPlugin):
|
|
44
|
+
"""Checkstyle linter plugin for Java code analysis."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
version: str = DEFAULT_VERSION,
|
|
49
|
+
project_root: Optional[Path] = None,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize CheckstyleLinter.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
version: Checkstyle version to use.
|
|
55
|
+
project_root: Optional project root for tool installation.
|
|
56
|
+
"""
|
|
57
|
+
self._version = version
|
|
58
|
+
if project_root:
|
|
59
|
+
self._paths = LucidscanPaths.for_project(project_root)
|
|
60
|
+
self._project_root = project_root
|
|
61
|
+
else:
|
|
62
|
+
self._paths = LucidscanPaths.default()
|
|
63
|
+
self._project_root = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def name(self) -> str:
|
|
67
|
+
"""Plugin identifier."""
|
|
68
|
+
return "checkstyle"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def languages(self) -> List[str]:
|
|
72
|
+
"""Supported languages."""
|
|
73
|
+
return ["java"]
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def supports_fix(self) -> bool:
|
|
77
|
+
"""Checkstyle does not support auto-fix."""
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def get_version(self) -> str:
|
|
81
|
+
"""Get Checkstyle version."""
|
|
82
|
+
return self._version
|
|
83
|
+
|
|
84
|
+
def _check_java_available(self) -> Optional[Path]:
|
|
85
|
+
"""Check if Java is available.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Path to java binary or None if not found.
|
|
89
|
+
"""
|
|
90
|
+
java_path = shutil.which("java")
|
|
91
|
+
return Path(java_path) if java_path else None
|
|
92
|
+
|
|
93
|
+
def ensure_binary(self) -> Path:
|
|
94
|
+
"""Ensure Checkstyle JAR is available.
|
|
95
|
+
|
|
96
|
+
Downloads from GitHub releases if not present.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Path to Checkstyle JAR file.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
FileNotFoundError: If Java is not installed.
|
|
103
|
+
"""
|
|
104
|
+
# First check if Java is available
|
|
105
|
+
if not self._check_java_available():
|
|
106
|
+
raise FileNotFoundError(
|
|
107
|
+
"Java is not installed. Checkstyle requires Java.\n"
|
|
108
|
+
"Install Java JDK/JRE to use Checkstyle."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
jar_dir = self._paths.plugin_bin_dir(self.name, self._version)
|
|
112
|
+
jar_name = f"checkstyle-{self._version}-all.jar"
|
|
113
|
+
jar_path = jar_dir / jar_name
|
|
114
|
+
|
|
115
|
+
if jar_path.exists():
|
|
116
|
+
return jar_path
|
|
117
|
+
|
|
118
|
+
LOGGER.info(f"Downloading Checkstyle {self._version}...")
|
|
119
|
+
jar_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
self._download_jar(jar_path)
|
|
122
|
+
|
|
123
|
+
LOGGER.info(f"Checkstyle {self._version} installed to {jar_dir}")
|
|
124
|
+
return jar_path
|
|
125
|
+
|
|
126
|
+
def _download_jar(self, target_path: Path) -> None:
|
|
127
|
+
"""Download Checkstyle JAR file.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
target_path: Path to save the JAR file.
|
|
131
|
+
"""
|
|
132
|
+
import urllib.request
|
|
133
|
+
|
|
134
|
+
url = (
|
|
135
|
+
f"https://github.com/checkstyle/checkstyle/releases/download/"
|
|
136
|
+
f"checkstyle-{self._version}/checkstyle-{self._version}-all.jar"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
LOGGER.debug(f"Downloading from {url}")
|
|
140
|
+
|
|
141
|
+
# Validate URL scheme and domain for security
|
|
142
|
+
if not url.startswith("https://github.com/"):
|
|
143
|
+
raise ValueError(f"Invalid download URL: {url}")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
urllib.request.urlretrieve(url, target_path) # nosec B310 nosemgrep
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise RuntimeError(f"Failed to download Checkstyle: {e}") from e
|
|
149
|
+
|
|
150
|
+
def lint(self, context: ScanContext) -> List[UnifiedIssue]:
|
|
151
|
+
"""Run Checkstyle linting.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
context: Scan context with paths and configuration.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of linting issues.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
jar_path = self.ensure_binary()
|
|
161
|
+
except FileNotFoundError as e:
|
|
162
|
+
LOGGER.warning(str(e))
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
java_path = self._check_java_available()
|
|
166
|
+
if not java_path:
|
|
167
|
+
LOGGER.warning("Java not found, skipping Checkstyle")
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
# Determine config file
|
|
171
|
+
config_file = self._find_config_file(context.project_root)
|
|
172
|
+
|
|
173
|
+
# Build command
|
|
174
|
+
cmd = [
|
|
175
|
+
str(java_path),
|
|
176
|
+
"-jar", str(jar_path),
|
|
177
|
+
"-c", config_file,
|
|
178
|
+
"-f", "xml",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
# Find Java source files
|
|
182
|
+
java_files = self._find_java_files(context)
|
|
183
|
+
if not java_files:
|
|
184
|
+
LOGGER.info("No Java files found to check")
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
cmd.extend(java_files)
|
|
188
|
+
|
|
189
|
+
LOGGER.debug(f"Running: {' '.join(cmd[:10])}...") # Truncate for readability
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
result = run_with_streaming(
|
|
193
|
+
cmd=cmd,
|
|
194
|
+
cwd=context.project_root,
|
|
195
|
+
tool_name="checkstyle",
|
|
196
|
+
stream_handler=context.stream_handler,
|
|
197
|
+
timeout=120,
|
|
198
|
+
)
|
|
199
|
+
except subprocess.TimeoutExpired:
|
|
200
|
+
LOGGER.warning("Checkstyle timed out after 120 seconds")
|
|
201
|
+
return []
|
|
202
|
+
except Exception as e:
|
|
203
|
+
LOGGER.error(f"Failed to run Checkstyle: {e}")
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
# Parse XML output
|
|
207
|
+
issues = self._parse_output(result.stdout, context.project_root)
|
|
208
|
+
|
|
209
|
+
LOGGER.info(f"Checkstyle found {len(issues)} issues")
|
|
210
|
+
return issues
|
|
211
|
+
|
|
212
|
+
def _find_config_file(self, project_root: Path) -> str:
|
|
213
|
+
"""Find Checkstyle configuration file.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
project_root: Project root directory.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Path to config file or built-in config name.
|
|
220
|
+
"""
|
|
221
|
+
# Check for custom config files
|
|
222
|
+
custom_configs = [
|
|
223
|
+
"checkstyle.xml",
|
|
224
|
+
".checkstyle.xml",
|
|
225
|
+
"config/checkstyle/checkstyle.xml",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
for config in custom_configs:
|
|
229
|
+
config_path = project_root / config
|
|
230
|
+
if config_path.exists():
|
|
231
|
+
return str(config_path)
|
|
232
|
+
|
|
233
|
+
# Use built-in Google checks as default
|
|
234
|
+
return "/google_checks.xml"
|
|
235
|
+
|
|
236
|
+
def _find_java_files(self, context: ScanContext) -> List[str]:
|
|
237
|
+
"""Find Java source files to check.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
context: Scan context.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of Java file paths.
|
|
244
|
+
"""
|
|
245
|
+
java_files = []
|
|
246
|
+
|
|
247
|
+
# Search in specified paths or common Java directories
|
|
248
|
+
search_dirs = []
|
|
249
|
+
if context.paths:
|
|
250
|
+
search_dirs = list(context.paths)
|
|
251
|
+
else:
|
|
252
|
+
# Common Java source directories
|
|
253
|
+
for src_dir in ["src", "src/main/java", "src/test/java"]:
|
|
254
|
+
src_path = context.project_root / src_dir
|
|
255
|
+
if src_path.exists():
|
|
256
|
+
search_dirs.append(src_path)
|
|
257
|
+
|
|
258
|
+
if not search_dirs:
|
|
259
|
+
search_dirs = [context.project_root]
|
|
260
|
+
|
|
261
|
+
for search_dir in search_dirs:
|
|
262
|
+
if not search_dir.exists():
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
for java_file in search_dir.rglob("*.java"):
|
|
266
|
+
# Check if file should be excluded using proper gitignore matching
|
|
267
|
+
if context.ignore_patterns is None or not context.ignore_patterns.matches(
|
|
268
|
+
java_file, context.project_root
|
|
269
|
+
):
|
|
270
|
+
java_files.append(str(java_file))
|
|
271
|
+
|
|
272
|
+
return java_files
|
|
273
|
+
|
|
274
|
+
def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
|
|
275
|
+
"""Parse Checkstyle XML output.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
output: XML output from Checkstyle.
|
|
279
|
+
project_root: Project root directory.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List of UnifiedIssue objects.
|
|
283
|
+
"""
|
|
284
|
+
if not output.strip():
|
|
285
|
+
return []
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
root = ET.fromstring(output)
|
|
289
|
+
except ET.ParseError as e:
|
|
290
|
+
LOGGER.warning(f"Failed to parse Checkstyle XML output: {e}")
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
issues = []
|
|
294
|
+
|
|
295
|
+
for file_elem in root.findall(".//file"):
|
|
296
|
+
file_path = file_elem.get("name", "")
|
|
297
|
+
|
|
298
|
+
for error_elem in file_elem.findall("error"):
|
|
299
|
+
issue = self._error_to_issue(error_elem, file_path, project_root)
|
|
300
|
+
if issue:
|
|
301
|
+
issues.append(issue)
|
|
302
|
+
|
|
303
|
+
return issues
|
|
304
|
+
|
|
305
|
+
def _error_to_issue(
|
|
306
|
+
self,
|
|
307
|
+
error_elem: ET.Element,
|
|
308
|
+
file_path: str,
|
|
309
|
+
project_root: Path,
|
|
310
|
+
) -> Optional[UnifiedIssue]:
|
|
311
|
+
"""Convert Checkstyle error element to UnifiedIssue.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
error_elem: XML error element.
|
|
315
|
+
file_path: File path from parent file element.
|
|
316
|
+
project_root: Project root directory.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
UnifiedIssue or None.
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
line = error_elem.get("line")
|
|
323
|
+
column = error_elem.get("column")
|
|
324
|
+
severity_str = error_elem.get("severity", "error")
|
|
325
|
+
message = error_elem.get("message", "")
|
|
326
|
+
source = error_elem.get("source", "")
|
|
327
|
+
|
|
328
|
+
# Extract rule name from source (e.g., "com.puppycrawl...WhitespaceAfterCheck")
|
|
329
|
+
rule = source.split(".")[-1] if source else ""
|
|
330
|
+
|
|
331
|
+
# Get severity
|
|
332
|
+
severity = SEVERITY_MAP.get(severity_str, Severity.MEDIUM)
|
|
333
|
+
|
|
334
|
+
# Build file path
|
|
335
|
+
path = Path(file_path)
|
|
336
|
+
if not path.is_absolute():
|
|
337
|
+
path = project_root / path
|
|
338
|
+
|
|
339
|
+
# Parse line/column
|
|
340
|
+
line_num = int(line) if line else None
|
|
341
|
+
col_num = int(column) if column else None
|
|
342
|
+
|
|
343
|
+
# Generate deterministic ID
|
|
344
|
+
issue_id = self._generate_issue_id(
|
|
345
|
+
rule, file_path, line_num, col_num, message
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return UnifiedIssue(
|
|
349
|
+
id=issue_id,
|
|
350
|
+
domain=ToolDomain.LINTING,
|
|
351
|
+
source_tool="checkstyle",
|
|
352
|
+
severity=severity,
|
|
353
|
+
rule_id=rule or "unknown",
|
|
354
|
+
title=f"[{rule}] {message}" if rule else message,
|
|
355
|
+
description=message,
|
|
356
|
+
documentation_url=f"https://checkstyle.org/checks.html#{rule}" if rule else None,
|
|
357
|
+
file_path=path,
|
|
358
|
+
line_start=line_num,
|
|
359
|
+
line_end=line_num,
|
|
360
|
+
column_start=col_num,
|
|
361
|
+
fixable=False, # Checkstyle doesn't support auto-fix
|
|
362
|
+
metadata={
|
|
363
|
+
"source": source,
|
|
364
|
+
"severity_raw": severity_str,
|
|
365
|
+
},
|
|
366
|
+
)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
LOGGER.warning(f"Failed to parse Checkstyle error: {e}")
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def _generate_issue_id(
|
|
372
|
+
self,
|
|
373
|
+
rule: str,
|
|
374
|
+
file: str,
|
|
375
|
+
line: Optional[int],
|
|
376
|
+
column: Optional[int],
|
|
377
|
+
message: str,
|
|
378
|
+
) -> str:
|
|
379
|
+
"""Generate deterministic issue ID.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
rule: Check/rule name.
|
|
383
|
+
file: File path.
|
|
384
|
+
line: Line number.
|
|
385
|
+
column: Column number.
|
|
386
|
+
message: Error message.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Unique issue ID.
|
|
390
|
+
"""
|
|
391
|
+
content = f"{rule}:{file}:{line or 0}:{column or 0}:{message}"
|
|
392
|
+
hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
|
|
393
|
+
return f"checkstyle-{rule}-{hash_val}" if rule else f"checkstyle-{hash_val}"
|