code-audit-23 0.1.1__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,302 @@
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
+ try:
13
+ from .logger import logger
14
+ except ImportError:
15
+ from logger import logger
16
+
17
+
18
+ def find_trivy():
19
+ """
20
+ Check system for Trivy installation.
21
+
22
+ Returns:
23
+ str: Path to trivy executable if found, None otherwise
24
+ """
25
+ logger.debug("Looking for Trivy installation")
26
+
27
+ # Check for trivy in PATH first (works on both Windows and Unix-like systems)
28
+ trivy_path = shutil.which("trivy")
29
+ if trivy_path:
30
+ logger.debug(f"Found Trivy in PATH: {trivy_path}")
31
+ return trivy_path
32
+
33
+ # Platform-specific checks
34
+ if os.name == "nt": # Windows
35
+ # Common Windows installation paths
36
+ windows_paths = [
37
+ os.path.expandvars("$ProgramFiles\\Aqua Security\\Trivy\\trivy.exe"),
38
+ os.path.expandvars("$ProgramFiles(x86)\\Aqua Security\\Trivy\\trivy.exe"),
39
+ os.path.expandvars("$LOCALAPPDATA\\aquasec\\trivy\\trivy.exe"),
40
+ os.path.expandvars(
41
+ "$USERPROFILE\\scoop\\shims\\trivy.exe"
42
+ ), # Scoop package manager
43
+ os.path.expandvars(
44
+ "$USERPROFILE\\AppData\\Local\\Microsoft\\WindowsApps\\trivy.exe"
45
+ ),
46
+ ]
47
+
48
+ for path in windows_paths:
49
+ if os.path.isfile(path):
50
+ logger.debug(f"Found Trivy at Windows location: {path}")
51
+ return path
52
+ else: # Unix-like systems (Linux, macOS)
53
+ # Common Unix installation paths
54
+ unix_paths = [
55
+ "/usr/local/bin/trivy",
56
+ "/usr/bin/trivy",
57
+ "/opt/homebrew/bin/trivy", # Homebrew on macOS (Apple Silicon)
58
+ "/usr/local/opt/trivy/bin/trivy", # Homebrew on macOS
59
+ "/snap/bin/trivy", # Snap package
60
+ os.path.expanduser("~/.local/bin/trivy"), # Local user installation
61
+ "/opt/trivy/trivy", # Manual installation
62
+ ]
63
+
64
+ for path in unix_paths:
65
+ if os.path.isfile(path) and os.access(path, os.X_OK):
66
+ logger.debug(f"Found Trivy at: {path}")
67
+ return path
68
+
69
+ # Check for TRIVY_INSTALL_DIR environment variable as a fallback
70
+ trivy_install_dir = os.environ.get("TRIVY_INSTALL_DIR")
71
+ if trivy_install_dir:
72
+ trivy_bin = Path(trivy_install_dir) / "trivy" + (
73
+ ".exe" if os.name == "nt" else ""
74
+ )
75
+ if trivy_bin.exists() and (os.name == "nt" or os.access(trivy_bin, os.X_OK)):
76
+ logger.debug(f"Found Trivy in TRIVY_INSTALL_DIR: {trivy_bin}")
77
+ return str(trivy_bin)
78
+
79
+ logger.warning("Trivy not found in PATH or common installation locations")
80
+ return None
81
+
82
+
83
+ def install_trivy():
84
+ """Install Trivy based on the current operating system.
85
+
86
+ Returns:
87
+ str: Path to the installed trivy binary if successful, None otherwise
88
+ """
89
+
90
+ system = platform.system().lower()
91
+ machine = platform.machine().lower()
92
+
93
+ # Map platform to Trivy's release assets
94
+ platform_map = {
95
+ "darwin": {"x86_64": "macOS-64bit.tar.gz", "arm64": "macOS-ARM64.tar.gz"},
96
+ "linux": {
97
+ "x86_64": "Linux-64bit.tar.gz",
98
+ "arm64": "Linux-ARM64.tar.gz",
99
+ "armv6": "Linux-ARM.tar.gz",
100
+ "armv7": "Linux-ARM.tar.gz",
101
+ },
102
+ "windows": {
103
+ "amd64": "Windows-64bit.zip",
104
+ "x86_64": "Windows-64bit.zip",
105
+ "x86": "Windows-32bit.zip",
106
+ },
107
+ }
108
+
109
+ # Get the appropriate asset name
110
+ try:
111
+ asset = platform_map.get(system, {}).get(machine)
112
+ if not asset:
113
+ logger.error(f"Unsupported platform: {system} {machine}")
114
+ return None
115
+
116
+ trivy_version = "0.49.0" # You might want to make this configurable
117
+ download_url = f"https://github.com/aquasecurity/trivy/releases/download/v{trivy_version}/trivy_{trivy_version}_{asset}"
118
+
119
+ logger.info(f"Downloading Trivy {trivy_version} for {system} {machine}...")
120
+ logger.debug(f"Download URL: {download_url}")
121
+
122
+ # Create temp directory for download
123
+ with tempfile.TemporaryDirectory() as temp_dir:
124
+ temp_dir_path = Path(temp_dir)
125
+ download_path = temp_dir_path / f"trivy_{asset}"
126
+
127
+ # Download the file
128
+ try:
129
+ urllib.request.urlretrieve(download_url, download_path)
130
+ logger.debug(f"Downloaded to: {download_path}")
131
+ except Exception as e:
132
+ logger.error(f"Failed to download Trivy: {e}")
133
+ return None
134
+
135
+ # Extract the archive
136
+ extract_dir = temp_dir_path / "extracted"
137
+ extract_dir.mkdir(exist_ok=True)
138
+
139
+ try:
140
+ if asset.endswith(".zip"):
141
+ with zipfile.ZipFile(download_path, "r") as zip_ref:
142
+ zip_ref.extractall(extract_dir)
143
+ else: # .tar.gz
144
+ with tarfile.open(download_path, "r:gz") as tar_ref:
145
+ tar_ref.extractall(extract_dir)
146
+ except Exception as e:
147
+ logger.error(f"Failed to extract Trivy: {e}")
148
+ return None
149
+
150
+ # Find the trivy binary in the extracted files
151
+ trivy_bin = None
152
+ for ext in ("", ".exe"):
153
+ bin_name = f"trivy{ext}"
154
+ bin_path = extract_dir / bin_name
155
+ if bin_path.exists():
156
+ trivy_bin = bin_path
157
+ break
158
+ # Check in subdirectories (common in tar.gz)
159
+ for sub_path in extract_dir.rglob(bin_name):
160
+ trivy_bin = sub_path
161
+ break
162
+
163
+ if trivy_bin:
164
+ break
165
+
166
+ if not trivy_bin or not trivy_bin.exists():
167
+ logger.error("Could not find trivy binary in the downloaded package")
168
+ return None
169
+
170
+ # Make the binary executable on Unix-like systems
171
+ if system != "windows":
172
+ trivy_bin.chmod(trivy_bin.stat().st_mode | stat.S_IEXEC)
173
+
174
+ # Determine installation directory
175
+ if system == "windows":
176
+ install_dir = Path.home() / "AppData" / "Local" / "aquasec" / "trivy"
177
+ install_dir.mkdir(parents=True, exist_ok=True)
178
+ install_path = install_dir / "trivy.exe"
179
+ else:
180
+ # Try system-wide installation first
181
+ system_bin = Path("/usr/local/bin")
182
+ if os.access(system_bin.parent, os.W_OK):
183
+ install_dir = system_bin
184
+ install_path = install_dir / "trivy"
185
+ else:
186
+ # Fall back to user's local bin
187
+ install_dir = Path.home() / ".local" / "bin"
188
+ install_dir.mkdir(parents=True, exist_ok=True)
189
+ install_path = install_dir / "trivy"
190
+
191
+ # Move the binary to the installation directory
192
+ shutil.move(str(trivy_bin), str(install_path))
193
+
194
+ # Add to PATH if not already there
195
+ bin_dir = str(install_dir)
196
+ path = os.environ.get("PATH", "")
197
+ if bin_dir not in path.split(os.pathsep):
198
+ logger.info(f"Adding {bin_dir} to PATH")
199
+ os.environ["PATH"] = f"{bin_dir}{os.pathsep}{path}"
200
+ # You might want to add this to the user's shell profile
201
+ # but that's more involved and platform-specific
202
+
203
+ logger.info(f"Successfully installed Trivy to {install_path}")
204
+ return str(install_path)
205
+
206
+ except Exception as e:
207
+ logger.error(f"Error installing Trivy: {e}", exc_info=True)
208
+ return None
209
+
210
+
211
+ def run_trivy_scan(report_path, target_path=".", install_if_missing=True, timeout=900):
212
+ """Run a Trivy security scan on the specified directory.
213
+
214
+ Args:
215
+ report_path (str): Path where the SARIF report will be saved
216
+ target_path (str, optional): Path to scan. Defaults to current directory.
217
+ install_if_missing (bool, optional): Whether to install Trivy if not found. Defaults to True.
218
+ timeout (int, optional): Maximum time in seconds to wait for the scan to complete. Defaults to 300s.
219
+
220
+ Returns:
221
+ bool: True if scan completed successfully, False otherwise
222
+ """
223
+ trivy_path = find_trivy()
224
+
225
+ # If Trivy not found, try to install it if allowed
226
+ if not trivy_path and install_if_missing:
227
+ logger.info("Trivy not found. Attempting to install...")
228
+ trivy_path = install_trivy()
229
+ if not trivy_path:
230
+ logger.error("Failed to install Trivy. Please install it manually.")
231
+ return False
232
+ elif not trivy_path:
233
+ logger.error("Trivy not found and automatic installation is disabled.")
234
+ return False
235
+
236
+ # Ensure target path exists
237
+ target_path = Path(target_path).absolute()
238
+ if not target_path.exists():
239
+ logger.error(f"Target path does not exist: {target_path}")
240
+ return False
241
+
242
+ # Ensure report directory exists
243
+ report_path = Path(report_path).absolute()
244
+ report_path.parent.mkdir(parents=True, exist_ok=True)
245
+
246
+ # # Prepare the command
247
+ # cmd = [
248
+ # str(trivy_path),
249
+ # "--cache-dir", str(Path.home() / ".cache" / "trivy"),
250
+ # "--security-checks", "vuln,config,secret",
251
+ # "--severity", "CRITICAL,HIGH,MEDIUM,LOW",
252
+ # "--format", "sarif",
253
+ # "--output", str(report_path),
254
+ # "--exit-code", "0", # Don't fail on findings, we'll handle that from the report
255
+ # "--quiet", # Only show progress in debug mode
256
+ # "--no-progress", # Disable progress bar
257
+ # str(target_path)
258
+ # ]
259
+ #
260
+ cmd = [
261
+ trivy_path, # Use the full path to trivy
262
+ "repository",
263
+ "--format",
264
+ "sarif",
265
+ "--output",
266
+ str(report_path),
267
+ str(target_path),
268
+ ]
269
+
270
+ try:
271
+ result = subprocess.run(
272
+ cmd,
273
+ check=False, # We'll handle the return code ourselves
274
+ capture_output=True,
275
+ text=True,
276
+ timeout=timeout,
277
+ )
278
+
279
+ # Log the output
280
+ if result.stdout:
281
+ logger.debug(f"Trivy output:\n{result.stdout}")
282
+ if result.stderr:
283
+ logger.debug(f"Trivy stderr:\n{result.stderr}")
284
+
285
+ # Check if the scan completed successfully
286
+ if result.returncode != 0:
287
+ logger.error(f"Trivy scan failed with exit code {result.returncode}")
288
+ if result.stderr:
289
+ logger.error(f"Error: {result.stderr.strip()}")
290
+ return False
291
+
292
+ logger.info(
293
+ f"Trivy scan completed successfully. Report saved to: {report_path}"
294
+ )
295
+ return True
296
+
297
+ except subprocess.TimeoutExpired:
298
+ logger.error(f"Trivy scan timed out after {timeout} seconds")
299
+ return False
300
+ except Exception as e:
301
+ logger.error(f"Error running Trivy scan: {str(e)}", exc_info=True)
302
+ return False