code-audit-23 0.1.3__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.
- code_audit_23/__init__.py +0 -0
- code_audit_23/logger.py +62 -0
- code_audit_23/main.py +286 -0
- code_audit_23/sonar-scanner/bin/sonar-scanner +74 -0
- code_audit_23/sonar-scanner/bin/sonar-scanner-debug +19 -0
- code_audit_23/sonar-scanner/bin/sonar-scanner-debug.bat +18 -0
- code_audit_23/sonar-scanner/bin/sonar-scanner.bat +92 -0
- code_audit_23/sonar-scanner/conf/sonar-scanner.properties +8 -0
- code_audit_23/sonar-scanner/lib/sonar-scanner-cli-7.3.0.5189.jar +0 -0
- code_audit_23/sonarqube_cli.py +292 -0
- code_audit_23/trivy_cli.py +305 -0
- code_audit_23-0.1.3.dist-info/METADATA +184 -0
- code_audit_23-0.1.3.dist-info/RECORD +17 -0
- code_audit_23-0.1.3.dist-info/WHEEL +5 -0
- code_audit_23-0.1.3.dist-info/entry_points.txt +2 -0
- code_audit_23-0.1.3.dist-info/licenses/LICENSE +21 -0
- code_audit_23-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import stat
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tarfile
|
|
9
|
+
import time
|
|
10
|
+
import urllib.request
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from .logger import logger
|
|
18
|
+
except ImportError:
|
|
19
|
+
from logger import logger
|
|
20
|
+
|
|
21
|
+
# Cache folder for downloaded JRE
|
|
22
|
+
CACHE_DIR = Path.home() / ".audit_scan"
|
|
23
|
+
JRE_DIR = CACHE_DIR / "jre"
|
|
24
|
+
JRE_DIR.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_java():
|
|
28
|
+
"""Check system java or JAVA_HOME"""
|
|
29
|
+
logger.debug("Looking for Java installation")
|
|
30
|
+
java_path = shutil.which("java")
|
|
31
|
+
if java_path:
|
|
32
|
+
logger.debug(f"Found Java at: {java_path}")
|
|
33
|
+
return java_path
|
|
34
|
+
|
|
35
|
+
java_home = os.environ.get("JAVA_HOME")
|
|
36
|
+
if java_home:
|
|
37
|
+
java_bin = Path(java_home) / "bin" / ("java.exe" if os.name == "nt" else "java")
|
|
38
|
+
if java_bin.exists():
|
|
39
|
+
logger.debug(f"Found Java in JAVA_HOME: {java_bin}")
|
|
40
|
+
return str(java_bin)
|
|
41
|
+
|
|
42
|
+
logger.warning("Java not found in PATH or JAVA_HOME")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def download_jre():
|
|
47
|
+
"""Download minimal JRE into cache folder"""
|
|
48
|
+
system = platform.system().lower()
|
|
49
|
+
dest = None
|
|
50
|
+
|
|
51
|
+
# Get the machine architecture (e.g., 'x86_64', 'arm64')
|
|
52
|
+
arch = platform.machine()
|
|
53
|
+
os = (
|
|
54
|
+
"macos"
|
|
55
|
+
if system == "darwin"
|
|
56
|
+
else ("windows" if system == "windows" else "linux")
|
|
57
|
+
)
|
|
58
|
+
ext = "zip" if system != "linux" else "tar.gz"
|
|
59
|
+
zulu_url = f"https://api.azul.com/zulu/download/community/v1.0/bundles/latest?os={os}&arch={arch}&ext={ext}&bundle_type=jre&java_version=17"
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(zulu_url) as r:
|
|
62
|
+
data = json.load(r)
|
|
63
|
+
url = data["url"]
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
error_msg = f"Failed to fetch JRE metadata: {exc}"
|
|
66
|
+
logger.exception(error_msg)
|
|
67
|
+
raise RuntimeError(error_msg) from exc
|
|
68
|
+
|
|
69
|
+
if "windows" in system:
|
|
70
|
+
dest = CACHE_DIR / "jre.zip"
|
|
71
|
+
elif "darwin" in system:
|
|
72
|
+
dest = CACHE_DIR / "jre.zip"
|
|
73
|
+
else: # linux
|
|
74
|
+
dest = CACHE_DIR / "jre.tar.gz"
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
print(f"🌐 Downloading JRE from {url} ...")
|
|
78
|
+
urllib.request.urlretrieve(url, dest)
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
error_msg = f"Failed to download JRE archive: {exc}"
|
|
81
|
+
logger.exception(error_msg)
|
|
82
|
+
raise RuntimeError(error_msg) from exc
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Extract
|
|
86
|
+
if dest.suffix == ".zip":
|
|
87
|
+
with zipfile.ZipFile(dest, "r") as zip_ref:
|
|
88
|
+
zip_ref.extractall(JRE_DIR)
|
|
89
|
+
else:
|
|
90
|
+
with tarfile.open(dest, "r:gz") as tar_ref:
|
|
91
|
+
tar_ref.extractall(JRE_DIR)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
error_msg = f"Failed to extract JRE archive: {exc}"
|
|
94
|
+
logger.exception(error_msg)
|
|
95
|
+
raise RuntimeError(error_msg) from exc
|
|
96
|
+
finally:
|
|
97
|
+
if dest.exists():
|
|
98
|
+
try:
|
|
99
|
+
dest.unlink()
|
|
100
|
+
except Exception as unlink_exc:
|
|
101
|
+
logger.warning(
|
|
102
|
+
f"Could not remove temporary JRE archive {dest}: {unlink_exc}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Ensure the extracted java binaries are executable (particularly for zip archives)
|
|
106
|
+
try:
|
|
107
|
+
for jre_root in sorted([d for d in JRE_DIR.iterdir() if d.is_dir()]):
|
|
108
|
+
bin_dir = jre_root / "bin"
|
|
109
|
+
if bin_dir.exists():
|
|
110
|
+
for binary in bin_dir.iterdir():
|
|
111
|
+
if binary.is_file():
|
|
112
|
+
current_mode = binary.stat().st_mode
|
|
113
|
+
binary.chmod(
|
|
114
|
+
current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
115
|
+
)
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
error_msg = f"Failed to set executable permissions on JRE binaries: {exc}"
|
|
118
|
+
logger.exception(error_msg)
|
|
119
|
+
raise RuntimeError(error_msg) from exc
|
|
120
|
+
|
|
121
|
+
print(f"✅ JRE installed to {JRE_DIR}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_jre_bin():
|
|
125
|
+
"""Return path to java binary"""
|
|
126
|
+
java_bin = find_java()
|
|
127
|
+
if java_bin:
|
|
128
|
+
return java_bin
|
|
129
|
+
|
|
130
|
+
# Download and find inside extracted folder
|
|
131
|
+
java_filename = "java.exe" if os.name == "nt" else "java"
|
|
132
|
+
subdirs = [
|
|
133
|
+
d
|
|
134
|
+
for d in JRE_DIR.iterdir()
|
|
135
|
+
if d.is_dir() and (d / "bin" / java_filename).exists()
|
|
136
|
+
]
|
|
137
|
+
if not subdirs:
|
|
138
|
+
download_jre()
|
|
139
|
+
subdirs = [
|
|
140
|
+
d
|
|
141
|
+
for d in JRE_DIR.iterdir()
|
|
142
|
+
if d.is_dir() and (d / "bin" / java_filename).exists()
|
|
143
|
+
]
|
|
144
|
+
if not subdirs:
|
|
145
|
+
raise RuntimeError("JRE download failed or empty.")
|
|
146
|
+
java_bin = subdirs[0] / "bin" / java_filename
|
|
147
|
+
if not java_bin.exists():
|
|
148
|
+
raise RuntimeError(f"Java binary not found in {java_bin}")
|
|
149
|
+
return str(java_bin)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_scanner_path():
|
|
153
|
+
"""Return path to sonar-scanner bundled folder"""
|
|
154
|
+
if getattr(sys, "frozen", False):
|
|
155
|
+
base_path = Path(sys._MEIPASS)
|
|
156
|
+
else:
|
|
157
|
+
base_path = Path(__file__).parent
|
|
158
|
+
|
|
159
|
+
# Assume a 'sonar-scanner' folder is next to CLI
|
|
160
|
+
scanner_bin = (
|
|
161
|
+
base_path
|
|
162
|
+
/ "sonar-scanner"
|
|
163
|
+
/ "bin"
|
|
164
|
+
/ ("sonar-scanner.bat" if os.name == "nt" else "sonar-scanner")
|
|
165
|
+
)
|
|
166
|
+
if not scanner_bin.exists():
|
|
167
|
+
raise FileNotFoundError(f"SonarScanner binary not found: {scanner_bin}")
|
|
168
|
+
return scanner_bin
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_sonarqube_scan(sonar_url, token, project_key, sources):
|
|
172
|
+
project_root = Path(sources).resolve()
|
|
173
|
+
if not project_root.exists():
|
|
174
|
+
click.echo("❌ Source directory not found.")
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
project_name = project_root.name
|
|
178
|
+
project_key = project_key or project_name
|
|
179
|
+
click.echo(f"🔍 Starting SonarQube scan for project: {project_key}")
|
|
180
|
+
|
|
181
|
+
# Create temporary sonar-project.properties if not exists
|
|
182
|
+
temp_props = None
|
|
183
|
+
props_file = project_root / "sonar-project.properties"
|
|
184
|
+
if not props_file.exists():
|
|
185
|
+
temp_props = props_file
|
|
186
|
+
temp_props.write_text(
|
|
187
|
+
f"""
|
|
188
|
+
sonar.projectKey={project_key}
|
|
189
|
+
sonar.projectName={project_name}
|
|
190
|
+
sonar.sources={sources}
|
|
191
|
+
""".strip()
|
|
192
|
+
)
|
|
193
|
+
click.echo("📝 Created temporary sonar-project.properties")
|
|
194
|
+
|
|
195
|
+
# Get scanner and Java paths
|
|
196
|
+
scanner_bin = get_scanner_path()
|
|
197
|
+
java_bin = get_jre_bin()
|
|
198
|
+
java_home = str(Path(java_bin).parent.parent)
|
|
199
|
+
|
|
200
|
+
env = os.environ.copy()
|
|
201
|
+
env["JAVA_HOME"] = java_home
|
|
202
|
+
env["PATH"] = f"{Path(java_bin).parent}:{env.get('PATH', '')}"
|
|
203
|
+
|
|
204
|
+
# # Ensure URL and token are properly formatted
|
|
205
|
+
# sonar_url = sonar_url or SONAR_HOST_URL
|
|
206
|
+
# token = token or SONAR_LOGIN
|
|
207
|
+
|
|
208
|
+
# if not sonar_url or sonar_url == SONAR_HOST_URL:
|
|
209
|
+
# click.echo(f"⚠️ Using default SonarQube URL: {SONAR_HOST_URL}")
|
|
210
|
+
if not token:
|
|
211
|
+
error_msg = "No SonarQube token provided. Please set SONAR_LOGIN in your .env file or use --token"
|
|
212
|
+
logger.error(error_msg)
|
|
213
|
+
click.echo(f"❌ {error_msg}")
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
|
|
216
|
+
env["SONAR_HOST_URL"] = sonar_url.rstrip("/")
|
|
217
|
+
env["SONAR_TOKEN"] = token.strip()
|
|
218
|
+
logger.debug("SonarQube configuration verified")
|
|
219
|
+
|
|
220
|
+
# Prepare SARIF report paths
|
|
221
|
+
reports_dir = project_root / "reports"
|
|
222
|
+
|
|
223
|
+
# Check which report files exist
|
|
224
|
+
report_files = [
|
|
225
|
+
"gitleaks.sarif",
|
|
226
|
+
"trivy.sarif",
|
|
227
|
+
"bandit.sarif",
|
|
228
|
+
"eslint.sarif",
|
|
229
|
+
"checkov.sarif",
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
# Find existing report files
|
|
233
|
+
existing_reports = [f for f in report_files if (reports_dir / f).exists()]
|
|
234
|
+
|
|
235
|
+
# Build sonar-scanner command
|
|
236
|
+
scanner_cmd = [str(scanner_bin), "-Dsonar.verbose=false"]
|
|
237
|
+
# scanner_cmd = [str(scanner_bin)]
|
|
238
|
+
|
|
239
|
+
# Add SARIF reports if any exist
|
|
240
|
+
if existing_reports:
|
|
241
|
+
sarif_paths = [f"reports/{report}" for report in existing_reports]
|
|
242
|
+
sarif_arg = "-Dsonar.sarifReportPaths=" + ",".join(sarif_paths)
|
|
243
|
+
scanner_cmd.append(sarif_arg)
|
|
244
|
+
click.echo(f"📊 Including SARIF reports: {', '.join(existing_reports)}")
|
|
245
|
+
|
|
246
|
+
click.echo("🚀 Starting SonarScanner...")
|
|
247
|
+
try:
|
|
248
|
+
# Start the subprocess and stream logs in real-time
|
|
249
|
+
process = subprocess.Popen(
|
|
250
|
+
scanner_cmd,
|
|
251
|
+
cwd=project_root,
|
|
252
|
+
env=env,
|
|
253
|
+
stdout=subprocess.PIPE,
|
|
254
|
+
stderr=subprocess.STDOUT,
|
|
255
|
+
text=True,
|
|
256
|
+
bufsize=1,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
click.echo("🔍 Scanning code (streaming Sonar logs)...")
|
|
260
|
+
streamed_output = []
|
|
261
|
+
assert process.stdout is not None
|
|
262
|
+
for line in process.stdout:
|
|
263
|
+
streamed_output.append(line)
|
|
264
|
+
click.echo(line.rstrip())
|
|
265
|
+
|
|
266
|
+
process.wait()
|
|
267
|
+
|
|
268
|
+
# Check the return code
|
|
269
|
+
if process.returncode != 0:
|
|
270
|
+
raise subprocess.CalledProcessError(
|
|
271
|
+
process.returncode, process.args, output="".join(streamed_output)
|
|
272
|
+
)
|
|
273
|
+
click.echo("✅ Sonar scan completed successfully!")
|
|
274
|
+
click.echo(f"👉 View results: {sonar_url}/dashboard?id={project_key}")
|
|
275
|
+
|
|
276
|
+
except subprocess.CalledProcessError as e:
|
|
277
|
+
error_msg = f"Sonar scan failed with exit code {e.returncode}"
|
|
278
|
+
logger.error(error_msg)
|
|
279
|
+
click.echo("❌ Sonar scan failed!")
|
|
280
|
+
click.echo(f"Exit code: {e.returncode}")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
error_msg = f"Unexpected error during Sonar scan: {str(e)}"
|
|
285
|
+
logger.exception(error_msg)
|
|
286
|
+
click.echo(f"❌ {error_msg}")
|
|
287
|
+
sys.exit(1)
|
|
288
|
+
|
|
289
|
+
finally:
|
|
290
|
+
if temp_props and temp_props.exists():
|
|
291
|
+
temp_props.unlink()
|
|
292
|
+
click.echo("🧹 Cleaned up temporary sonar-project.properties")
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import shutil
|
|
4
|
+
import stat
|
|
5
|
+
import subprocess
|
|
6
|
+
import tarfile
|
|
7
|
+
import tempfile
|
|
8
|
+
import urllib.request
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from .logger import logger
|
|
16
|
+
except ImportError:
|
|
17
|
+
from logger import logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_trivy():
|
|
21
|
+
"""
|
|
22
|
+
Check system for Trivy installation.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
str: Path to trivy executable if found, None otherwise
|
|
26
|
+
"""
|
|
27
|
+
logger.debug("Looking for Trivy installation")
|
|
28
|
+
|
|
29
|
+
# Check for trivy in PATH first (works on both Windows and Unix-like systems)
|
|
30
|
+
trivy_path = shutil.which("trivy")
|
|
31
|
+
if trivy_path:
|
|
32
|
+
logger.debug(f"Found Trivy in PATH: {trivy_path}")
|
|
33
|
+
return trivy_path
|
|
34
|
+
|
|
35
|
+
# Platform-specific checks
|
|
36
|
+
if os.name == "nt": # Windows
|
|
37
|
+
# Common Windows installation paths
|
|
38
|
+
windows_paths = [
|
|
39
|
+
os.path.expandvars("$ProgramFiles\\Aqua Security\\Trivy\\trivy.exe"),
|
|
40
|
+
os.path.expandvars("$ProgramFiles(x86)\\Aqua Security\\Trivy\\trivy.exe"),
|
|
41
|
+
os.path.expandvars("$LOCALAPPDATA\\aquasec\\trivy\\trivy.exe"),
|
|
42
|
+
os.path.expandvars(
|
|
43
|
+
"$USERPROFILE\\scoop\\shims\\trivy.exe"
|
|
44
|
+
), # Scoop package manager
|
|
45
|
+
os.path.expandvars(
|
|
46
|
+
"$USERPROFILE\\AppData\\Local\\Microsoft\\WindowsApps\\trivy.exe"
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for path in windows_paths:
|
|
51
|
+
if os.path.isfile(path):
|
|
52
|
+
logger.debug(f"Found Trivy at Windows location: {path}")
|
|
53
|
+
return path
|
|
54
|
+
else: # Unix-like systems (Linux, macOS)
|
|
55
|
+
# Common Unix installation paths
|
|
56
|
+
unix_paths = [
|
|
57
|
+
"/usr/local/bin/trivy",
|
|
58
|
+
"/usr/bin/trivy",
|
|
59
|
+
"/opt/homebrew/bin/trivy", # Homebrew on macOS (Apple Silicon)
|
|
60
|
+
"/usr/local/opt/trivy/bin/trivy", # Homebrew on macOS
|
|
61
|
+
"/snap/bin/trivy", # Snap package
|
|
62
|
+
os.path.expanduser("~/.local/bin/trivy"), # Local user installation
|
|
63
|
+
"/opt/trivy/trivy", # Manual installation
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for path in unix_paths:
|
|
67
|
+
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
68
|
+
logger.debug(f"Found Trivy at: {path}")
|
|
69
|
+
return path
|
|
70
|
+
|
|
71
|
+
# Check for TRIVY_INSTALL_DIR environment variable as a fallback
|
|
72
|
+
trivy_install_dir = os.environ.get("TRIVY_INSTALL_DIR")
|
|
73
|
+
if trivy_install_dir:
|
|
74
|
+
trivy_bin = Path(trivy_install_dir) / "trivy" + (
|
|
75
|
+
".exe" if os.name == "nt" else ""
|
|
76
|
+
)
|
|
77
|
+
if trivy_bin.exists() and (os.name == "nt" or os.access(trivy_bin, os.X_OK)):
|
|
78
|
+
logger.debug(f"Found Trivy in TRIVY_INSTALL_DIR: {trivy_bin}")
|
|
79
|
+
return str(trivy_bin)
|
|
80
|
+
|
|
81
|
+
logger.warning("Trivy not found in PATH or common installation locations")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def install_trivy():
|
|
86
|
+
"""Install Trivy based on the current operating system.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str: Path to the installed trivy binary if successful, None otherwise
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
system = platform.system().lower()
|
|
93
|
+
machine = platform.machine().lower()
|
|
94
|
+
|
|
95
|
+
# Map platform to Trivy's release assets
|
|
96
|
+
platform_map = {
|
|
97
|
+
"darwin": {"x86_64": "macOS-64bit.tar.gz", "arm64": "macOS-ARM64.tar.gz"},
|
|
98
|
+
"linux": {
|
|
99
|
+
"x86_64": "Linux-64bit.tar.gz",
|
|
100
|
+
"arm64": "Linux-ARM64.tar.gz",
|
|
101
|
+
"armv6": "Linux-ARM.tar.gz",
|
|
102
|
+
"armv7": "Linux-ARM.tar.gz",
|
|
103
|
+
},
|
|
104
|
+
"windows": {
|
|
105
|
+
"amd64": "Windows-64bit.zip",
|
|
106
|
+
"x86_64": "Windows-64bit.zip",
|
|
107
|
+
"x86": "Windows-32bit.zip",
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Get the appropriate asset name
|
|
112
|
+
try:
|
|
113
|
+
asset = platform_map.get(system, {}).get(machine)
|
|
114
|
+
if not asset:
|
|
115
|
+
logger.error(f"Unsupported platform: {system} {machine}")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
trivy_version = "0.49.0" # You might want to make this configurable
|
|
119
|
+
download_url = f"https://github.com/aquasecurity/trivy/releases/download/v{trivy_version}/trivy_{trivy_version}_{asset}"
|
|
120
|
+
|
|
121
|
+
logger.info(f"Downloading Trivy {trivy_version} for {system} {machine}...")
|
|
122
|
+
logger.debug(f"Download URL: {download_url}")
|
|
123
|
+
|
|
124
|
+
# Create temp directory for download
|
|
125
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
126
|
+
temp_dir_path = Path(temp_dir)
|
|
127
|
+
download_path = temp_dir_path / f"trivy_{asset}"
|
|
128
|
+
|
|
129
|
+
# Download the file
|
|
130
|
+
try:
|
|
131
|
+
urllib.request.urlretrieve(download_url, download_path)
|
|
132
|
+
logger.debug(f"Downloaded to: {download_path}")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to download Trivy: {e}")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Extract the archive
|
|
138
|
+
extract_dir = temp_dir_path / "extracted"
|
|
139
|
+
extract_dir.mkdir(exist_ok=True)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if asset.endswith(".zip"):
|
|
143
|
+
with zipfile.ZipFile(download_path, "r") as zip_ref:
|
|
144
|
+
zip_ref.extractall(extract_dir)
|
|
145
|
+
else: # .tar.gz
|
|
146
|
+
with tarfile.open(download_path, "r:gz") as tar_ref:
|
|
147
|
+
tar_ref.extractall(extract_dir)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Failed to extract Trivy: {e}")
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
# Find the trivy binary in the extracted files
|
|
153
|
+
trivy_bin = None
|
|
154
|
+
for ext in ("", ".exe"):
|
|
155
|
+
bin_name = f"trivy{ext}"
|
|
156
|
+
bin_path = extract_dir / bin_name
|
|
157
|
+
if bin_path.exists():
|
|
158
|
+
trivy_bin = bin_path
|
|
159
|
+
break
|
|
160
|
+
# Check in subdirectories (common in tar.gz)
|
|
161
|
+
for sub_path in extract_dir.rglob(bin_name):
|
|
162
|
+
trivy_bin = sub_path
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
if trivy_bin:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
if not trivy_bin or not trivy_bin.exists():
|
|
169
|
+
logger.error("Could not find trivy binary in the downloaded package")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Make the binary executable on Unix-like systems
|
|
173
|
+
if system != "windows":
|
|
174
|
+
trivy_bin.chmod(trivy_bin.stat().st_mode | stat.S_IEXEC)
|
|
175
|
+
|
|
176
|
+
# Determine installation directory
|
|
177
|
+
if system == "windows":
|
|
178
|
+
install_dir = Path.home() / "AppData" / "Local" / "aquasec" / "trivy"
|
|
179
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
install_path = install_dir / "trivy.exe"
|
|
181
|
+
else:
|
|
182
|
+
# Try system-wide installation first
|
|
183
|
+
system_bin = Path("/usr/local/bin")
|
|
184
|
+
if os.access(system_bin.parent, os.W_OK):
|
|
185
|
+
install_dir = system_bin
|
|
186
|
+
install_path = install_dir / "trivy"
|
|
187
|
+
else:
|
|
188
|
+
# Fall back to user's local bin
|
|
189
|
+
install_dir = Path.home() / ".local" / "bin"
|
|
190
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
install_path = install_dir / "trivy"
|
|
192
|
+
|
|
193
|
+
# Move the binary to the installation directory
|
|
194
|
+
shutil.move(str(trivy_bin), str(install_path))
|
|
195
|
+
|
|
196
|
+
# Add to PATH if not already there
|
|
197
|
+
bin_dir = str(install_dir)
|
|
198
|
+
path = os.environ.get("PATH", "")
|
|
199
|
+
if bin_dir not in path.split(os.pathsep):
|
|
200
|
+
logger.info(f"Adding {bin_dir} to PATH")
|
|
201
|
+
os.environ["PATH"] = f"{bin_dir}{os.pathsep}{path}"
|
|
202
|
+
# You might want to add this to the user's shell profile
|
|
203
|
+
# but that's more involved and platform-specific
|
|
204
|
+
|
|
205
|
+
logger.info(f"Successfully installed Trivy to {install_path}")
|
|
206
|
+
return str(install_path)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(f"Error installing Trivy: {e}", exc_info=True)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def run_trivy_scan(report_path, target_path=".", install_if_missing=True, timeout=900):
|
|
214
|
+
"""Run a Trivy security scan on the specified directory.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
report_path (str): Path where the SARIF report will be saved
|
|
218
|
+
target_path (str, optional): Path to scan. Defaults to current directory.
|
|
219
|
+
install_if_missing (bool, optional): Whether to install Trivy if not found. Defaults to True.
|
|
220
|
+
timeout (int, optional): Maximum time in seconds to wait for the scan to complete. Defaults to 300s.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
bool: True if scan completed successfully, False otherwise
|
|
224
|
+
"""
|
|
225
|
+
trivy_path = find_trivy()
|
|
226
|
+
|
|
227
|
+
# If Trivy not found, try to install it if allowed
|
|
228
|
+
if not trivy_path and install_if_missing:
|
|
229
|
+
logger.info("Trivy not found. Attempting to install...")
|
|
230
|
+
trivy_path = install_trivy()
|
|
231
|
+
if not trivy_path:
|
|
232
|
+
logger.error("Failed to install Trivy. Please install it manually.")
|
|
233
|
+
return False
|
|
234
|
+
elif not trivy_path:
|
|
235
|
+
logger.error("Trivy not found and automatic installation is disabled.")
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
# Ensure target path exists
|
|
239
|
+
target_path = Path(target_path).absolute()
|
|
240
|
+
if not target_path.exists():
|
|
241
|
+
logger.error(f"Target path does not exist: {target_path}")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
# Ensure report directory exists
|
|
245
|
+
report_path = Path(report_path).absolute()
|
|
246
|
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
|
|
248
|
+
# # Prepare the command
|
|
249
|
+
# cmd = [
|
|
250
|
+
# str(trivy_path),
|
|
251
|
+
# "--cache-dir", str(Path.home() / ".cache" / "trivy"),
|
|
252
|
+
# "--security-checks", "vuln,config,secret",
|
|
253
|
+
# "--severity", "CRITICAL,HIGH,MEDIUM,LOW",
|
|
254
|
+
# "--format", "sarif",
|
|
255
|
+
# "--output", str(report_path),
|
|
256
|
+
# "--exit-code", "0", # Don't fail on findings, we'll handle that from the report
|
|
257
|
+
# "--quiet", # Only show progress in debug mode
|
|
258
|
+
# "--no-progress", # Disable progress bar
|
|
259
|
+
# str(target_path)
|
|
260
|
+
# ]
|
|
261
|
+
#
|
|
262
|
+
cmd = [
|
|
263
|
+
trivy_path, # Use the full path to trivy
|
|
264
|
+
"repository",
|
|
265
|
+
"--format",
|
|
266
|
+
"sarif",
|
|
267
|
+
"--output",
|
|
268
|
+
str(report_path),
|
|
269
|
+
str(target_path),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
click.echo('Starting Trivy scan... This may take a while...')
|
|
274
|
+
result = subprocess.run(
|
|
275
|
+
cmd,
|
|
276
|
+
check=False, # We'll handle the return code ourselves
|
|
277
|
+
capture_output=True,
|
|
278
|
+
text=True,
|
|
279
|
+
timeout=timeout,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Log the output
|
|
283
|
+
if result.stdout:
|
|
284
|
+
logger.debug(f"Trivy output:\n{result.stdout}")
|
|
285
|
+
if result.stderr:
|
|
286
|
+
logger.debug(f"Trivy stderr:\n{result.stderr}")
|
|
287
|
+
|
|
288
|
+
# Check if the scan completed successfully
|
|
289
|
+
if result.returncode != 0:
|
|
290
|
+
logger.error(f"Trivy scan failed with exit code {result.returncode}")
|
|
291
|
+
if result.stderr:
|
|
292
|
+
logger.error(f"Error: {result.stderr.strip()}")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
logger.info(
|
|
296
|
+
f"Trivy scan completed successfully. Report saved to: {report_path}"
|
|
297
|
+
)
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
except subprocess.TimeoutExpired:
|
|
301
|
+
logger.error(f"Trivy scan timed out after {timeout} seconds")
|
|
302
|
+
return False
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Error running Trivy scan: {str(e)}", exc_info=True)
|
|
305
|
+
return False
|