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,656 @@
|
|
|
1
|
+
"""Init command implementation.
|
|
2
|
+
|
|
3
|
+
Configure AI tools (Claude Code, Cursor) to use LucidScan via MCP.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from argparse import Namespace
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from lucidscan.config.models import LucidScanConfig
|
|
17
|
+
|
|
18
|
+
from lucidscan.cli.commands import Command
|
|
19
|
+
from lucidscan.cli.exit_codes import EXIT_SUCCESS, EXIT_INVALID_USAGE
|
|
20
|
+
from lucidscan.core.logging import get_logger
|
|
21
|
+
|
|
22
|
+
LOGGER = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
# MCP server arguments for LucidScan
|
|
25
|
+
LUCIDSCAN_MCP_ARGS = ["serve", "--mcp"]
|
|
26
|
+
|
|
27
|
+
# Instructions to add to CLAUDE.md for proactive lucidscan usage
|
|
28
|
+
LUCIDSCAN_CLAUDE_MD_INSTRUCTIONS = """
|
|
29
|
+
## Code Quality with LucidScan
|
|
30
|
+
|
|
31
|
+
IMPORTANT: You MUST run LucidScan after completing code changes. This is required, not optional.
|
|
32
|
+
|
|
33
|
+
### When to Scan
|
|
34
|
+
|
|
35
|
+
Run a scan after completing a batch of related code changes (not after each individual edit):
|
|
36
|
+
- After finishing a feature or fix (before reporting completion to user)
|
|
37
|
+
- Before creating a commit
|
|
38
|
+
- When the user asks you to check code quality
|
|
39
|
+
|
|
40
|
+
### Fast Scan (After Code Changes)
|
|
41
|
+
|
|
42
|
+
After completing code modifications, run a scan (scans changed files by default):
|
|
43
|
+
```
|
|
44
|
+
scan(domains=["linting", "type_checking", "sast", "sca"])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For full project scan:
|
|
48
|
+
```
|
|
49
|
+
scan(domains=["linting", "type_checking", "sast", "sca"], all_files=true)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Full Scan (Before Commit)
|
|
53
|
+
|
|
54
|
+
Before committing, run a full scan including tests:
|
|
55
|
+
```
|
|
56
|
+
scan(domains=["all"])
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Fixing Issues
|
|
60
|
+
|
|
61
|
+
1. Run `scan(fix=true)` to auto-fix linting issues
|
|
62
|
+
2. Use `get_fix_instructions(issue_id)` for detailed guidance on specific issues
|
|
63
|
+
3. Re-scan after manual fixes to verify resolution
|
|
64
|
+
|
|
65
|
+
### Workflow Example
|
|
66
|
+
|
|
67
|
+
1. Make code changes (multiple edits)
|
|
68
|
+
2. When done with changes, run `scan(domains=["linting", "type_checking"], fix=true)`
|
|
69
|
+
3. Fix any remaining issues that couldn't be auto-fixed
|
|
70
|
+
4. Re-scan to verify all issues resolved
|
|
71
|
+
5. Report completion to user
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
LUCIDSCAN_CLAUDE_MD_MARKER = "## Code Quality with LucidScan"
|
|
75
|
+
|
|
76
|
+
# Cursor rules for proactive lucidscan usage
|
|
77
|
+
LUCIDSCAN_CURSOR_RULES = """---
|
|
78
|
+
description: LucidScan code quality rules
|
|
79
|
+
globs: ["**/*.py", "**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx"]
|
|
80
|
+
alwaysApply: true
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# LucidScan Code Quality
|
|
84
|
+
|
|
85
|
+
IMPORTANT: You MUST run LucidScan after completing code changes. This is required, not optional.
|
|
86
|
+
|
|
87
|
+
## When to Scan
|
|
88
|
+
|
|
89
|
+
Run a scan after completing a batch of related code changes (not after each individual edit):
|
|
90
|
+
- After finishing a feature or fix (before reporting completion to user)
|
|
91
|
+
- Before creating a commit
|
|
92
|
+
- When the user asks you to check code quality
|
|
93
|
+
|
|
94
|
+
## Fast Scan (After Code Changes)
|
|
95
|
+
|
|
96
|
+
After completing code modifications, run a scan (scans changed files by default):
|
|
97
|
+
```
|
|
98
|
+
scan(domains=["linting", "type_checking", "sast", "sca"])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For full project scan:
|
|
102
|
+
```
|
|
103
|
+
scan(domains=["linting", "type_checking", "sast", "sca"], all_files=true)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Full Scan (Before Commit)
|
|
107
|
+
|
|
108
|
+
Before committing, run a full scan including tests:
|
|
109
|
+
```
|
|
110
|
+
scan(domains=["all"])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Fixing Issues
|
|
114
|
+
|
|
115
|
+
1. Run `scan(fix=true)` to auto-fix linting issues
|
|
116
|
+
2. Use `get_fix_instructions(issue_id)` for detailed guidance on specific issues
|
|
117
|
+
3. Re-scan after manual fixes to verify resolution
|
|
118
|
+
|
|
119
|
+
## Workflow Example
|
|
120
|
+
|
|
121
|
+
1. Make code changes (multiple edits)
|
|
122
|
+
2. When done with changes, run `scan(domains=["linting", "type_checking"], fix=true)`
|
|
123
|
+
3. Fix any remaining issues that couldn't be auto-fixed
|
|
124
|
+
4. Re-scan to verify all issues resolved
|
|
125
|
+
5. Report completion to user
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
class InitCommand(Command):
|
|
129
|
+
"""Configure AI tools to use LucidScan via MCP."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, version: str):
|
|
132
|
+
"""Initialize InitCommand.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
version: Current lucidscan version string.
|
|
136
|
+
"""
|
|
137
|
+
self._version = version
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def name(self) -> str:
|
|
141
|
+
"""Command identifier."""
|
|
142
|
+
return "init"
|
|
143
|
+
|
|
144
|
+
def execute(self, args: Namespace, config: "LucidScanConfig | None" = None) -> int:
|
|
145
|
+
"""Execute the init command.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
args: Parsed command-line arguments.
|
|
149
|
+
config: Optional LucidScan configuration (unused).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Exit code.
|
|
153
|
+
"""
|
|
154
|
+
# Determine which tools to configure
|
|
155
|
+
configure_claude = getattr(args, "claude_code", False)
|
|
156
|
+
configure_cursor = getattr(args, "cursor", False)
|
|
157
|
+
configure_all = getattr(args, "init_all", False)
|
|
158
|
+
|
|
159
|
+
if configure_all:
|
|
160
|
+
configure_claude = True
|
|
161
|
+
configure_cursor = True
|
|
162
|
+
|
|
163
|
+
if not configure_claude and not configure_cursor:
|
|
164
|
+
print("No AI tool specified. Use --claude-code, --cursor, or --all.")
|
|
165
|
+
print("\nRun 'lucidscan init --help' for more options.")
|
|
166
|
+
return EXIT_INVALID_USAGE
|
|
167
|
+
|
|
168
|
+
dry_run = getattr(args, "dry_run", False)
|
|
169
|
+
force = getattr(args, "force", False)
|
|
170
|
+
remove = getattr(args, "remove", False)
|
|
171
|
+
|
|
172
|
+
success = True
|
|
173
|
+
|
|
174
|
+
if configure_claude:
|
|
175
|
+
if not self._setup_claude_code(dry_run, force, remove):
|
|
176
|
+
success = False
|
|
177
|
+
|
|
178
|
+
if configure_cursor:
|
|
179
|
+
if not self._setup_cursor(dry_run, force, remove):
|
|
180
|
+
success = False
|
|
181
|
+
|
|
182
|
+
if success and not dry_run:
|
|
183
|
+
print("\nRestart your AI tool to apply changes.")
|
|
184
|
+
|
|
185
|
+
return EXIT_SUCCESS if success else EXIT_INVALID_USAGE
|
|
186
|
+
|
|
187
|
+
def _setup_claude_code(
|
|
188
|
+
self,
|
|
189
|
+
dry_run: bool = False,
|
|
190
|
+
force: bool = False,
|
|
191
|
+
remove: bool = False,
|
|
192
|
+
) -> bool:
|
|
193
|
+
"""Configure Claude Code MCP settings in project .mcp.json.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
dry_run: If True, only show what would be done.
|
|
197
|
+
force: If True, overwrite existing config.
|
|
198
|
+
remove: If True, remove LucidScan from config.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if successful.
|
|
202
|
+
"""
|
|
203
|
+
print("Configuring Claude Code (.mcp.json)...")
|
|
204
|
+
|
|
205
|
+
config_path = self._get_claude_code_config_path()
|
|
206
|
+
if config_path is None:
|
|
207
|
+
print(" Could not determine Claude Code config location.")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
mcp_success = self._configure_mcp_tool(
|
|
211
|
+
tool_name="Claude Code",
|
|
212
|
+
config_path=config_path,
|
|
213
|
+
config_key="mcpServers",
|
|
214
|
+
dry_run=dry_run,
|
|
215
|
+
force=force,
|
|
216
|
+
remove=remove,
|
|
217
|
+
use_portable_path=True, # .mcp.json is version controlled
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Also configure CLAUDE.md with instructions
|
|
221
|
+
claude_md_success = self._configure_claude_md(
|
|
222
|
+
dry_run=dry_run,
|
|
223
|
+
force=force,
|
|
224
|
+
remove=remove,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return mcp_success and claude_md_success
|
|
228
|
+
|
|
229
|
+
def _setup_cursor(
|
|
230
|
+
self,
|
|
231
|
+
dry_run: bool = False,
|
|
232
|
+
force: bool = False,
|
|
233
|
+
remove: bool = False,
|
|
234
|
+
) -> bool:
|
|
235
|
+
"""Configure Cursor MCP settings.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
dry_run: If True, only show what would be done.
|
|
239
|
+
force: If True, overwrite existing config.
|
|
240
|
+
remove: If True, remove LucidScan from config.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if successful.
|
|
244
|
+
"""
|
|
245
|
+
print("Configuring Cursor...")
|
|
246
|
+
|
|
247
|
+
config_path = self._get_cursor_config_path()
|
|
248
|
+
if config_path is None:
|
|
249
|
+
print(" Could not determine Cursor config location.")
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
mcp_success = self._configure_mcp_tool(
|
|
253
|
+
tool_name="Cursor",
|
|
254
|
+
config_path=config_path,
|
|
255
|
+
config_key="mcpServers",
|
|
256
|
+
dry_run=dry_run,
|
|
257
|
+
force=force,
|
|
258
|
+
remove=remove,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Configure Cursor rules for automatic scanning
|
|
262
|
+
rules_success = self._configure_cursor_rules(
|
|
263
|
+
dry_run=dry_run,
|
|
264
|
+
force=force,
|
|
265
|
+
remove=remove,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return mcp_success and rules_success
|
|
269
|
+
|
|
270
|
+
def _find_lucidscan_path(self, portable: bool = False) -> Optional[str]:
|
|
271
|
+
"""Find the lucidscan executable path.
|
|
272
|
+
|
|
273
|
+
Searches in order:
|
|
274
|
+
1. PATH via shutil.which
|
|
275
|
+
2. Same directory as current Python interpreter (for venv installs)
|
|
276
|
+
3. Scripts directory on Windows
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
portable: If True, return a relative path suitable for version control.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Path to lucidscan executable, or None if not found.
|
|
283
|
+
"""
|
|
284
|
+
# First try PATH (only if not looking for portable path)
|
|
285
|
+
if not portable:
|
|
286
|
+
lucidscan_path = shutil.which("lucidscan")
|
|
287
|
+
if lucidscan_path:
|
|
288
|
+
return lucidscan_path
|
|
289
|
+
|
|
290
|
+
# Try to find in the same directory as the Python interpreter
|
|
291
|
+
# This handles venv installations where lucidscan isn't in global PATH
|
|
292
|
+
python_dir = Path(sys.executable).parent
|
|
293
|
+
cwd = Path.cwd()
|
|
294
|
+
|
|
295
|
+
if sys.platform == "win32":
|
|
296
|
+
# On Windows, check both Scripts and the python directory
|
|
297
|
+
candidates = [
|
|
298
|
+
python_dir / "lucidscan.exe",
|
|
299
|
+
python_dir / "Scripts" / "lucidscan.exe",
|
|
300
|
+
]
|
|
301
|
+
else:
|
|
302
|
+
# On Unix-like systems
|
|
303
|
+
candidates = [
|
|
304
|
+
python_dir / "lucidscan",
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
for candidate in candidates:
|
|
308
|
+
if candidate.exists():
|
|
309
|
+
if portable:
|
|
310
|
+
# Try to make it relative to cwd for version control
|
|
311
|
+
try:
|
|
312
|
+
relative = candidate.relative_to(cwd)
|
|
313
|
+
return str(relative)
|
|
314
|
+
except ValueError:
|
|
315
|
+
# Not relative to cwd, can't use portable path
|
|
316
|
+
pass
|
|
317
|
+
else:
|
|
318
|
+
return str(candidate)
|
|
319
|
+
|
|
320
|
+
# For portable, fall back to just "lucidscan"
|
|
321
|
+
if portable:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
def _build_mcp_config(self, lucidscan_path: Optional[str]) -> dict:
|
|
327
|
+
"""Build MCP server configuration.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
lucidscan_path: Full path to lucidscan executable, or None.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
MCP server configuration dict.
|
|
334
|
+
"""
|
|
335
|
+
command = lucidscan_path if lucidscan_path else "lucidscan"
|
|
336
|
+
return {
|
|
337
|
+
"command": command,
|
|
338
|
+
"args": LUCIDSCAN_MCP_ARGS.copy(),
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
def _configure_mcp_tool(
|
|
342
|
+
self,
|
|
343
|
+
tool_name: str,
|
|
344
|
+
config_path: Path,
|
|
345
|
+
config_key: str,
|
|
346
|
+
dry_run: bool = False,
|
|
347
|
+
force: bool = False,
|
|
348
|
+
remove: bool = False,
|
|
349
|
+
use_portable_path: bool = False,
|
|
350
|
+
) -> bool:
|
|
351
|
+
"""Configure an MCP-compatible tool.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
tool_name: Name of the tool for display.
|
|
355
|
+
config_path: Path to the config file.
|
|
356
|
+
config_key: Key in the config for MCP servers.
|
|
357
|
+
dry_run: If True, only show what would be done.
|
|
358
|
+
force: If True, overwrite existing config.
|
|
359
|
+
remove: If True, remove LucidScan from config.
|
|
360
|
+
use_portable_path: If True, use relative path for version control.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
True if successful.
|
|
364
|
+
"""
|
|
365
|
+
# Find lucidscan executable
|
|
366
|
+
lucidscan_path = self._find_lucidscan_path(portable=use_portable_path)
|
|
367
|
+
if lucidscan_path:
|
|
368
|
+
print(f" Using lucidscan command: {lucidscan_path}")
|
|
369
|
+
elif not dry_run:
|
|
370
|
+
print(" Warning: 'lucidscan' command not found in PATH or venv.")
|
|
371
|
+
print(" Using 'lucidscan' as command (must be in PATH at runtime).")
|
|
372
|
+
|
|
373
|
+
# Read existing config
|
|
374
|
+
config, error = self._read_json_config(config_path)
|
|
375
|
+
if error and not remove:
|
|
376
|
+
# For new config, start fresh
|
|
377
|
+
config = {}
|
|
378
|
+
|
|
379
|
+
# Get or create the MCP servers section
|
|
380
|
+
mcp_servers = config.get(config_key, {})
|
|
381
|
+
|
|
382
|
+
if remove:
|
|
383
|
+
# Remove LucidScan from config
|
|
384
|
+
if "lucidscan" in mcp_servers:
|
|
385
|
+
if dry_run:
|
|
386
|
+
print(f" Would remove lucidscan from {config_path}")
|
|
387
|
+
else:
|
|
388
|
+
del mcp_servers["lucidscan"]
|
|
389
|
+
config[config_key] = mcp_servers
|
|
390
|
+
if not mcp_servers:
|
|
391
|
+
del config[config_key]
|
|
392
|
+
self._write_json_config(config_path, config)
|
|
393
|
+
print(f" Removed lucidscan from {config_path}")
|
|
394
|
+
else:
|
|
395
|
+
print(f" lucidscan not found in {config_path}")
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
# Check if LucidScan is already configured
|
|
399
|
+
if "lucidscan" in mcp_servers and not force:
|
|
400
|
+
print(f" LucidScan already configured in {config_path}")
|
|
401
|
+
print(" Use --force to overwrite.")
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
# Add LucidScan config with found path
|
|
405
|
+
mcp_config = self._build_mcp_config(lucidscan_path)
|
|
406
|
+
mcp_servers["lucidscan"] = mcp_config
|
|
407
|
+
config[config_key] = mcp_servers
|
|
408
|
+
|
|
409
|
+
if dry_run:
|
|
410
|
+
print(f" Would write to {config_path}:")
|
|
411
|
+
print(f" {json.dumps(config, indent=2)}")
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
# Ensure parent directory exists
|
|
415
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
|
|
417
|
+
# Write config
|
|
418
|
+
success = self._write_json_config(config_path, config)
|
|
419
|
+
if success:
|
|
420
|
+
print(f" Added lucidscan to {config_path}")
|
|
421
|
+
self._print_available_tools()
|
|
422
|
+
return success
|
|
423
|
+
|
|
424
|
+
def _configure_claude_md(
|
|
425
|
+
self,
|
|
426
|
+
dry_run: bool = False,
|
|
427
|
+
force: bool = False,
|
|
428
|
+
remove: bool = False,
|
|
429
|
+
) -> bool:
|
|
430
|
+
"""Configure CLAUDE.md with lucidscan instructions.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
dry_run: If True, only show what would be done.
|
|
434
|
+
force: If True, overwrite existing instructions.
|
|
435
|
+
remove: If True, remove lucidscan instructions.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if successful.
|
|
439
|
+
"""
|
|
440
|
+
claude_md_path = Path.cwd() / ".claude" / "CLAUDE.md"
|
|
441
|
+
|
|
442
|
+
print("Configuring CLAUDE.md...")
|
|
443
|
+
|
|
444
|
+
# Read existing content
|
|
445
|
+
existing_content = ""
|
|
446
|
+
if claude_md_path.exists():
|
|
447
|
+
try:
|
|
448
|
+
existing_content = claude_md_path.read_text()
|
|
449
|
+
except Exception as e:
|
|
450
|
+
print(f" Error reading {claude_md_path}: {e}")
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
has_lucidscan_section = LUCIDSCAN_CLAUDE_MD_MARKER in existing_content
|
|
454
|
+
|
|
455
|
+
if remove:
|
|
456
|
+
if has_lucidscan_section:
|
|
457
|
+
if dry_run:
|
|
458
|
+
print(f" Would remove lucidscan instructions from {claude_md_path}")
|
|
459
|
+
else:
|
|
460
|
+
# Remove the lucidscan section
|
|
461
|
+
new_content = self._remove_lucidscan_section(existing_content)
|
|
462
|
+
try:
|
|
463
|
+
claude_md_path.write_text(new_content)
|
|
464
|
+
print(f" Removed lucidscan instructions from {claude_md_path}")
|
|
465
|
+
except Exception as e:
|
|
466
|
+
print(f" Error writing {claude_md_path}: {e}")
|
|
467
|
+
return False
|
|
468
|
+
else:
|
|
469
|
+
print(f" Lucidscan instructions not found in {claude_md_path}")
|
|
470
|
+
return True
|
|
471
|
+
|
|
472
|
+
if has_lucidscan_section and not force:
|
|
473
|
+
print(f" Lucidscan instructions already in {claude_md_path}")
|
|
474
|
+
print(" Use --force to overwrite.")
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
# Build new content
|
|
478
|
+
if has_lucidscan_section:
|
|
479
|
+
# Replace existing section
|
|
480
|
+
new_content = self._remove_lucidscan_section(existing_content)
|
|
481
|
+
new_content = new_content.rstrip() + LUCIDSCAN_CLAUDE_MD_INSTRUCTIONS
|
|
482
|
+
else:
|
|
483
|
+
# Append to existing content
|
|
484
|
+
new_content = existing_content.rstrip() + LUCIDSCAN_CLAUDE_MD_INSTRUCTIONS
|
|
485
|
+
|
|
486
|
+
if dry_run:
|
|
487
|
+
print(f" Would add lucidscan instructions to {claude_md_path}")
|
|
488
|
+
return True
|
|
489
|
+
|
|
490
|
+
# Ensure directory exists
|
|
491
|
+
claude_md_path.parent.mkdir(parents=True, exist_ok=True)
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
claude_md_path.write_text(new_content)
|
|
495
|
+
print(f" Added lucidscan instructions to {claude_md_path}")
|
|
496
|
+
return True
|
|
497
|
+
except Exception as e:
|
|
498
|
+
print(f" Error writing {claude_md_path}: {e}")
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
def _remove_lucidscan_section(self, content: str) -> str:
|
|
502
|
+
"""Remove the lucidscan section from CLAUDE.md content.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
content: The current CLAUDE.md content.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Content with lucidscan section removed.
|
|
509
|
+
"""
|
|
510
|
+
lines = content.split("\n")
|
|
511
|
+
new_lines = []
|
|
512
|
+
in_lucidscan_section = False
|
|
513
|
+
|
|
514
|
+
for line in lines:
|
|
515
|
+
if line.strip() == LUCIDSCAN_CLAUDE_MD_MARKER.strip():
|
|
516
|
+
in_lucidscan_section = True
|
|
517
|
+
continue
|
|
518
|
+
if in_lucidscan_section:
|
|
519
|
+
# Check if we've hit another section (line starting with ##)
|
|
520
|
+
if line.startswith("## ") and LUCIDSCAN_CLAUDE_MD_MARKER.strip() not in line:
|
|
521
|
+
in_lucidscan_section = False
|
|
522
|
+
new_lines.append(line)
|
|
523
|
+
# Skip lines in the lucidscan section
|
|
524
|
+
continue
|
|
525
|
+
new_lines.append(line)
|
|
526
|
+
|
|
527
|
+
return "\n".join(new_lines)
|
|
528
|
+
|
|
529
|
+
def _configure_cursor_rules(
|
|
530
|
+
self,
|
|
531
|
+
dry_run: bool = False,
|
|
532
|
+
force: bool = False,
|
|
533
|
+
remove: bool = False,
|
|
534
|
+
) -> bool:
|
|
535
|
+
"""Configure Cursor rules for automatic scanning.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
dry_run: If True, only show what would be done.
|
|
539
|
+
force: If True, overwrite existing rules.
|
|
540
|
+
remove: If True, remove lucidscan rules.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
True if successful.
|
|
544
|
+
"""
|
|
545
|
+
rules_dir = Path.cwd() / ".cursor" / "rules"
|
|
546
|
+
rules_file = rules_dir / "lucidscan.mdc"
|
|
547
|
+
|
|
548
|
+
print("Configuring Cursor rules...")
|
|
549
|
+
|
|
550
|
+
if remove:
|
|
551
|
+
if rules_file.exists():
|
|
552
|
+
if dry_run:
|
|
553
|
+
print(f" Would remove {rules_file}")
|
|
554
|
+
else:
|
|
555
|
+
rules_file.unlink()
|
|
556
|
+
print(f" Removed {rules_file}")
|
|
557
|
+
else:
|
|
558
|
+
print(f" LucidScan rules not found at {rules_file}")
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
if rules_file.exists() and not force:
|
|
562
|
+
print(f" LucidScan rules already exist at {rules_file}")
|
|
563
|
+
print(" Use --force to overwrite.")
|
|
564
|
+
return True
|
|
565
|
+
|
|
566
|
+
if dry_run:
|
|
567
|
+
print(f" Would create {rules_file}")
|
|
568
|
+
return True
|
|
569
|
+
|
|
570
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
571
|
+
try:
|
|
572
|
+
rules_file.write_text(LUCIDSCAN_CURSOR_RULES.lstrip())
|
|
573
|
+
print(f" Created {rules_file}")
|
|
574
|
+
return True
|
|
575
|
+
except Exception as e:
|
|
576
|
+
print(f" Error writing {rules_file}: {e}")
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
def _get_claude_code_config_path(self) -> Optional[Path]:
|
|
580
|
+
"""Get the Claude Code MCP config file path.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Path to .mcp.json at project root.
|
|
584
|
+
"""
|
|
585
|
+
# Claude Code project-scoped MCP servers in .mcp.json
|
|
586
|
+
return Path.cwd() / ".mcp.json"
|
|
587
|
+
|
|
588
|
+
def _get_cursor_config_path(self) -> Optional[Path]:
|
|
589
|
+
"""Get the Cursor MCP config file path.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
Path to config file or None if not determinable.
|
|
593
|
+
"""
|
|
594
|
+
home = Path.home()
|
|
595
|
+
|
|
596
|
+
if sys.platform == "win32":
|
|
597
|
+
# Windows: %USERPROFILE%\.cursor\mcp.json
|
|
598
|
+
return home / ".cursor" / "mcp.json"
|
|
599
|
+
elif sys.platform == "darwin":
|
|
600
|
+
# macOS: ~/.cursor/mcp.json
|
|
601
|
+
return home / ".cursor" / "mcp.json"
|
|
602
|
+
else:
|
|
603
|
+
# Linux: ~/.cursor/mcp.json
|
|
604
|
+
return home / ".cursor" / "mcp.json"
|
|
605
|
+
|
|
606
|
+
def _read_json_config(self, path: Path) -> Tuple[Dict[str, Any], Optional[str]]:
|
|
607
|
+
"""Read a JSON config file.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
path: Path to the config file.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Tuple of (config dict, error message or None).
|
|
614
|
+
"""
|
|
615
|
+
if not path.exists():
|
|
616
|
+
return {}, f"Config file does not exist: {path}"
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
with open(path, "r") as f:
|
|
620
|
+
content = f.read().strip()
|
|
621
|
+
if not content:
|
|
622
|
+
return {}, None
|
|
623
|
+
return json.loads(content), None
|
|
624
|
+
except json.JSONDecodeError as e:
|
|
625
|
+
return {}, f"Invalid JSON in {path}: {e}"
|
|
626
|
+
except Exception as e:
|
|
627
|
+
return {}, f"Error reading {path}: {e}"
|
|
628
|
+
|
|
629
|
+
def _write_json_config(self, path: Path, config: Dict[str, Any]) -> bool:
|
|
630
|
+
"""Write a JSON config file.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
path: Path to the config file.
|
|
634
|
+
config: Configuration dictionary.
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
True if successful.
|
|
638
|
+
"""
|
|
639
|
+
try:
|
|
640
|
+
with open(path, "w") as f:
|
|
641
|
+
json.dump(config, f, indent=2)
|
|
642
|
+
f.write("\n")
|
|
643
|
+
return True
|
|
644
|
+
except Exception as e:
|
|
645
|
+
print(f" Error writing {path}: {e}")
|
|
646
|
+
return False
|
|
647
|
+
|
|
648
|
+
def _print_available_tools(self) -> None:
|
|
649
|
+
"""Print available MCP tools."""
|
|
650
|
+
print("\n Available MCP tools:")
|
|
651
|
+
print(" - scan: Run quality checks on the codebase")
|
|
652
|
+
print(" - check_file: Check a specific file")
|
|
653
|
+
print(" - get_fix_instructions: Get detailed fix guidance")
|
|
654
|
+
print(" - apply_fix: Auto-fix linting issues")
|
|
655
|
+
print(" - get_status: Show LucidScan configuration")
|
|
656
|
+
print(" - autoconfigure: Get instructions for generating lucidscan.yml")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""List scanners command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from lucidscan.config.models import LucidScanConfig
|
|
10
|
+
|
|
11
|
+
from lucidscan.cli.commands import Command
|
|
12
|
+
from lucidscan.cli.exit_codes import EXIT_SUCCESS
|
|
13
|
+
from lucidscan.plugins.scanners import discover_scanner_plugins
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ListScannersCommand(Command):
|
|
17
|
+
"""Lists all available scanner plugins."""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
"""Command identifier."""
|
|
22
|
+
return "list_scanners"
|
|
23
|
+
|
|
24
|
+
def execute(self, args: Namespace, config: "LucidScanConfig | None" = None) -> int:
|
|
25
|
+
"""Execute the list-scanners command.
|
|
26
|
+
|
|
27
|
+
Displays all available scanner plugins with their domains and versions.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
args: Parsed command-line arguments.
|
|
31
|
+
config: Optional LucidScan configuration (unused).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Exit code (always 0 for list-scanners).
|
|
35
|
+
"""
|
|
36
|
+
plugins = discover_scanner_plugins()
|
|
37
|
+
|
|
38
|
+
print("Available scanner plugins:")
|
|
39
|
+
print()
|
|
40
|
+
|
|
41
|
+
if plugins:
|
|
42
|
+
for name, plugin_class in sorted(plugins.items()):
|
|
43
|
+
try:
|
|
44
|
+
plugin = plugin_class()
|
|
45
|
+
domains = ", ".join(d.value.upper() for d in plugin.domains)
|
|
46
|
+
version_str = plugin.get_version()
|
|
47
|
+
print(f" {name}")
|
|
48
|
+
print(f" Domains: {domains}")
|
|
49
|
+
print(f" Version: {version_str}")
|
|
50
|
+
print()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f" {name}: error loading plugin ({e})")
|
|
53
|
+
print()
|
|
54
|
+
else:
|
|
55
|
+
print(" No plugins discovered.")
|
|
56
|
+
print()
|
|
57
|
+
print("Install plugins via pip, e.g.: pip install lucidscan-snyk")
|
|
58
|
+
|
|
59
|
+
return EXIT_SUCCESS
|