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,152 @@
|
|
|
1
|
+
"""Bridge between CLI arguments and configuration models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from lucidscan.config.models import LucidScanConfig
|
|
9
|
+
from lucidscan.core.logging import get_logger
|
|
10
|
+
from lucidscan.core.models import ScanDomain
|
|
11
|
+
|
|
12
|
+
LOGGER = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigBridge:
|
|
16
|
+
"""Translates CLI arguments to configuration objects."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def args_to_overrides(args: argparse.Namespace) -> Dict[str, Any]:
|
|
20
|
+
"""Convert CLI arguments to config override dict.
|
|
21
|
+
|
|
22
|
+
CLI arguments take precedence over config file values.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
args: Parsed CLI arguments.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary of config overrides.
|
|
29
|
+
"""
|
|
30
|
+
overrides: Dict[str, Any] = {}
|
|
31
|
+
|
|
32
|
+
# Domain toggles - only set if explicitly provided on CLI
|
|
33
|
+
# Use getattr with defaults for subcommand compatibility
|
|
34
|
+
scanners: Dict[str, Dict[str, Any]] = {}
|
|
35
|
+
linters: Dict[str, Dict[str, Any]] = {}
|
|
36
|
+
|
|
37
|
+
all_domains = getattr(args, "all", False)
|
|
38
|
+
sca = getattr(args, "sca", False)
|
|
39
|
+
sast = getattr(args, "sast", False)
|
|
40
|
+
iac = getattr(args, "iac", False)
|
|
41
|
+
container = getattr(args, "container", False)
|
|
42
|
+
linting = getattr(args, "linting", False)
|
|
43
|
+
fix = getattr(args, "fix", False)
|
|
44
|
+
images = getattr(args, "images", None)
|
|
45
|
+
|
|
46
|
+
if all_domains:
|
|
47
|
+
# Enable all domains including linting
|
|
48
|
+
for domain in ["sca", "sast", "iac", "container"]:
|
|
49
|
+
scanners[domain] = {"enabled": True}
|
|
50
|
+
linters["ruff"] = {"enabled": True}
|
|
51
|
+
else:
|
|
52
|
+
if sca:
|
|
53
|
+
scanners["sca"] = {"enabled": True}
|
|
54
|
+
if sast:
|
|
55
|
+
scanners["sast"] = {"enabled": True}
|
|
56
|
+
if iac:
|
|
57
|
+
scanners["iac"] = {"enabled": True}
|
|
58
|
+
if container:
|
|
59
|
+
scanners["container"] = {"enabled": True}
|
|
60
|
+
if linting:
|
|
61
|
+
linters["ruff"] = {"enabled": True}
|
|
62
|
+
|
|
63
|
+
# Container images go into container scanner options
|
|
64
|
+
if images:
|
|
65
|
+
if "container" not in scanners:
|
|
66
|
+
scanners["container"] = {}
|
|
67
|
+
scanners["container"]["enabled"] = True
|
|
68
|
+
scanners["container"]["images"] = images
|
|
69
|
+
|
|
70
|
+
if scanners:
|
|
71
|
+
overrides["scanners"] = scanners
|
|
72
|
+
|
|
73
|
+
if linters:
|
|
74
|
+
overrides["linters"] = linters
|
|
75
|
+
|
|
76
|
+
# Fix mode for linting
|
|
77
|
+
if fix:
|
|
78
|
+
overrides["fix"] = True
|
|
79
|
+
|
|
80
|
+
# Fail-on threshold
|
|
81
|
+
fail_on = getattr(args, "fail_on", None)
|
|
82
|
+
if fail_on:
|
|
83
|
+
overrides["fail_on"] = fail_on
|
|
84
|
+
|
|
85
|
+
return overrides
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def get_enabled_domains(
|
|
89
|
+
config: LucidScanConfig,
|
|
90
|
+
args: argparse.Namespace,
|
|
91
|
+
) -> List[ScanDomain]:
|
|
92
|
+
"""Determine which scan domains are enabled.
|
|
93
|
+
|
|
94
|
+
If specific CLI flags (--sca, --sast, etc.) are provided, use those.
|
|
95
|
+
If --all is provided, use domains from config file.
|
|
96
|
+
If other domain flags (--test, --coverage, --lint, --type-check) are set,
|
|
97
|
+
return empty list (user wants only those specific domains, not security).
|
|
98
|
+
Otherwise, use domains enabled in config file.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
config: Loaded configuration.
|
|
102
|
+
args: Parsed CLI arguments.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of enabled ScanDomain values.
|
|
106
|
+
"""
|
|
107
|
+
# Use getattr for subcommand compatibility
|
|
108
|
+
sca = getattr(args, "sca", False)
|
|
109
|
+
sast = getattr(args, "sast", False)
|
|
110
|
+
iac = getattr(args, "iac", False)
|
|
111
|
+
container = getattr(args, "container", False)
|
|
112
|
+
all_domains = getattr(args, "all", False)
|
|
113
|
+
|
|
114
|
+
# Check if specific security domain flags were set
|
|
115
|
+
security_domains_set = any([sca, sast, iac, container])
|
|
116
|
+
|
|
117
|
+
if security_domains_set:
|
|
118
|
+
# Specific CLI flags take precedence
|
|
119
|
+
domains: List[ScanDomain] = []
|
|
120
|
+
if sca:
|
|
121
|
+
domains.append(ScanDomain.SCA)
|
|
122
|
+
if sast:
|
|
123
|
+
domains.append(ScanDomain.SAST)
|
|
124
|
+
if iac:
|
|
125
|
+
domains.append(ScanDomain.IAC)
|
|
126
|
+
if container:
|
|
127
|
+
domains.append(ScanDomain.CONTAINER)
|
|
128
|
+
return domains
|
|
129
|
+
|
|
130
|
+
# Check if user specified non-security domain flags (testing, coverage, etc.)
|
|
131
|
+
# If so, they don't want security scanning unless explicitly requested
|
|
132
|
+
linting = getattr(args, "linting", False)
|
|
133
|
+
type_checking = getattr(args, "type_checking", False)
|
|
134
|
+
testing = getattr(args, "testing", False)
|
|
135
|
+
coverage = getattr(args, "coverage", False)
|
|
136
|
+
non_security_domains_set = any([linting, type_checking, testing, coverage])
|
|
137
|
+
|
|
138
|
+
if non_security_domains_set and not all_domains:
|
|
139
|
+
# User explicitly requested non-security domains only
|
|
140
|
+
# Don't run security scanners unless explicitly requested
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
# --all or no flags: use config file settings
|
|
144
|
+
# This respects what's actually configured in lucidscan.yml
|
|
145
|
+
enabled_domains: List[ScanDomain] = []
|
|
146
|
+
for domain_name in config.get_enabled_domains():
|
|
147
|
+
try:
|
|
148
|
+
enabled_domains.append(ScanDomain(domain_name))
|
|
149
|
+
except ValueError:
|
|
150
|
+
LOGGER.warning(f"Unknown domain in config: {domain_name}")
|
|
151
|
+
|
|
152
|
+
return enabled_domains
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Exit codes for lucidscan CLI.
|
|
2
|
+
|
|
3
|
+
Exit codes follow Section 14 of the specification:
|
|
4
|
+
- 0: Success (no issues found or below threshold)
|
|
5
|
+
- 1: Issues found at or above severity threshold
|
|
6
|
+
- 2: Scanner error
|
|
7
|
+
- 3: Invalid usage (bad arguments, missing config)
|
|
8
|
+
- 4: Bootstrap failure (binary download failed)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
EXIT_SUCCESS = 0
|
|
14
|
+
EXIT_ISSUES_FOUND = 1
|
|
15
|
+
EXIT_SCANNER_ERROR = 2
|
|
16
|
+
EXIT_INVALID_USAGE = 3
|
|
17
|
+
EXIT_BOOTSTRAP_FAILURE = 4
|
lucidscan/cli/runner.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""CLI runner orchestration.
|
|
2
|
+
|
|
3
|
+
This module handles command dispatch and execution for the lucidscan CLI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterable, Optional
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
12
|
+
|
|
13
|
+
from lucidscan.cli.arguments import build_parser
|
|
14
|
+
from lucidscan.cli.config_bridge import ConfigBridge
|
|
15
|
+
from lucidscan.cli.exit_codes import (
|
|
16
|
+
EXIT_INVALID_USAGE,
|
|
17
|
+
EXIT_SCANNER_ERROR,
|
|
18
|
+
EXIT_SUCCESS,
|
|
19
|
+
)
|
|
20
|
+
from lucidscan.cli.commands.status import StatusCommand
|
|
21
|
+
from lucidscan.cli.commands.scan import ScanCommand
|
|
22
|
+
from lucidscan.cli.commands.help import HelpCommand
|
|
23
|
+
from lucidscan.config import load_config
|
|
24
|
+
from lucidscan.config.loader import ConfigError
|
|
25
|
+
from lucidscan.core.logging import configure_logging, get_logger
|
|
26
|
+
|
|
27
|
+
LOGGER = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_version() -> str:
|
|
31
|
+
"""Get lucidscan version.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Version string from package metadata or fallback.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
return version("lucidscan")
|
|
38
|
+
except PackageNotFoundError:
|
|
39
|
+
# Fallback for editable installs that have not yet built metadata.
|
|
40
|
+
from lucidscan import __version__
|
|
41
|
+
return __version__
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CLIRunner:
|
|
45
|
+
"""Orchestrates CLI execution with subcommand dispatch."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
"""Initialize CLIRunner with parser and commands."""
|
|
49
|
+
self.parser = build_parser()
|
|
50
|
+
self._version = get_version()
|
|
51
|
+
self.status_cmd = StatusCommand(version=self._version)
|
|
52
|
+
self.scan_cmd = ScanCommand(version=self._version)
|
|
53
|
+
self.help_cmd = HelpCommand(version=self._version)
|
|
54
|
+
# InitCommand and AutoconfigureCommand will be imported lazily when needed
|
|
55
|
+
self._init_cmd = None
|
|
56
|
+
self._autoconfigure_cmd = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def init_cmd(self):
|
|
60
|
+
"""Lazy-load InitCommand to avoid import errors during development."""
|
|
61
|
+
if self._init_cmd is None:
|
|
62
|
+
try:
|
|
63
|
+
from lucidscan.cli.commands.init import InitCommand
|
|
64
|
+
self._init_cmd = InitCommand(version=self._version)
|
|
65
|
+
except ImportError:
|
|
66
|
+
self._init_cmd = None
|
|
67
|
+
return self._init_cmd
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def autoconfigure_cmd(self):
|
|
71
|
+
"""Lazy-load AutoconfigureCommand to avoid import errors during development."""
|
|
72
|
+
if self._autoconfigure_cmd is None:
|
|
73
|
+
try:
|
|
74
|
+
from lucidscan.cli.commands.autoconfigure import AutoconfigureCommand
|
|
75
|
+
self._autoconfigure_cmd = AutoconfigureCommand()
|
|
76
|
+
except ImportError:
|
|
77
|
+
self._autoconfigure_cmd = None
|
|
78
|
+
return self._autoconfigure_cmd
|
|
79
|
+
|
|
80
|
+
def run(self, argv: Optional[Iterable[str]] = None) -> int:
|
|
81
|
+
"""Run the CLI.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
argv: Command-line arguments (defaults to sys.argv).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Exit code.
|
|
88
|
+
"""
|
|
89
|
+
# Handle --help specially to return 0
|
|
90
|
+
if argv is not None:
|
|
91
|
+
argv_list = list(argv)
|
|
92
|
+
if "--help" in argv_list or "-h" in argv_list:
|
|
93
|
+
self.parser.print_help()
|
|
94
|
+
return EXIT_SUCCESS
|
|
95
|
+
else:
|
|
96
|
+
argv_list = None
|
|
97
|
+
|
|
98
|
+
args = self.parser.parse_args(argv_list)
|
|
99
|
+
|
|
100
|
+
# Configure logging as early as possible
|
|
101
|
+
configure_logging(
|
|
102
|
+
debug=args.debug,
|
|
103
|
+
verbose=args.verbose,
|
|
104
|
+
quiet=args.quiet,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Handle --version
|
|
108
|
+
if args.version:
|
|
109
|
+
print(self._version)
|
|
110
|
+
return EXIT_SUCCESS
|
|
111
|
+
|
|
112
|
+
# Dispatch to appropriate command handler
|
|
113
|
+
command = getattr(args, "command", None)
|
|
114
|
+
|
|
115
|
+
if command == "init":
|
|
116
|
+
return self._handle_init(args)
|
|
117
|
+
elif command == "autoconfigure":
|
|
118
|
+
return self._handle_autoconfigure(args)
|
|
119
|
+
elif command == "scan":
|
|
120
|
+
return self._handle_scan(args)
|
|
121
|
+
elif command == "status":
|
|
122
|
+
return self._handle_status(args)
|
|
123
|
+
elif command == "serve":
|
|
124
|
+
return self._handle_serve(args)
|
|
125
|
+
elif command == "help":
|
|
126
|
+
return self._handle_help(args)
|
|
127
|
+
elif command == "validate":
|
|
128
|
+
return self._handle_validate(args)
|
|
129
|
+
else:
|
|
130
|
+
# No command specified - show help
|
|
131
|
+
self.parser.print_help()
|
|
132
|
+
return EXIT_SUCCESS
|
|
133
|
+
|
|
134
|
+
def _handle_init(self, args) -> int:
|
|
135
|
+
"""Handle the init command (configure AI tools).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
args: Parsed command-line arguments.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Exit code.
|
|
142
|
+
"""
|
|
143
|
+
if self.init_cmd is None:
|
|
144
|
+
LOGGER.error("Init command not available. This feature is in development.")
|
|
145
|
+
return EXIT_INVALID_USAGE
|
|
146
|
+
|
|
147
|
+
return self.init_cmd.execute(args)
|
|
148
|
+
|
|
149
|
+
def _handle_autoconfigure(self, args) -> int:
|
|
150
|
+
"""Handle the autoconfigure command (generate lucidscan.yml).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
args: Parsed command-line arguments.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Exit code.
|
|
157
|
+
"""
|
|
158
|
+
if self.autoconfigure_cmd is None:
|
|
159
|
+
LOGGER.error("Autoconfigure command not available. This feature is in development.")
|
|
160
|
+
return EXIT_INVALID_USAGE
|
|
161
|
+
|
|
162
|
+
return self.autoconfigure_cmd.execute(args)
|
|
163
|
+
|
|
164
|
+
def _handle_scan(self, args) -> int:
|
|
165
|
+
"""Handle the scan command.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
args: Parsed command-line arguments.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Exit code.
|
|
172
|
+
"""
|
|
173
|
+
# Load configuration
|
|
174
|
+
project_root = Path(args.path).resolve()
|
|
175
|
+
cli_overrides = ConfigBridge.args_to_overrides(args)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
config = load_config(
|
|
179
|
+
project_root=project_root,
|
|
180
|
+
cli_config_path=getattr(args, "config", None),
|
|
181
|
+
cli_overrides=cli_overrides,
|
|
182
|
+
)
|
|
183
|
+
except ConfigError as e:
|
|
184
|
+
LOGGER.error(str(e))
|
|
185
|
+
return EXIT_INVALID_USAGE
|
|
186
|
+
|
|
187
|
+
# Check if any domains are enabled
|
|
188
|
+
cli_scan_requested = any([
|
|
189
|
+
getattr(args, "sca", False),
|
|
190
|
+
getattr(args, "container", False),
|
|
191
|
+
getattr(args, "iac", False),
|
|
192
|
+
getattr(args, "sast", False),
|
|
193
|
+
getattr(args, "linting", False),
|
|
194
|
+
getattr(args, "type_checking", False),
|
|
195
|
+
getattr(args, "testing", False),
|
|
196
|
+
getattr(args, "coverage", False),
|
|
197
|
+
getattr(args, "all", False),
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
config_has_enabled_domains = bool(config.get_enabled_domains())
|
|
201
|
+
|
|
202
|
+
if cli_scan_requested or config_has_enabled_domains:
|
|
203
|
+
try:
|
|
204
|
+
return self.scan_cmd.execute(args, config)
|
|
205
|
+
except FileNotFoundError:
|
|
206
|
+
return EXIT_INVALID_USAGE
|
|
207
|
+
except Exception as e:
|
|
208
|
+
if args.debug:
|
|
209
|
+
import traceback
|
|
210
|
+
traceback.print_exc()
|
|
211
|
+
LOGGER.error(f"Scan failed: {e}")
|
|
212
|
+
return EXIT_SCANNER_ERROR
|
|
213
|
+
|
|
214
|
+
# No scanners selected - show scan help
|
|
215
|
+
print("No scan domains selected. Use --sca, --sast, --iac, --linting, --type-checking, or --all.")
|
|
216
|
+
print("\nRun 'lucidscan scan --help' for more options.")
|
|
217
|
+
return EXIT_SUCCESS
|
|
218
|
+
|
|
219
|
+
def _handle_status(self, args) -> int:
|
|
220
|
+
"""Handle the status command.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
args: Parsed command-line arguments.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Exit code.
|
|
227
|
+
"""
|
|
228
|
+
return self.status_cmd.execute(args)
|
|
229
|
+
|
|
230
|
+
def _handle_serve(self, args) -> int:
|
|
231
|
+
"""Handle the serve command.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
args: Parsed command-line arguments.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Exit code.
|
|
238
|
+
"""
|
|
239
|
+
# Load configuration
|
|
240
|
+
project_root = Path(args.path).resolve()
|
|
241
|
+
cli_overrides = ConfigBridge.args_to_overrides(args)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
config = load_config(
|
|
245
|
+
project_root=project_root,
|
|
246
|
+
cli_config_path=getattr(args, "config", None),
|
|
247
|
+
cli_overrides=cli_overrides,
|
|
248
|
+
)
|
|
249
|
+
except ConfigError as e:
|
|
250
|
+
LOGGER.error(str(e))
|
|
251
|
+
return EXIT_INVALID_USAGE
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
from lucidscan.cli.commands.serve import ServeCommand
|
|
255
|
+
serve_cmd = ServeCommand(version=self._version)
|
|
256
|
+
return serve_cmd.execute(args, config)
|
|
257
|
+
except ImportError as e:
|
|
258
|
+
LOGGER.error(f"Serve command not available: {e}")
|
|
259
|
+
return EXIT_INVALID_USAGE
|
|
260
|
+
|
|
261
|
+
def _handle_help(self, args) -> int:
|
|
262
|
+
"""Handle the help command.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
args: Parsed command-line arguments.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Exit code.
|
|
269
|
+
"""
|
|
270
|
+
return self.help_cmd.execute(args)
|
|
271
|
+
|
|
272
|
+
def _handle_validate(self, args) -> int:
|
|
273
|
+
"""Handle the validate command.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
args: Parsed command-line arguments.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Exit code.
|
|
280
|
+
"""
|
|
281
|
+
from lucidscan.cli.commands.validate import ValidateCommand
|
|
282
|
+
|
|
283
|
+
validate_cmd = ValidateCommand()
|
|
284
|
+
return validate_cmd.execute(args)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Configuration module for lucidscan.
|
|
2
|
+
|
|
3
|
+
Provides configuration file loading, parsing, and validation with support for:
|
|
4
|
+
- Project-level config (.lucidscan.yml)
|
|
5
|
+
- Global config (~/.lucidscan/config/config.yml)
|
|
6
|
+
- Environment variable expansion
|
|
7
|
+
- Plugin-specific configuration passthrough
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from lucidscan.config.models import (
|
|
11
|
+
LucidScanConfig,
|
|
12
|
+
OutputConfig,
|
|
13
|
+
ScannerDomainConfig,
|
|
14
|
+
DEFAULT_PLUGINS,
|
|
15
|
+
)
|
|
16
|
+
from lucidscan.config.loader import load_config, find_project_config, find_global_config
|
|
17
|
+
from lucidscan.config.validation import validate_config, ConfigValidationWarning
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"LucidScanConfig",
|
|
21
|
+
"OutputConfig",
|
|
22
|
+
"ScannerDomainConfig",
|
|
23
|
+
"DEFAULT_PLUGINS",
|
|
24
|
+
"load_config",
|
|
25
|
+
"find_project_config",
|
|
26
|
+
"find_global_config",
|
|
27
|
+
"validate_config",
|
|
28
|
+
"ConfigValidationWarning",
|
|
29
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Gitignore-style pattern parsing and matching.
|
|
2
|
+
|
|
3
|
+
Handles loading and matching of ignore patterns from:
|
|
4
|
+
- .lucidscanignore file (gitignore syntax)
|
|
5
|
+
- config.ignore list (gitignore syntax)
|
|
6
|
+
|
|
7
|
+
Uses pathspec library for full gitignore compliance including:
|
|
8
|
+
- ** recursive globbing
|
|
9
|
+
- ! negation patterns
|
|
10
|
+
- # comments
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
17
|
+
|
|
18
|
+
import pathspec
|
|
19
|
+
|
|
20
|
+
from lucidscan.core.logging import get_logger
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
LOGGER = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
LUCIDSCANIGNORE_NAMES = [".lucidscanignore"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IgnorePatterns:
|
|
31
|
+
"""Manages ignore patterns from multiple sources."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
patterns: List[str],
|
|
36
|
+
source: str = "config",
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Initialize with a list of gitignore-style patterns.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
patterns: List of gitignore-style patterns.
|
|
42
|
+
source: Source description for logging.
|
|
43
|
+
"""
|
|
44
|
+
self._source = source
|
|
45
|
+
self._raw_patterns = patterns
|
|
46
|
+
|
|
47
|
+
# Filter out empty lines and comments for the PathSpec
|
|
48
|
+
clean_patterns = [
|
|
49
|
+
p for p in patterns if p.strip() and not p.strip().startswith("#")
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
self._spec = pathspec.PathSpec.from_lines(
|
|
53
|
+
pathspec.patterns.GitWildMatchPattern,
|
|
54
|
+
clean_patterns,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if clean_patterns:
|
|
58
|
+
LOGGER.debug(f"Loaded {len(clean_patterns)} ignore patterns from {source}")
|
|
59
|
+
|
|
60
|
+
def matches(self, path: Path, root: Path) -> bool:
|
|
61
|
+
"""Check if a path matches any ignore pattern.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
path: Path to check (absolute or relative).
|
|
65
|
+
root: Project root for relative path calculation.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if path should be ignored, False otherwise.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
rel_path = path.relative_to(root)
|
|
72
|
+
except ValueError:
|
|
73
|
+
rel_path = path
|
|
74
|
+
|
|
75
|
+
# pathspec expects forward-slash paths
|
|
76
|
+
rel_str = str(rel_path).replace("\\", "/")
|
|
77
|
+
return self._spec.match_file(rel_str)
|
|
78
|
+
|
|
79
|
+
def get_exclude_patterns(self) -> List[str]:
|
|
80
|
+
"""Get patterns suitable for scanner --exclude flags.
|
|
81
|
+
|
|
82
|
+
Returns clean patterns without comments, suitable for
|
|
83
|
+
passing to scanner CLIs.
|
|
84
|
+
"""
|
|
85
|
+
return [
|
|
86
|
+
p.strip()
|
|
87
|
+
for p in self._raw_patterns
|
|
88
|
+
if p.strip() and not p.strip().startswith("#")
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_file(cls, file_path: Path) -> Optional["IgnorePatterns"]:
|
|
93
|
+
"""Load patterns from a file.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path: Path to ignore file.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
IgnorePatterns instance, or None if file doesn't exist.
|
|
100
|
+
"""
|
|
101
|
+
if not file_path.exists():
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
content = file_path.read_text(encoding="utf-8")
|
|
106
|
+
patterns = content.splitlines()
|
|
107
|
+
return cls(patterns, source=str(file_path))
|
|
108
|
+
except Exception as e:
|
|
109
|
+
LOGGER.warning(f"Failed to load ignore file {file_path}: {e}")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def merge(cls, *pattern_sets: Optional["IgnorePatterns"]) -> "IgnorePatterns":
|
|
114
|
+
"""Merge multiple IgnorePatterns instances.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
pattern_sets: IgnorePatterns instances to merge.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
New IgnorePatterns with combined patterns.
|
|
121
|
+
"""
|
|
122
|
+
all_patterns: List[str] = []
|
|
123
|
+
sources: List[str] = []
|
|
124
|
+
|
|
125
|
+
for ps in pattern_sets:
|
|
126
|
+
if ps is not None:
|
|
127
|
+
all_patterns.extend(ps._raw_patterns)
|
|
128
|
+
sources.append(ps._source)
|
|
129
|
+
|
|
130
|
+
return cls(all_patterns, source="+".join(sources) if sources else "empty")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def find_lucidscanignore(project_root: Path) -> Optional[Path]:
|
|
134
|
+
"""Find .lucidscanignore file in project root.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
project_root: Project root directory.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Path to ignore file if found, None otherwise.
|
|
141
|
+
"""
|
|
142
|
+
for name in LUCIDSCANIGNORE_NAMES:
|
|
143
|
+
ignore_path = project_root / name
|
|
144
|
+
if ignore_path.exists():
|
|
145
|
+
return ignore_path
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_ignore_patterns(
|
|
150
|
+
project_root: Path,
|
|
151
|
+
config_patterns: List[str],
|
|
152
|
+
) -> IgnorePatterns:
|
|
153
|
+
"""Load and merge ignore patterns from all sources.
|
|
154
|
+
|
|
155
|
+
Loads patterns from:
|
|
156
|
+
1. .lucidscanignore file (if present)
|
|
157
|
+
2. config.ignore list from .lucidscan.yml
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
project_root: Project root directory.
|
|
161
|
+
config_patterns: Patterns from config.ignore.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Merged IgnorePatterns instance.
|
|
165
|
+
"""
|
|
166
|
+
# Load from .lucidscanignore
|
|
167
|
+
ignore_file = find_lucidscanignore(project_root)
|
|
168
|
+
file_patterns = IgnorePatterns.from_file(ignore_file) if ignore_file else None
|
|
169
|
+
|
|
170
|
+
# Load from config
|
|
171
|
+
config_ignore = (
|
|
172
|
+
IgnorePatterns(config_patterns, source="config.ignore")
|
|
173
|
+
if config_patterns
|
|
174
|
+
else None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Merge (file patterns first, then config)
|
|
178
|
+
return IgnorePatterns.merge(file_patterns, config_ignore)
|