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.
@@ -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