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
lucidscan/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bootstrap module for lucidscan plugin binary management.
|
|
3
|
+
|
|
4
|
+
This module handles:
|
|
5
|
+
- Platform detection (OS + architecture)
|
|
6
|
+
- Plugin binary directory management (~/.lucidscan/bin/)
|
|
7
|
+
- Binary validation utilities
|
|
8
|
+
|
|
9
|
+
Each scanner plugin is responsible for downloading its own binary
|
|
10
|
+
using the utilities provided by this module.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from lucidscan.bootstrap.platform import get_platform_info, PlatformInfo
|
|
14
|
+
from lucidscan.bootstrap.paths import get_lucidscan_home, LucidscanPaths
|
|
15
|
+
from lucidscan.bootstrap.validation import validate_binary, PluginValidationResult, ToolStatus
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"get_platform_info",
|
|
19
|
+
"PlatformInfo",
|
|
20
|
+
"get_lucidscan_home",
|
|
21
|
+
"LucidscanPaths",
|
|
22
|
+
"validate_binary",
|
|
23
|
+
"PluginValidationResult",
|
|
24
|
+
"ToolStatus",
|
|
25
|
+
]
|
|
26
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Path management for lucidscan plugin binary cache.
|
|
2
|
+
|
|
3
|
+
Handles the .lucidscan directory structure and path resolution.
|
|
4
|
+
Each scanner plugin manages its own binary under .lucidscan/bin/{tool}/{version}/.
|
|
5
|
+
|
|
6
|
+
By default, tools are stored in the project root under .lucidscan/.
|
|
7
|
+
The LUCIDSCAN_HOME environment variable can override this for global installations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import ClassVar, Optional
|
|
16
|
+
|
|
17
|
+
# Default directory name
|
|
18
|
+
DEFAULT_HOME_DIR_NAME = ".lucidscan"
|
|
19
|
+
|
|
20
|
+
# Environment variable to override home directory (for global installations)
|
|
21
|
+
LUCIDSCAN_HOME_ENV = "LUCIDSCAN_HOME"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_lucidscan_home(project_root: Optional[Path] = None) -> Path:
|
|
25
|
+
"""Get the lucidscan home directory path.
|
|
26
|
+
|
|
27
|
+
Resolution order:
|
|
28
|
+
1. LUCIDSCAN_HOME environment variable (if set) - for global installations
|
|
29
|
+
2. {project_root}/.lucidscan (if project_root provided)
|
|
30
|
+
3. {cwd}/.lucidscan (default)
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project_root: Optional project root directory. If not provided,
|
|
34
|
+
uses current working directory.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to the lucidscan home directory.
|
|
38
|
+
"""
|
|
39
|
+
# Global override takes precedence
|
|
40
|
+
env_home = os.environ.get(LUCIDSCAN_HOME_ENV)
|
|
41
|
+
if env_home:
|
|
42
|
+
return Path(env_home)
|
|
43
|
+
|
|
44
|
+
# Use project root or current directory
|
|
45
|
+
if project_root:
|
|
46
|
+
return project_root / DEFAULT_HOME_DIR_NAME
|
|
47
|
+
|
|
48
|
+
return Path.cwd() / DEFAULT_HOME_DIR_NAME
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class LucidscanPaths:
|
|
53
|
+
"""Manages paths within the lucidscan home directory.
|
|
54
|
+
|
|
55
|
+
Directory structure (plugin-based):
|
|
56
|
+
{project}/.lucidscan/
|
|
57
|
+
bin/
|
|
58
|
+
trivy/{version}/trivy - Trivy binary
|
|
59
|
+
opengrep/{version}/opengrep - OpenGrep binary
|
|
60
|
+
checkov/{version}/venv/ - Checkov virtualenv
|
|
61
|
+
ruff/{version}/ruff - Ruff binary
|
|
62
|
+
cache/
|
|
63
|
+
trivy/ - Trivy vulnerability DB
|
|
64
|
+
config/ - Configuration files
|
|
65
|
+
logs/ - Debug/diagnostic logs
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
home: Path
|
|
69
|
+
|
|
70
|
+
# Subdirectory names
|
|
71
|
+
_BIN_DIR: ClassVar[str] = "bin"
|
|
72
|
+
_CACHE_DIR: ClassVar[str] = "cache"
|
|
73
|
+
_CONFIG_DIR: ClassVar[str] = "config"
|
|
74
|
+
_LOGS_DIR: ClassVar[str] = "logs"
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def default(cls) -> "LucidscanPaths":
|
|
78
|
+
"""Create paths from the default lucidscan home (cwd/.lucidscan)."""
|
|
79
|
+
return cls(get_lucidscan_home())
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def for_project(cls, project_root: Path) -> "LucidscanPaths":
|
|
83
|
+
"""Create paths for a specific project.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
project_root: Project root directory.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
LucidscanPaths configured for the project.
|
|
90
|
+
"""
|
|
91
|
+
return cls(get_lucidscan_home(project_root))
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def bin_dir(self) -> Path:
|
|
95
|
+
"""Directory containing scanner plugin binaries."""
|
|
96
|
+
return self.home / self._BIN_DIR
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def cache_dir(self) -> Path:
|
|
100
|
+
"""Directory for scanner caches."""
|
|
101
|
+
return self.home / self._CACHE_DIR
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def config_dir(self) -> Path:
|
|
105
|
+
"""Directory for configuration files."""
|
|
106
|
+
return self.home / self._CONFIG_DIR
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def logs_dir(self) -> Path:
|
|
110
|
+
"""Directory for log files."""
|
|
111
|
+
return self.home / self._LOGS_DIR
|
|
112
|
+
|
|
113
|
+
def plugin_bin_dir(self, plugin_name: str, version: str) -> Path:
|
|
114
|
+
"""Get the binary directory for a specific plugin version.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
plugin_name: Name of the plugin (e.g., 'trivy', 'opengrep').
|
|
118
|
+
version: Version string.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Path to the plugin's version-specific binary directory.
|
|
122
|
+
"""
|
|
123
|
+
return self.bin_dir / plugin_name / version
|
|
124
|
+
|
|
125
|
+
def plugin_cache_dir(self, plugin_name: str) -> Path:
|
|
126
|
+
"""Get the cache directory for a specific plugin.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
plugin_name: Name of the plugin.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Path to the plugin's cache directory.
|
|
133
|
+
"""
|
|
134
|
+
return self.cache_dir / plugin_name
|
|
135
|
+
|
|
136
|
+
def ensure_directories(self) -> None:
|
|
137
|
+
"""Create all required directories if they don't exist."""
|
|
138
|
+
directories = [
|
|
139
|
+
self.home,
|
|
140
|
+
self.bin_dir,
|
|
141
|
+
self.cache_dir,
|
|
142
|
+
self.config_dir,
|
|
143
|
+
self.logs_dir,
|
|
144
|
+
]
|
|
145
|
+
for directory in directories:
|
|
146
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
def is_initialized(self) -> bool:
|
|
149
|
+
"""Check if any scanner plugins have been installed.
|
|
150
|
+
|
|
151
|
+
Returns True if the bin directory exists and contains at least
|
|
152
|
+
one plugin subdirectory.
|
|
153
|
+
"""
|
|
154
|
+
if not self.bin_dir.exists():
|
|
155
|
+
return False
|
|
156
|
+
# Check if any plugin directories exist
|
|
157
|
+
for plugin_dir in self.bin_dir.iterdir():
|
|
158
|
+
if plugin_dir.is_dir():
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Platform detection for lucidscan scanner plugins.
|
|
2
|
+
|
|
3
|
+
Detects OS and architecture to determine which scanner binaries to download.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
# Supported operating systems (lowercase)
|
|
13
|
+
SUPPORTED_OS = frozenset({"darwin", "linux", "windows"})
|
|
14
|
+
|
|
15
|
+
# Supported architectures (normalized)
|
|
16
|
+
SUPPORTED_ARCH = frozenset({"amd64", "arm64"})
|
|
17
|
+
|
|
18
|
+
# Architecture normalization map
|
|
19
|
+
_ARCH_MAP = {
|
|
20
|
+
"x86_64": "amd64",
|
|
21
|
+
"amd64": "amd64",
|
|
22
|
+
"arm64": "arm64",
|
|
23
|
+
"aarch64": "arm64",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_arch(machine: str) -> Optional[str]:
|
|
28
|
+
"""Normalize architecture string to standard form.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
machine: Raw architecture string from platform.machine()
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Normalized architecture string or None if unknown.
|
|
35
|
+
"""
|
|
36
|
+
return _ARCH_MAP.get(machine.lower())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detect_os() -> str:
|
|
40
|
+
"""Detect the current operating system.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Lowercase OS name (darwin, linux, windows).
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the OS is not supported.
|
|
47
|
+
"""
|
|
48
|
+
system = platform.system().lower()
|
|
49
|
+
if system not in SUPPORTED_OS:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Unsupported operating system: {platform.system()}. "
|
|
52
|
+
f"Supported: {', '.join(sorted(SUPPORTED_OS))}"
|
|
53
|
+
)
|
|
54
|
+
return system
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def detect_arch() -> str:
|
|
58
|
+
"""Detect the current CPU architecture.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Normalized architecture string (amd64 or arm64).
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If the architecture is not supported.
|
|
65
|
+
"""
|
|
66
|
+
machine = platform.machine()
|
|
67
|
+
normalized = normalize_arch(machine)
|
|
68
|
+
if normalized is None:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Unsupported architecture: {machine}. "
|
|
71
|
+
f"Supported: {', '.join(sorted(SUPPORTED_ARCH))}"
|
|
72
|
+
)
|
|
73
|
+
return normalized
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class PlatformInfo:
|
|
78
|
+
"""Information about the current platform.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
os: Operating system (darwin, linux, windows).
|
|
82
|
+
arch: CPU architecture (amd64, arm64).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
os: str
|
|
86
|
+
arch: str
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def bundle_name(self) -> str:
|
|
90
|
+
"""Return the bundle name suffix for this platform.
|
|
91
|
+
|
|
92
|
+
Example: "darwin-arm64", "linux-amd64"
|
|
93
|
+
"""
|
|
94
|
+
return f"{self.os}-{self.arch}"
|
|
95
|
+
|
|
96
|
+
def is_supported(self) -> bool:
|
|
97
|
+
"""Check if this platform is supported."""
|
|
98
|
+
return self.os in SUPPORTED_OS and self.arch in SUPPORTED_ARCH
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_platform_info() -> PlatformInfo:
|
|
102
|
+
"""Detect and return current platform information.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
PlatformInfo with detected OS and architecture.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If the platform is not supported.
|
|
109
|
+
"""
|
|
110
|
+
return PlatformInfo(os=detect_os(), arch=detect_arch())
|
|
111
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tool validation for lucidscan bootstrap.
|
|
2
|
+
|
|
3
|
+
Validates that scanner plugin tools are present and executable.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List
|
|
13
|
+
|
|
14
|
+
from lucidscan.core.logging import get_logger
|
|
15
|
+
|
|
16
|
+
LOGGER = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolStatus(str, Enum):
|
|
20
|
+
"""Status of a tool binary."""
|
|
21
|
+
|
|
22
|
+
PRESENT = "present"
|
|
23
|
+
MISSING = "missing"
|
|
24
|
+
NOT_EXECUTABLE = "not_executable"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PluginValidationResult:
|
|
29
|
+
"""Result of validating scanner plugin tools.
|
|
30
|
+
|
|
31
|
+
Stores validation status for each discovered plugin by name.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
statuses: Dict[str, ToolStatus] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
def all_valid(self) -> bool:
|
|
37
|
+
"""Check if all validated plugins are present and executable."""
|
|
38
|
+
if not self.statuses:
|
|
39
|
+
return True
|
|
40
|
+
return all(status == ToolStatus.PRESENT for status in self.statuses.values())
|
|
41
|
+
|
|
42
|
+
def missing_plugins(self) -> List[str]:
|
|
43
|
+
"""Return list of plugins that are missing or not executable."""
|
|
44
|
+
return [
|
|
45
|
+
name
|
|
46
|
+
for name, status in self.statuses.items()
|
|
47
|
+
if status != ToolStatus.PRESENT
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def get_status(self, plugin_name: str) -> ToolStatus:
|
|
51
|
+
"""Get status for a specific plugin."""
|
|
52
|
+
return self.statuses.get(plugin_name, ToolStatus.MISSING)
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> Dict[str, str]:
|
|
55
|
+
"""Convert to dictionary for JSON serialization."""
|
|
56
|
+
return {name: status.value for name, status in self.statuses.items()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_binary(path: Path) -> ToolStatus:
|
|
60
|
+
"""Validate a single binary file.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
path: Path to the binary file.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ToolStatus indicating whether the binary is present and executable.
|
|
67
|
+
"""
|
|
68
|
+
if not path.exists():
|
|
69
|
+
return ToolStatus.MISSING
|
|
70
|
+
|
|
71
|
+
# Check if executable
|
|
72
|
+
if not os.access(path, os.X_OK):
|
|
73
|
+
return ToolStatus.NOT_EXECUTABLE
|
|
74
|
+
|
|
75
|
+
return ToolStatus.PRESENT
|
|
76
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Centralized tool version management.
|
|
2
|
+
|
|
3
|
+
Reads tool versions from pyproject.toml [tool.lucidscan.tools] section.
|
|
4
|
+
This is the single source of truth for all lucidscan-managed tool versions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
# Import tomllib (Python 3.11+) or tomli (Python 3.10)
|
|
15
|
+
try:
|
|
16
|
+
if sys.version_info >= (3, 11):
|
|
17
|
+
import tomllib
|
|
18
|
+
|
|
19
|
+
_tomllib: Any = tomllib
|
|
20
|
+
else:
|
|
21
|
+
import tomli
|
|
22
|
+
|
|
23
|
+
_tomllib = tomli
|
|
24
|
+
except ImportError:
|
|
25
|
+
_tomllib = None # Will use fallback versions
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Hardcoded fallback versions (kept in sync with pyproject.toml)
|
|
29
|
+
# These are used if pyproject.toml cannot be read at runtime
|
|
30
|
+
_FALLBACK_VERSIONS: Dict[str, str] = {
|
|
31
|
+
# Security scanners
|
|
32
|
+
"trivy": "0.68.2",
|
|
33
|
+
"opengrep": "1.15.0",
|
|
34
|
+
"checkov": "3.2.497",
|
|
35
|
+
# Linters
|
|
36
|
+
"ruff": "0.14.11",
|
|
37
|
+
"biome": "2.3.11",
|
|
38
|
+
"checkstyle": "13.0.0",
|
|
39
|
+
# Type checkers
|
|
40
|
+
"pyright": "1.1.408",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@lru_cache(maxsize=1)
|
|
45
|
+
def _load_pyproject_versions() -> Dict[str, str]:
|
|
46
|
+
"""Load tool versions from lucidscan's pyproject.toml.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary mapping tool names to versions.
|
|
50
|
+
"""
|
|
51
|
+
if _tomllib is None:
|
|
52
|
+
return _FALLBACK_VERSIONS.copy()
|
|
53
|
+
|
|
54
|
+
# Find pyproject.toml relative to this module
|
|
55
|
+
# Structure: src/lucidscan/bootstrap/versions.py -> ../../../pyproject.toml
|
|
56
|
+
pyproject_path = Path(__file__).parent.parent.parent.parent / "pyproject.toml"
|
|
57
|
+
|
|
58
|
+
if not pyproject_path.exists():
|
|
59
|
+
# Installed package - pyproject.toml not available
|
|
60
|
+
return _FALLBACK_VERSIONS.copy()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with open(pyproject_path, "rb") as f:
|
|
64
|
+
data = _tomllib.load(f)
|
|
65
|
+
|
|
66
|
+
versions = {}
|
|
67
|
+
|
|
68
|
+
# Read from [tool.lucidscan.tools] section (new unified section)
|
|
69
|
+
tools_section = data.get("tool", {}).get("lucidscan", {}).get("tools", {})
|
|
70
|
+
versions.update(tools_section)
|
|
71
|
+
|
|
72
|
+
# Also read from legacy [tool.lucidscan.scanners] section for backwards compat
|
|
73
|
+
scanners_section = data.get("tool", {}).get("lucidscan", {}).get("scanners", {})
|
|
74
|
+
for tool, version in scanners_section.items():
|
|
75
|
+
if tool not in versions:
|
|
76
|
+
versions[tool] = version
|
|
77
|
+
|
|
78
|
+
# Fill in any missing tools from fallbacks
|
|
79
|
+
for tool, version in _FALLBACK_VERSIONS.items():
|
|
80
|
+
if tool not in versions:
|
|
81
|
+
versions[tool] = version
|
|
82
|
+
|
|
83
|
+
return versions
|
|
84
|
+
|
|
85
|
+
except Exception:
|
|
86
|
+
return _FALLBACK_VERSIONS.copy()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_tool_version(tool_name: str, default: Optional[str] = None) -> str:
|
|
90
|
+
"""Get the version for a specific tool.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
tool_name: Name of the tool (e.g., 'trivy', 'ruff').
|
|
94
|
+
default: Optional default version if tool not found.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Version string for the tool.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
KeyError: If tool not found and no default provided.
|
|
101
|
+
"""
|
|
102
|
+
versions = _load_pyproject_versions()
|
|
103
|
+
|
|
104
|
+
if tool_name in versions:
|
|
105
|
+
return versions[tool_name]
|
|
106
|
+
|
|
107
|
+
if default is not None:
|
|
108
|
+
return default
|
|
109
|
+
|
|
110
|
+
raise KeyError(f"Unknown tool: {tool_name}. Available: {list(versions.keys())}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_all_versions() -> Dict[str, str]:
|
|
114
|
+
"""Get all tool versions.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary mapping tool names to versions.
|
|
118
|
+
"""
|
|
119
|
+
return _load_pyproject_versions().copy()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""LucidScan CLI package.
|
|
2
|
+
|
|
3
|
+
This package provides the command-line interface for lucidscan.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Iterable, Optional
|
|
9
|
+
|
|
10
|
+
from lucidscan.cli.runner import CLIRunner, get_version
|
|
11
|
+
from lucidscan.cli.arguments import build_parser
|
|
12
|
+
from lucidscan.cli.exit_codes import (
|
|
13
|
+
EXIT_SUCCESS,
|
|
14
|
+
EXIT_ISSUES_FOUND,
|
|
15
|
+
EXIT_SCANNER_ERROR,
|
|
16
|
+
EXIT_INVALID_USAGE,
|
|
17
|
+
EXIT_BOOTSTRAP_FAILURE,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: Optional[Iterable[str]] = None) -> int:
|
|
22
|
+
"""CLI entrypoint.
|
|
23
|
+
|
|
24
|
+
Returns an exit code suitable for use as a console script.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
argv: Command-line arguments (defaults to sys.argv).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Exit code.
|
|
31
|
+
"""
|
|
32
|
+
runner = CLIRunner()
|
|
33
|
+
return runner.run(argv)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"main",
|
|
38
|
+
"build_parser",
|
|
39
|
+
"get_version",
|
|
40
|
+
"CLIRunner",
|
|
41
|
+
"EXIT_SUCCESS",
|
|
42
|
+
"EXIT_ISSUES_FOUND",
|
|
43
|
+
"EXIT_SCANNER_ERROR",
|
|
44
|
+
"EXIT_INVALID_USAGE",
|
|
45
|
+
"EXIT_BOOTSTRAP_FAILURE",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__": # pragma: no cover
|
|
50
|
+
raise SystemExit(main())
|