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,484 @@
|
|
|
1
|
+
"""Checkov scanner plugin for IaC (Infrastructure as Code) scanning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import venv
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from lucidscan.plugins.scanners.base import ScannerPlugin
|
|
14
|
+
from lucidscan.core.models import ScanContext, ScanDomain, Severity, UnifiedIssue
|
|
15
|
+
from lucidscan.bootstrap.paths import LucidscanPaths
|
|
16
|
+
from lucidscan.bootstrap.versions import get_tool_version
|
|
17
|
+
from lucidscan.core.logging import get_logger
|
|
18
|
+
from lucidscan.core.subprocess_runner import run_with_streaming
|
|
19
|
+
|
|
20
|
+
LOGGER = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _glob_to_regex(pattern: str) -> str:
|
|
24
|
+
"""Convert a gitignore-style glob pattern to a regex pattern.
|
|
25
|
+
|
|
26
|
+
Checkov's Bicep runner (and possibly others) treats --skip-path
|
|
27
|
+
values as regex patterns, so we need to convert glob patterns.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
pattern: Gitignore-style glob pattern (e.g., ".venv/**", "*.tf").
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Equivalent regex pattern.
|
|
34
|
+
"""
|
|
35
|
+
# Escape regex special characters except * and ?
|
|
36
|
+
# These are the special chars in regex that need escaping
|
|
37
|
+
result = ""
|
|
38
|
+
i = 0
|
|
39
|
+
while i < len(pattern):
|
|
40
|
+
c = pattern[i]
|
|
41
|
+
|
|
42
|
+
if c == "*":
|
|
43
|
+
# Check for ** (match anything including path separators)
|
|
44
|
+
if i + 1 < len(pattern) and pattern[i + 1] == "*":
|
|
45
|
+
result += ".*"
|
|
46
|
+
i += 2
|
|
47
|
+
continue
|
|
48
|
+
else:
|
|
49
|
+
# Single * matches anything except path separator
|
|
50
|
+
result += "[^/]*"
|
|
51
|
+
elif c == "?":
|
|
52
|
+
# ? matches any single character except path separator
|
|
53
|
+
result += "[^/]"
|
|
54
|
+
elif c in r"\.^$+{}[]|()":
|
|
55
|
+
# Escape regex special characters
|
|
56
|
+
result += "\\" + c
|
|
57
|
+
else:
|
|
58
|
+
result += c
|
|
59
|
+
|
|
60
|
+
i += 1
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
# Default version from pyproject.toml [tool.lucidscan.tools]
|
|
65
|
+
DEFAULT_VERSION = get_tool_version("checkov")
|
|
66
|
+
|
|
67
|
+
# Checkov severity mapping to unified severity
|
|
68
|
+
CHECKOV_SEVERITY_MAP: Dict[str, Severity] = {
|
|
69
|
+
"CRITICAL": Severity.CRITICAL,
|
|
70
|
+
"HIGH": Severity.HIGH,
|
|
71
|
+
"MEDIUM": Severity.MEDIUM,
|
|
72
|
+
"LOW": Severity.LOW,
|
|
73
|
+
"INFO": Severity.INFO,
|
|
74
|
+
"UNKNOWN": Severity.INFO,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CheckovScanner(ScannerPlugin):
|
|
79
|
+
"""Scanner plugin for Checkov (IaC scanning).
|
|
80
|
+
|
|
81
|
+
Handles:
|
|
82
|
+
- Infrastructure-as-Code scanning for Terraform, Kubernetes,
|
|
83
|
+
CloudFormation, ARM templates, and more via `checkov`
|
|
84
|
+
|
|
85
|
+
Binary management:
|
|
86
|
+
- Installs via pip into a virtual environment
|
|
87
|
+
- Caches at {project}/.lucidscan/bin/checkov/{version}/venv/
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
version: str = DEFAULT_VERSION,
|
|
93
|
+
project_root: Optional[Path] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._version = version
|
|
96
|
+
if project_root:
|
|
97
|
+
self._paths = LucidscanPaths.for_project(project_root)
|
|
98
|
+
else:
|
|
99
|
+
self._paths = LucidscanPaths.default()
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def name(self) -> str:
|
|
103
|
+
return "checkov"
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def domains(self) -> List[ScanDomain]:
|
|
107
|
+
return [ScanDomain.IAC]
|
|
108
|
+
|
|
109
|
+
def get_version(self) -> str:
|
|
110
|
+
return self._version
|
|
111
|
+
|
|
112
|
+
def ensure_binary(self) -> Path:
|
|
113
|
+
"""Ensure the Checkov binary is available, installing if needed.
|
|
114
|
+
|
|
115
|
+
Checkov is a Python package, so we install it into a dedicated
|
|
116
|
+
virtual environment to avoid conflicts with system packages.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Path to the checkov binary in the virtual environment.
|
|
120
|
+
"""
|
|
121
|
+
venv_dir = self._paths.plugin_bin_dir(self.name, self._version) / "venv"
|
|
122
|
+
binary_path = self._get_binary_path(venv_dir)
|
|
123
|
+
|
|
124
|
+
if binary_path.exists():
|
|
125
|
+
LOGGER.debug(f"Checkov binary found at {binary_path}")
|
|
126
|
+
return binary_path
|
|
127
|
+
|
|
128
|
+
LOGGER.info(f"Installing Checkov v{self._version}...")
|
|
129
|
+
self._install_checkov(venv_dir)
|
|
130
|
+
|
|
131
|
+
if not binary_path.exists():
|
|
132
|
+
raise RuntimeError(f"Failed to install Checkov to {binary_path}")
|
|
133
|
+
|
|
134
|
+
return binary_path
|
|
135
|
+
|
|
136
|
+
def _get_binary_path(self, venv_dir: Path) -> Path:
|
|
137
|
+
"""Get the path to the checkov binary in the virtual environment."""
|
|
138
|
+
# On Windows, binaries are in Scripts/, on Unix in bin/
|
|
139
|
+
if sys.platform == "win32":
|
|
140
|
+
return venv_dir / "Scripts" / "checkov.exe"
|
|
141
|
+
return venv_dir / "bin" / "checkov"
|
|
142
|
+
|
|
143
|
+
def _get_pip_path(self, venv_dir: Path) -> Path:
|
|
144
|
+
"""Get the path to pip in the virtual environment."""
|
|
145
|
+
if sys.platform == "win32":
|
|
146
|
+
return venv_dir / "Scripts" / "pip.exe"
|
|
147
|
+
return venv_dir / "bin" / "pip"
|
|
148
|
+
|
|
149
|
+
def _install_checkov(self, venv_dir: Path) -> None:
|
|
150
|
+
"""Install Checkov into a virtual environment.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
venv_dir: Path to the virtual environment directory.
|
|
154
|
+
"""
|
|
155
|
+
# Create parent directories
|
|
156
|
+
venv_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
|
|
158
|
+
# Create virtual environment
|
|
159
|
+
LOGGER.debug(f"Creating virtual environment at {venv_dir}")
|
|
160
|
+
venv.create(venv_dir, with_pip=True)
|
|
161
|
+
|
|
162
|
+
# Install checkov
|
|
163
|
+
pip_path = self._get_pip_path(venv_dir)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Upgrade pip first to avoid issues (best effort, don't fail if it doesn't work)
|
|
167
|
+
# On Windows, pip upgrade can fail with exit code 1 due to file locking
|
|
168
|
+
# when trying to upgrade itself while running
|
|
169
|
+
subprocess.run(
|
|
170
|
+
[str(pip_path), "install", "--upgrade", "pip"],
|
|
171
|
+
capture_output=True,
|
|
172
|
+
check=False, # Don't fail if pip upgrade fails
|
|
173
|
+
timeout=120, # 2 minute timeout for pip upgrade
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Install specific version of checkov
|
|
177
|
+
package_spec = f"checkov=={self._version}"
|
|
178
|
+
LOGGER.debug(f"Installing {package_spec}")
|
|
179
|
+
|
|
180
|
+
# Use UTF-8 encoding with error replacement to handle Windows cp1252 issues
|
|
181
|
+
result = subprocess.run(
|
|
182
|
+
[str(pip_path), "install", package_spec],
|
|
183
|
+
capture_output=True,
|
|
184
|
+
text=True,
|
|
185
|
+
encoding="utf-8",
|
|
186
|
+
errors="replace",
|
|
187
|
+
check=False,
|
|
188
|
+
timeout=300, # 5 minute timeout for checkov install
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if result.returncode != 0:
|
|
192
|
+
LOGGER.error(f"pip install failed: {result.stderr}")
|
|
193
|
+
raise RuntimeError(f"Failed to install checkov: {result.stderr}")
|
|
194
|
+
|
|
195
|
+
LOGGER.info(f"Checkov v{self._version} installed to {venv_dir}")
|
|
196
|
+
|
|
197
|
+
except subprocess.CalledProcessError as e:
|
|
198
|
+
# Clean up failed installation
|
|
199
|
+
if venv_dir.exists():
|
|
200
|
+
import shutil
|
|
201
|
+
shutil.rmtree(venv_dir, ignore_errors=True)
|
|
202
|
+
raise RuntimeError(f"Failed to install Checkov: {e}") from e
|
|
203
|
+
|
|
204
|
+
def scan(self, context: ScanContext) -> List[UnifiedIssue]:
|
|
205
|
+
"""Execute Checkov scan and return normalized issues.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
context: Scan context containing target paths and configuration.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List of unified issues found during the scan.
|
|
212
|
+
"""
|
|
213
|
+
if ScanDomain.IAC not in context.enabled_domains:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
binary = self.ensure_binary()
|
|
217
|
+
return self._run_iac_scan(binary, context)
|
|
218
|
+
|
|
219
|
+
def _run_iac_scan(
|
|
220
|
+
self, binary: Path, context: ScanContext
|
|
221
|
+
) -> List[UnifiedIssue]:
|
|
222
|
+
"""Run Checkov IaC scan.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
binary: Path to the Checkov binary.
|
|
226
|
+
context: Scan context with project root and configuration.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of unified issues from the IaC scan.
|
|
230
|
+
"""
|
|
231
|
+
# Get IaC-specific config options
|
|
232
|
+
iac_config = context.get_scanner_options("iac")
|
|
233
|
+
|
|
234
|
+
# Build command
|
|
235
|
+
cmd = [
|
|
236
|
+
str(binary),
|
|
237
|
+
"--directory", str(context.project_root),
|
|
238
|
+
"--output", "json",
|
|
239
|
+
"--quiet",
|
|
240
|
+
"--compact",
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
# Add framework filter if specified in config
|
|
244
|
+
frameworks = iac_config.get("framework", [])
|
|
245
|
+
if frameworks:
|
|
246
|
+
for framework in frameworks:
|
|
247
|
+
cmd.extend(["--framework", framework])
|
|
248
|
+
|
|
249
|
+
# Add skip checks if specified
|
|
250
|
+
skip_checks = iac_config.get("skip_checks", [])
|
|
251
|
+
if skip_checks:
|
|
252
|
+
cmd.extend(["--skip-check", ",".join(skip_checks)])
|
|
253
|
+
|
|
254
|
+
# Apply ignore patterns from .lucidscanignore and config
|
|
255
|
+
# Convert glob patterns to regex since Checkov's Bicep runner
|
|
256
|
+
# (and possibly others) treats --skip-path as regex
|
|
257
|
+
exclude_patterns = context.get_exclude_patterns()
|
|
258
|
+
for pattern in exclude_patterns:
|
|
259
|
+
regex_pattern = _glob_to_regex(pattern)
|
|
260
|
+
cmd.extend(["--skip-path", regex_pattern])
|
|
261
|
+
|
|
262
|
+
LOGGER.debug(f"Running: {' '.join(cmd)}")
|
|
263
|
+
|
|
264
|
+
# Checkov doesn't support custom env in run_with_streaming, so set env vars first
|
|
265
|
+
import os
|
|
266
|
+
env = self._get_scan_env()
|
|
267
|
+
old_env: Dict[str, Optional[str]] = {}
|
|
268
|
+
for key, value in env.items():
|
|
269
|
+
if key not in os.environ or os.environ[key] != value:
|
|
270
|
+
old_env[key] = os.environ.get(key)
|
|
271
|
+
os.environ[key] = value
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
result = run_with_streaming(
|
|
275
|
+
cmd=cmd,
|
|
276
|
+
cwd=context.project_root,
|
|
277
|
+
tool_name="checkov",
|
|
278
|
+
stream_handler=context.stream_handler,
|
|
279
|
+
timeout=180,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Checkov returns non-zero exit code when findings exist
|
|
283
|
+
# Exit code 1 means findings found (expected)
|
|
284
|
+
# Exit code 2 means error
|
|
285
|
+
if result.returncode == 2 and result.stderr:
|
|
286
|
+
LOGGER.warning(f"Checkov stderr: {result.stderr}")
|
|
287
|
+
|
|
288
|
+
if not result.stdout.strip():
|
|
289
|
+
LOGGER.debug("Checkov returned empty output")
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
return self._parse_checkov_json(result.stdout, context.project_root)
|
|
293
|
+
|
|
294
|
+
except subprocess.TimeoutExpired:
|
|
295
|
+
LOGGER.warning("Checkov scan timed out after 180 seconds")
|
|
296
|
+
return []
|
|
297
|
+
except Exception as e:
|
|
298
|
+
LOGGER.error(f"Checkov scan failed: {e}")
|
|
299
|
+
return []
|
|
300
|
+
finally:
|
|
301
|
+
# Restore original environment
|
|
302
|
+
for key, value in old_env.items(): # type: ignore[assignment]
|
|
303
|
+
if value is None:
|
|
304
|
+
os.environ.pop(key, None)
|
|
305
|
+
else:
|
|
306
|
+
os.environ[key] = value
|
|
307
|
+
|
|
308
|
+
def _get_scan_env(self) -> Dict[str, str]:
|
|
309
|
+
"""Get environment variables for the scan process."""
|
|
310
|
+
import os
|
|
311
|
+
env = os.environ.copy()
|
|
312
|
+
# Disable telemetry/analytics
|
|
313
|
+
env["BC_SKIP_MAPPING"] = "TRUE"
|
|
314
|
+
env["CHECKOV_RUN_SCA_PACKAGE_SCAN"] = "false"
|
|
315
|
+
return env
|
|
316
|
+
|
|
317
|
+
def _parse_checkov_json(
|
|
318
|
+
self,
|
|
319
|
+
json_output: str,
|
|
320
|
+
project_root: Path,
|
|
321
|
+
) -> List[UnifiedIssue]:
|
|
322
|
+
"""Parse Checkov JSON output and convert to UnifiedIssue list.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
json_output: Raw JSON string from Checkov.
|
|
326
|
+
project_root: Project root path for relative path resolution.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of unified issues parsed from the JSON.
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
data = json.loads(json_output)
|
|
333
|
+
except json.JSONDecodeError as e:
|
|
334
|
+
LOGGER.error(f"Failed to parse Checkov JSON: {e}")
|
|
335
|
+
return []
|
|
336
|
+
|
|
337
|
+
issues: List[UnifiedIssue] = []
|
|
338
|
+
|
|
339
|
+
# Checkov can output a list of results (one per framework) or a single result
|
|
340
|
+
if isinstance(data, list):
|
|
341
|
+
results_list = data
|
|
342
|
+
else:
|
|
343
|
+
results_list = [data]
|
|
344
|
+
|
|
345
|
+
for framework_result in results_list:
|
|
346
|
+
# Skip if not a dict (could be error message)
|
|
347
|
+
if not isinstance(framework_result, dict):
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Get the check type (framework)
|
|
351
|
+
check_type = framework_result.get("check_type", "unknown")
|
|
352
|
+
|
|
353
|
+
# Process failed checks
|
|
354
|
+
results = framework_result.get("results", {})
|
|
355
|
+
failed_checks = results.get("failed_checks", [])
|
|
356
|
+
|
|
357
|
+
for check in failed_checks:
|
|
358
|
+
issue = self._check_to_unified_issue(check, check_type, project_root)
|
|
359
|
+
if issue:
|
|
360
|
+
issues.append(issue)
|
|
361
|
+
|
|
362
|
+
LOGGER.debug(f"Parsed {len(issues)} issues from Checkov output")
|
|
363
|
+
return issues
|
|
364
|
+
|
|
365
|
+
def _check_to_unified_issue(
|
|
366
|
+
self,
|
|
367
|
+
check: Dict[str, Any],
|
|
368
|
+
check_type: str,
|
|
369
|
+
project_root: Path,
|
|
370
|
+
) -> Optional[UnifiedIssue]:
|
|
371
|
+
"""Convert a single Checkov failed check to a UnifiedIssue.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
check: Failed check dict from Checkov JSON.
|
|
375
|
+
check_type: The framework/check type (e.g., 'terraform', 'kubernetes').
|
|
376
|
+
project_root: Project root path for relative path resolution.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
UnifiedIssue or None if conversion fails.
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
# Extract basic fields
|
|
383
|
+
check_id = check.get("check_id", "UNKNOWN")
|
|
384
|
+
check_name = check.get("check", "Unknown check")
|
|
385
|
+
file_path_str = check.get("file_path", "")
|
|
386
|
+
resource = check.get("resource", "")
|
|
387
|
+
guideline = check.get("guideline", "")
|
|
388
|
+
|
|
389
|
+
# Extract line numbers
|
|
390
|
+
file_line_range = check.get("file_line_range", [])
|
|
391
|
+
line_start = file_line_range[0] if len(file_line_range) > 0 else None
|
|
392
|
+
line_end = file_line_range[1] if len(file_line_range) > 1 else line_start
|
|
393
|
+
|
|
394
|
+
# Extract severity (Checkov includes severity in some checks)
|
|
395
|
+
severity_str = check.get("severity", "MEDIUM")
|
|
396
|
+
if severity_str is None:
|
|
397
|
+
severity_str = "MEDIUM"
|
|
398
|
+
severity = CHECKOV_SEVERITY_MAP.get(severity_str.upper(), Severity.MEDIUM)
|
|
399
|
+
|
|
400
|
+
# Generate deterministic issue ID
|
|
401
|
+
issue_id = self._generate_issue_id(
|
|
402
|
+
check_id, file_path_str, resource, line_start
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Build file path
|
|
406
|
+
file_path = None
|
|
407
|
+
if file_path_str:
|
|
408
|
+
# Remove leading slash if present (Checkov sometimes includes it)
|
|
409
|
+
clean_path = file_path_str.lstrip("/")
|
|
410
|
+
file_path = project_root / clean_path
|
|
411
|
+
|
|
412
|
+
# Build title
|
|
413
|
+
title = f"{check_id}: {check_name}"
|
|
414
|
+
|
|
415
|
+
# Build description
|
|
416
|
+
description = check_name
|
|
417
|
+
if resource:
|
|
418
|
+
description += f"\n\nResource: {resource}"
|
|
419
|
+
|
|
420
|
+
# Build recommendation
|
|
421
|
+
recommendation = None
|
|
422
|
+
if guideline:
|
|
423
|
+
recommendation = f"See: {guideline}"
|
|
424
|
+
|
|
425
|
+
# Build IaC resource string
|
|
426
|
+
iac_resource = resource if resource else None
|
|
427
|
+
|
|
428
|
+
# Build scanner metadata
|
|
429
|
+
scanner_metadata: Dict[str, Any] = {
|
|
430
|
+
"check_id": check_id,
|
|
431
|
+
"check_type": check_type,
|
|
432
|
+
"resource": resource,
|
|
433
|
+
"resource_address": check.get("resource_address"),
|
|
434
|
+
"guideline": guideline,
|
|
435
|
+
"severity_raw": severity_str,
|
|
436
|
+
"bc_check_id": check.get("bc_check_id"),
|
|
437
|
+
"evaluations": check.get("evaluations"),
|
|
438
|
+
"check_class": check.get("check_class"),
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return UnifiedIssue(
|
|
442
|
+
id=issue_id,
|
|
443
|
+
domain=ScanDomain.IAC,
|
|
444
|
+
source_tool="checkov",
|
|
445
|
+
severity=severity,
|
|
446
|
+
rule_id=check_id,
|
|
447
|
+
title=title,
|
|
448
|
+
description=description,
|
|
449
|
+
documentation_url=guideline,
|
|
450
|
+
file_path=file_path,
|
|
451
|
+
line_start=line_start,
|
|
452
|
+
line_end=line_end,
|
|
453
|
+
iac_resource=iac_resource,
|
|
454
|
+
recommendation=recommendation,
|
|
455
|
+
fixable=False, # Checkov doesn't auto-fix
|
|
456
|
+
metadata=scanner_metadata,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
LOGGER.warning(f"Failed to convert Checkov check to UnifiedIssue: {e}")
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
def _generate_issue_id(
|
|
464
|
+
self,
|
|
465
|
+
check_id: str,
|
|
466
|
+
file_path: str,
|
|
467
|
+
resource: str,
|
|
468
|
+
line: Optional[int],
|
|
469
|
+
) -> str:
|
|
470
|
+
"""Generate a deterministic issue ID for deduplication.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
check_id: Checkov check identifier (e.g., CKV_AWS_123).
|
|
474
|
+
file_path: File path.
|
|
475
|
+
resource: Resource identifier.
|
|
476
|
+
line: Line number (optional).
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
A stable hash-based ID string.
|
|
480
|
+
"""
|
|
481
|
+
line_str = str(line) if line is not None else ""
|
|
482
|
+
components = f"checkov:{check_id}:{file_path}:{resource}:{line_str}"
|
|
483
|
+
hash_digest = hashlib.sha256(components.encode()).hexdigest()[:16]
|
|
484
|
+
return f"checkov-{hash_digest}"
|