prismor 0.1.0__tar.gz → 0.1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prismor
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A CLI tool for scanning GitHub repositories for vulnerabilities, secrets, and generating SBOMs
5
5
  Home-page: https://github.com/PrismorSec/prismor-cli
6
6
  Author: Prismor
@@ -1,6 +1,6 @@
1
1
  """Prismor CLI - Security scanning tool for GitHub repositories."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
4
4
  __author__ = "Prismor"
5
5
  __description__ = "A CLI tool for scanning GitHub repositories for vulnerabilities, secrets, and generating SBOMs"
6
6
 
@@ -0,0 +1,242 @@
1
+ """API client for Prismor security scanning service."""
2
+
3
+ import os
4
+ import requests
5
+ from typing import Optional, Dict, Any
6
+
7
+
8
+ class PrismorAPIError(Exception):
9
+ """Custom exception for Prismor API errors."""
10
+ pass
11
+
12
+
13
+ class PrismorClient:
14
+ """Client for interacting with Prismor API."""
15
+
16
+ def __init__(self, api_key: Optional[str] = None):
17
+ """Initialize the Prismor API client.
18
+
19
+ Args:
20
+ api_key: Prismor API key. If not provided, will look for PRISMOR_API_KEY env var.
21
+ """
22
+ self.api_key = api_key or os.environ.get("PRISMOR_API_KEY")
23
+ if not self.api_key:
24
+ raise PrismorAPIError(
25
+ "PRISMOR_API_KEY environment variable is not set. "
26
+ "Please set it with: export PRISMOR_API_KEY=your_api_key"
27
+ )
28
+
29
+ # self.base_url = "http://localhost:3000"
30
+ self.base_url = "https://prismor.dev"
31
+ self.headers = {
32
+ "Authorization": f"Bearer {self.api_key}",
33
+ "Content-Type": "application/json"
34
+ }
35
+
36
+ def authenticate(self) -> Dict[str, Any]:
37
+ """Authenticate with the Prismor API using the API key.
38
+
39
+ Returns:
40
+ Dictionary containing user information and repositories
41
+
42
+ Raises:
43
+ PrismorAPIError: If authentication fails
44
+ """
45
+ try:
46
+ response = requests.post(
47
+ f"{self.base_url}/api/cli/auth",
48
+ json={"apiKey": self.api_key},
49
+ headers={"Content-Type": "application/json"},
50
+ timeout=30
51
+ )
52
+
53
+ if response.status_code == 401:
54
+ raise PrismorAPIError("Invalid API key. Please check your PRISMOR_API_KEY.")
55
+
56
+ if response.status_code == 400:
57
+ raise PrismorAPIError("API key is required.")
58
+
59
+ if response.status_code >= 400:
60
+ error_msg = response.json().get("error", "Authentication failed")
61
+ raise PrismorAPIError(f"Authentication error: {error_msg}")
62
+
63
+ response.raise_for_status()
64
+ return response.json()
65
+
66
+ except requests.exceptions.Timeout:
67
+ raise PrismorAPIError("Authentication request timed out.")
68
+ except requests.exceptions.ConnectionError:
69
+ raise PrismorAPIError(
70
+ "Failed to connect to Prismor API. Please check your internet connection."
71
+ )
72
+ except requests.exceptions.RequestException as e:
73
+ raise PrismorAPIError(f"Authentication request failed: {str(e)}")
74
+
75
+ def normalize_repo_url(self, repo: str) -> str:
76
+ """Normalize repository input to a full GitHub URL.
77
+
78
+ Args:
79
+ repo: Repository in format 'username/repo' or full GitHub URL
80
+
81
+ Returns:
82
+ Full GitHub repository URL
83
+ """
84
+ if repo.startswith("http://") or repo.startswith("https://"):
85
+ return repo
86
+
87
+ # Assume it's in username/repo format
88
+ if "/" in repo:
89
+ return f"https://github.com/{repo}"
90
+
91
+ raise PrismorAPIError(
92
+ f"Invalid repository format: {repo}. "
93
+ "Please use 'username/repo' or full GitHub URL"
94
+ )
95
+
96
+ def scan(
97
+ self,
98
+ repo: str,
99
+ vex: bool = False,
100
+ sbom: bool = False,
101
+ detect_secret: bool = False,
102
+ fullscan: bool = False,
103
+ branch: Optional[str] = None
104
+ ) -> Dict[str, Any]:
105
+ """Perform security scan on a GitHub repository.
106
+
107
+ Args:
108
+ repo: Repository URL or username/repo format
109
+ vex: Enable vulnerability scanning
110
+ sbom: Enable SBOM generation
111
+ detect_secret: Enable secret detection
112
+ fullscan: Enable all scan types
113
+ branch: Specific branch to scan (defaults to main/master)
114
+
115
+ Returns:
116
+ Dictionary containing scan results
117
+ """
118
+ repo_url = self.normalize_repo_url(repo)
119
+
120
+ # First authenticate to get user info
121
+ auth_response = self.authenticate()
122
+ user_info = auth_response.get("user", {})
123
+
124
+ # Prepare request payload for CLI scan
125
+ payload = {
126
+ "repo_url": repo_url,
127
+ "api_key": self.api_key,
128
+ "vex": vex or fullscan,
129
+ "sbom": sbom or fullscan,
130
+ "detect_secret": detect_secret or fullscan,
131
+ "fullscan": fullscan,
132
+ "branch": branch
133
+ }
134
+
135
+ try:
136
+ response = requests.post(
137
+ f"{self.base_url}/api/cli/scan",
138
+ json=payload,
139
+ headers={"Content-Type": "application/json"},
140
+ timeout=300 # 5 minute timeout
141
+ )
142
+
143
+ if response.status_code == 401:
144
+ error_data = response.json()
145
+ if error_data.get("action") == "integrate_github":
146
+ raise PrismorAPIError(
147
+ f"{error_data.get('message', 'GitHub integration required')}\n"
148
+ f"Please visit: {error_data.get('integration_url', 'https://prismor.dev/dashboard')}"
149
+ )
150
+ raise PrismorAPIError("Invalid API key. Please check your PRISMOR_API_KEY.")
151
+
152
+ if response.status_code == 404:
153
+ raise PrismorAPIError("CLI scan endpoint not found. Please check if CLI endpoints are available.")
154
+
155
+ if response.status_code >= 400:
156
+ error_msg = response.json().get("error", "Unknown error")
157
+ raise PrismorAPIError(f"API error: {error_msg}")
158
+
159
+ response.raise_for_status()
160
+ result = response.json()
161
+
162
+ # Handle the new response format from CLI endpoint
163
+ if result.get("ok") and "results" in result:
164
+ return result["results"]
165
+ return result
166
+
167
+ except requests.exceptions.Timeout:
168
+ raise PrismorAPIError(
169
+ "Request timed out. The repository scan is taking longer than expected."
170
+ )
171
+ except requests.exceptions.ConnectionError:
172
+ raise PrismorAPIError(
173
+ "Failed to connect to Prismor API. Please check your internet connection."
174
+ )
175
+ except requests.exceptions.RequestException as e:
176
+ raise PrismorAPIError(f"Request failed: {str(e)}")
177
+
178
+ def get_repositories(self) -> Dict[str, Any]:
179
+ """Get user's repositories.
180
+
181
+ Returns:
182
+ Dictionary containing user repositories
183
+
184
+ Raises:
185
+ PrismorAPIError: If request fails
186
+ """
187
+ auth_response = self.authenticate()
188
+ user_info = auth_response.get("user", {})
189
+ return {
190
+ "repositories": user_info.get("repositories", []),
191
+ "user": {
192
+ "id": user_info.get("id"),
193
+ "email": user_info.get("email"),
194
+ "name": user_info.get("name")
195
+ }
196
+ }
197
+
198
+ def get_repository_by_name(self, repo_name: str) -> Dict[str, Any]:
199
+ """Get repository ID by repository name.
200
+
201
+ Args:
202
+ repo_name: Repository name (e.g., "username/repo")
203
+
204
+ Returns:
205
+ Dictionary containing repository information including ID
206
+
207
+ Raises:
208
+ PrismorAPIError: If request fails
209
+ """
210
+ try:
211
+ response = requests.post(
212
+ f"{self.base_url}/api/repositories/by-name",
213
+ json={
214
+ "apiKey": self.api_key,
215
+ "repoName": repo_name
216
+ },
217
+ headers={"Content-Type": "application/json"},
218
+ timeout=30
219
+ )
220
+
221
+ if response.status_code == 401:
222
+ raise PrismorAPIError("Invalid API key. Please check your PRISMOR_API_KEY.")
223
+
224
+ if response.status_code == 404:
225
+ raise PrismorAPIError(f"Repository '{repo_name}' not found.")
226
+
227
+ if response.status_code >= 400:
228
+ error_msg = response.json().get("error", "Unknown error")
229
+ raise PrismorAPIError(f"API error: {error_msg}")
230
+
231
+ response.raise_for_status()
232
+ return response.json()
233
+
234
+ except requests.exceptions.Timeout:
235
+ raise PrismorAPIError("Request timed out.")
236
+ except requests.exceptions.ConnectionError:
237
+ raise PrismorAPIError(
238
+ "Failed to connect to Prismor API. Please check your internet connection."
239
+ )
240
+ except requests.exceptions.RequestException as e:
241
+ raise PrismorAPIError(f"Request failed: {str(e)}")
242
+
@@ -0,0 +1,351 @@
1
+ """Command-line interface for Prismor security scanning tool."""
2
+
3
+ import sys
4
+ import json
5
+ import click
6
+ from typing import Optional
7
+ from .api import PrismorClient, PrismorAPIError
8
+
9
+
10
+ def print_success(message: str):
11
+ """Print success message in green."""
12
+ click.secho(f"✓ {message}", fg="green")
13
+
14
+
15
+ def print_error(message: str):
16
+ """Print error message in red."""
17
+ click.secho(f"✗ {message}", fg="red", err=True)
18
+
19
+
20
+ def print_info(message: str):
21
+ """Print info message in blue."""
22
+ click.secho(f"ℹ {message}", fg="blue")
23
+
24
+
25
+ def print_warning(message: str):
26
+ """Print warning message in yellow."""
27
+ click.secho(f"⚠ {message}", fg="yellow")
28
+
29
+
30
+ def format_scan_results(results: dict, scan_type: str):
31
+ """Format and display scan results."""
32
+ click.echo("\n" + "=" * 60)
33
+ click.secho(f" Scan Results - {scan_type}", fg="cyan", bold=True)
34
+ click.echo("=" * 60 + "\n")
35
+
36
+ # Display repository information
37
+ if "repository" in results:
38
+ click.secho("Repository:", fg="yellow", bold=True)
39
+ click.echo(f" {results['repository']}\n")
40
+
41
+ if "branch" in results:
42
+ click.secho("Branch:", fg="yellow", bold=True)
43
+ click.echo(f" {results['branch']}\n")
44
+
45
+ if "commit_sha" in results:
46
+ click.secho("Commit SHA:", fg="yellow", bold=True)
47
+ click.echo(f" {results['commit_sha']}\n")
48
+
49
+ # Display scan results for each scan type
50
+ if "scans" in results:
51
+ scans = results["scans"]
52
+
53
+ # Vulnerability scan results
54
+ if "vulnerability" in scans:
55
+ vuln_scan = scans["vulnerability"]
56
+ click.secho("Vulnerability Scan:", fg="yellow", bold=True)
57
+ status_color = "green" if vuln_scan.get("status") == "success" else "red"
58
+ click.secho(f" Status: {vuln_scan.get('status', 'unknown')}", fg=status_color)
59
+
60
+ if "scan_results" in vuln_scan:
61
+ scan_data = vuln_scan["scan_results"]
62
+ if "vulnerabilities" in scan_data or "Results" in scan_data:
63
+ vuln_data = scan_data.get("vulnerabilities", scan_data.get("Results", []))
64
+ if isinstance(vuln_data, list):
65
+ click.echo(f" Vulnerabilities Found: {len(vuln_data)}")
66
+ else:
67
+ click.echo(f" Vulnerabilities: Data available")
68
+
69
+ if "public_url" in vuln_scan:
70
+ click.echo(f" Results URL: {vuln_scan['public_url']}")
71
+ click.echo()
72
+
73
+ # SBOM results
74
+ if "sbom" in scans:
75
+ sbom_scan = scans["sbom"]
76
+ click.secho("SBOM Generation:", fg="yellow", bold=True)
77
+ status_color = "green" if sbom_scan.get("status") == "success" else "red"
78
+ click.secho(f" Status: {sbom_scan.get('status', 'unknown')}", fg=status_color)
79
+
80
+ if "sbom" in sbom_scan:
81
+ sbom_data = sbom_scan["sbom"]
82
+ if isinstance(sbom_data, list):
83
+ click.echo(f" Artifacts Found: {len(sbom_data)}")
84
+ else:
85
+ click.echo(f" SBOM: Data available")
86
+
87
+ if "public_url" in sbom_scan:
88
+ click.echo(f" Results URL: {sbom_scan['public_url']}")
89
+ click.echo()
90
+
91
+ # Secret scan results
92
+ if "secret" in scans:
93
+ secret_scan = scans["secret"]
94
+ click.secho("Secret Detection:", fg="yellow", bold=True)
95
+ status_color = "green" if secret_scan.get("status") == "success" else "red"
96
+ click.secho(f" Status: {secret_scan.get('status', 'unknown')}", fg=status_color)
97
+
98
+ if "summary" in secret_scan:
99
+ summary = secret_scan["summary"]
100
+ if isinstance(summary, dict):
101
+ for key, value in summary.items():
102
+ click.echo(f" {key}: {value}")
103
+ else:
104
+ click.echo(f" Summary: {summary}")
105
+
106
+ if "public_url" in secret_scan:
107
+ click.echo(f" Results URL: {secret_scan['public_url']}")
108
+ click.echo()
109
+
110
+ # Display scan timestamp
111
+ if "scanned_at" in results:
112
+ click.secho("Scanned At:", fg="yellow", bold=True)
113
+ click.echo(f" {results['scanned_at']}\n")
114
+
115
+ click.echo("=" * 60 + "\n")
116
+
117
+
118
+ @click.group(invoke_without_command=True)
119
+ @click.option(
120
+ "--scan",
121
+ type=str,
122
+ help="Repository to scan (username/repo or full GitHub URL)"
123
+ )
124
+ @click.option("--vex", is_flag=True, help="Perform vulnerability scanning")
125
+ @click.option("--sbom", is_flag=True, help="Generate Software Bill of Materials")
126
+ @click.option("--detect-secret", is_flag=True, help="Detect secrets in repository")
127
+ @click.option("--fullscan", is_flag=True, help="Perform all scan types")
128
+ @click.option("--branch", type=str, help="Specific branch to scan (defaults to main/master)")
129
+ @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
130
+ @click.version_option(version="0.1.2", prog_name="prismor")
131
+ @click.pass_context
132
+ def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
133
+ fullscan: bool, branch: Optional[str], output_json: bool):
134
+ """Prismor CLI - Security scanning tool for GitHub repositories.
135
+
136
+ Examples:
137
+ prismor --scan username/repo --vex
138
+ prismor --scan username/repo --fullscan
139
+ prismor --scan https://github.com/username/repo --detect-secret
140
+ prismor --scan username/repo --sbom --branch develop
141
+ prismor status
142
+ prismor repos
143
+ """
144
+ # If no command and no scan option, show help
145
+ if ctx.invoked_subcommand is None and not scan:
146
+ click.echo(ctx.get_help())
147
+ return
148
+
149
+ # If scan option is provided, perform the scan
150
+ if scan:
151
+ # Check if at least one scan type is selected
152
+ if not any([vex, sbom, detect_secret, fullscan]):
153
+ print_error("Please specify at least one scan type: --vex, --sbom, --detect-secret, or --fullscan")
154
+ sys.exit(1)
155
+
156
+ try:
157
+ # Initialize API client
158
+ print_info(f"Initializing Prismor scan for: {scan}")
159
+ client = PrismorClient()
160
+
161
+ # Determine scan type for display
162
+ scan_types = []
163
+ if fullscan:
164
+ scan_types.append("Full Scan (VEX + SBOM + Secret Detection)")
165
+ else:
166
+ if vex:
167
+ scan_types.append("VEX")
168
+ if sbom:
169
+ scan_types.append("SBOM")
170
+ if detect_secret:
171
+ scan_types.append("Secret Detection")
172
+
173
+ print_info(f"Scan type: {', '.join(scan_types)}")
174
+ print_info("Starting scan... (this may take a few minutes)")
175
+
176
+ # Perform scan
177
+ results = client.scan(
178
+ repo=scan,
179
+ vex=vex,
180
+ sbom=sbom,
181
+ detect_secret=detect_secret,
182
+ fullscan=fullscan,
183
+ branch=branch
184
+ )
185
+
186
+ # Output results
187
+ if output_json:
188
+ click.echo(json.dumps(results, indent=2))
189
+ else:
190
+ print_success("Scan completed successfully!")
191
+ format_scan_results(results, ', '.join(scan_types))
192
+
193
+ # Try to get repository ID and display dashboard link
194
+ try:
195
+ # Extract repo name from scan input
196
+ repo_name = scan
197
+ if scan.startswith("http://") or scan.startswith("https://"):
198
+ # Extract from GitHub URL
199
+ if "github.com/" in scan:
200
+ repo_name = scan.split("github.com/")[1].rstrip("/")
201
+
202
+ # Get repository ID
203
+ repo_info = client.get_repository_by_name(repo_name)
204
+ if repo_info.get("success") and "repository" in repo_info:
205
+ repo_id = repo_info["repository"]["id"]
206
+ dashboard_url = f"https://prismor.dev/repositories/{repo_id}"
207
+
208
+ click.echo("\n" + "=" * 60)
209
+ click.secho(" 📊 Dashboard Analysis", fg="cyan", bold=True)
210
+ click.echo("=" * 60)
211
+ click.secho(f"🔗 View detailed analysis and insights:", fg="blue")
212
+ click.secho(f" {dashboard_url}", fg="green", bold=True)
213
+ click.echo("\n💡 The dashboard provides:")
214
+ click.echo(" • Interactive visualizations and charts")
215
+ click.echo(" • Historical vulnerability trends")
216
+ click.echo(" • Detailed security reports")
217
+ click.echo(" • Team collaboration features")
218
+ click.echo(" • Export capabilities")
219
+ click.echo("=" * 60 + "\n")
220
+
221
+ except PrismorAPIError as e:
222
+ # Repository might not be found, continue without dashboard link
223
+ print_warning(f"Could not generate dashboard link: {str(e)}")
224
+ except Exception as e:
225
+ # Any other error, continue without dashboard link
226
+ print_warning(f"Could not generate dashboard link: {str(e)}")
227
+
228
+ except PrismorAPIError as e:
229
+ print_error(str(e))
230
+ sys.exit(1)
231
+ except Exception as e:
232
+ print_error(f"Unexpected error: {str(e)}")
233
+ sys.exit(1)
234
+
235
+
236
+ @cli.command()
237
+ def version():
238
+ """Display the version of Prismor CLI."""
239
+ click.echo("Prismor CLI v0.1.0")
240
+
241
+
242
+ @cli.command()
243
+ def config():
244
+ """Display current configuration."""
245
+ import os
246
+
247
+ click.echo("\n" + "=" * 60)
248
+ click.secho(" Prismor CLI Configuration", fg="cyan", bold=True)
249
+ click.echo("=" * 60 + "\n")
250
+
251
+ # Check API key
252
+ api_key = os.environ.get("PRISMOR_API_KEY")
253
+ if api_key:
254
+ # Show only first and last 4 characters
255
+ masked_key = f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else "***"
256
+ print_success(f"PRISMOR_API_KEY: {masked_key}")
257
+ else:
258
+ print_error("PRISMOR_API_KEY: Not set")
259
+ click.echo("\nTo set your API key, run:")
260
+ click.echo(" export PRISMOR_API_KEY=your_api_key")
261
+
262
+ # click.echo("\nAPI Endpoint: http://localhost:3000")
263
+ click.echo("=" * 60 + "\n")
264
+
265
+
266
+ @cli.command()
267
+ def repos():
268
+ """List your connected repositories."""
269
+ try:
270
+ client = PrismorClient()
271
+ repos_data = client.get_repositories()
272
+
273
+ click.echo("\n" + "=" * 60)
274
+ click.secho(" Your Repositories", fg="cyan", bold=True)
275
+ click.echo("=" * 60 + "\n")
276
+
277
+ user_info = repos_data.get("user", {})
278
+ if user_info:
279
+ click.secho(f"User: {user_info.get('name', 'Unknown')} ({user_info.get('email', 'No email')})", fg="yellow")
280
+ click.echo()
281
+
282
+ repositories = repos_data.get("repositories", [])
283
+ if repositories:
284
+ for repo in repositories:
285
+ click.secho(f"• {repo.get('name', 'Unknown')}", fg="green")
286
+ click.echo(f" URL: {repo.get('htmlUrl', 'No URL')}")
287
+ click.echo(f" Owner: {repo.get('githubOwner', 'Unknown')}")
288
+ click.echo()
289
+ else:
290
+ print_warning("No repositories found. Connect repositories through the web interface.")
291
+
292
+ click.echo("=" * 60 + "\n")
293
+
294
+ except PrismorAPIError as e:
295
+ print_error(str(e))
296
+ sys.exit(1)
297
+ except Exception as e:
298
+ print_error(f"Unexpected error: {str(e)}")
299
+ sys.exit(1)
300
+
301
+
302
+ @cli.command()
303
+ def status():
304
+ """Check your account status and GitHub integration."""
305
+ try:
306
+ client = PrismorClient()
307
+ auth_data = client.authenticate()
308
+
309
+ click.echo("\n" + "=" * 60)
310
+ click.secho(" Account Status", fg="cyan", bold=True)
311
+ click.echo("=" * 60 + "\n")
312
+
313
+ user_info = auth_data.get("user", {})
314
+ if user_info:
315
+ click.secho(f"User: {user_info.get('name', 'Unknown')} ({user_info.get('email', 'No email')})", fg="yellow")
316
+ click.echo()
317
+
318
+ repositories = user_info.get("repositories", [])
319
+ click.secho(f"Connected Repositories: {len(repositories)}", fg="green")
320
+
321
+ if repositories:
322
+ click.echo("\nRepository List:")
323
+ for repo in repositories:
324
+ click.echo(f" • {repo.get('name', 'Unknown')} ({repo.get('htmlUrl', 'No URL')})")
325
+ else:
326
+ print_warning("No repositories connected.")
327
+ click.echo("\nTo connect repositories:")
328
+ click.echo(" 1. Visit https://prismor.dev/dashboard")
329
+ click.echo(" 2. Connect your GitHub account")
330
+ click.echo(" 3. Select repositories to scan")
331
+ else:
332
+ print_error("Failed to retrieve account information.")
333
+
334
+ click.echo("\n" + "=" * 60 + "\n")
335
+
336
+ except PrismorAPIError as e:
337
+ print_error(str(e))
338
+ sys.exit(1)
339
+ except Exception as e:
340
+ print_error(f"Unexpected error: {str(e)}")
341
+ sys.exit(1)
342
+
343
+
344
+ def main():
345
+ """Entry point for the CLI."""
346
+ cli()
347
+
348
+
349
+ if __name__ == "__main__":
350
+ main()
351
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prismor
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A CLI tool for scanning GitHub repositories for vulnerabilities, secrets, and generating SBOMs
5
5
  Home-page: https://github.com/PrismorSec/prismor-cli
6
6
  Author: Prismor
@@ -17,7 +17,7 @@ if os.path.exists("README.md"):
17
17
 
18
18
  setup(
19
19
  name="prismor",
20
- version="0.1.0",
20
+ version="0.1.2",
21
21
  author="Prismor",
22
22
  author_email="support@prismor.dev",
23
23
  description="A CLI tool for scanning GitHub repositories for vulnerabilities, secrets, and generating SBOMs",
@@ -1,118 +0,0 @@
1
- """API client for Prismor security scanning service."""
2
-
3
- import os
4
- import requests
5
- from typing import Optional, Dict, Any
6
-
7
-
8
- class PrismorAPIError(Exception):
9
- """Custom exception for Prismor API errors."""
10
- pass
11
-
12
-
13
- class PrismorClient:
14
- """Client for interacting with Prismor API."""
15
-
16
- def __init__(self, api_key: Optional[str] = None):
17
- """Initialize the Prismor API client.
18
-
19
- Args:
20
- api_key: Prismor API key. If not provided, will look for PRISMOR_API_KEY env var.
21
- """
22
- self.api_key = api_key or os.environ.get("PRISMOR_API_KEY")
23
- if not self.api_key:
24
- raise PrismorAPIError(
25
- "PRISMOR_API_KEY environment variable is not set. "
26
- "Please set it with: export PRISMOR_API_KEY=your_api_key"
27
- )
28
-
29
- self.base_url = "https://api.prismor.dev"
30
- self.headers = {
31
- "Authorization": f"Bearer {self.api_key}",
32
- "Content-Type": "application/json"
33
- }
34
-
35
- def normalize_repo_url(self, repo: str) -> str:
36
- """Normalize repository input to a full GitHub URL.
37
-
38
- Args:
39
- repo: Repository in format 'username/repo' or full GitHub URL
40
-
41
- Returns:
42
- Full GitHub repository URL
43
- """
44
- if repo.startswith("http://") or repo.startswith("https://"):
45
- return repo
46
-
47
- # Assume it's in username/repo format
48
- if "/" in repo:
49
- return f"https://github.com/{repo}"
50
-
51
- raise PrismorAPIError(
52
- f"Invalid repository format: {repo}. "
53
- "Please use 'username/repo' or full GitHub URL"
54
- )
55
-
56
- def scan(
57
- self,
58
- repo: str,
59
- vex: bool = False,
60
- sbom: bool = False,
61
- detect_secret: bool = False,
62
- fullscan: bool = False
63
- ) -> Dict[str, Any]:
64
- """Perform security scan on a GitHub repository.
65
-
66
- Args:
67
- repo: Repository URL or username/repo format
68
- vex: Enable vulnerability scanning
69
- sbom: Enable SBOM generation
70
- detect_secret: Enable secret detection
71
- fullscan: Enable all scan types
72
-
73
- Returns:
74
- Dictionary containing scan results
75
- """
76
- repo_url = self.normalize_repo_url(repo)
77
-
78
- # Prepare request payload
79
- payload = {
80
- "repo_url": repo_url,
81
- "vex": vex or fullscan,
82
- "sbom": sbom or fullscan,
83
- "detect_secret": detect_secret or fullscan,
84
- "fullscan": fullscan
85
- }
86
-
87
- try:
88
- response = requests.post(
89
- f"{self.base_url}/scan",
90
- json=payload,
91
- headers=self.headers,
92
- timeout=300 # 5 minute timeout
93
- )
94
-
95
- if response.status_code == 401:
96
- raise PrismorAPIError("Invalid API key. Please check your PRISMOR_API_KEY.")
97
-
98
- if response.status_code == 404:
99
- raise PrismorAPIError("API endpoint not found. Please check the API URL.")
100
-
101
- if response.status_code >= 400:
102
- error_msg = response.json().get("error", "Unknown error")
103
- raise PrismorAPIError(f"API error: {error_msg}")
104
-
105
- response.raise_for_status()
106
- return response.json()
107
-
108
- except requests.exceptions.Timeout:
109
- raise PrismorAPIError(
110
- "Request timed out. The repository scan is taking longer than expected."
111
- )
112
- except requests.exceptions.ConnectionError:
113
- raise PrismorAPIError(
114
- "Failed to connect to Prismor API. Please check your internet connection."
115
- )
116
- except requests.exceptions.RequestException as e:
117
- raise PrismorAPIError(f"Request failed: {str(e)}")
118
-
@@ -1,210 +0,0 @@
1
- """Command-line interface for Prismor security scanning tool."""
2
-
3
- import sys
4
- import json
5
- import click
6
- from typing import Optional
7
- from .api import PrismorClient, PrismorAPIError
8
-
9
-
10
- def print_success(message: str):
11
- """Print success message in green."""
12
- click.secho(f"✓ {message}", fg="green")
13
-
14
-
15
- def print_error(message: str):
16
- """Print error message in red."""
17
- click.secho(f"✗ {message}", fg="red", err=True)
18
-
19
-
20
- def print_info(message: str):
21
- """Print info message in blue."""
22
- click.secho(f"ℹ {message}", fg="blue")
23
-
24
-
25
- def print_warning(message: str):
26
- """Print warning message in yellow."""
27
- click.secho(f"⚠ {message}", fg="yellow")
28
-
29
-
30
- def format_scan_results(results: dict, scan_type: str):
31
- """Format and display scan results."""
32
- click.echo("\n" + "=" * 60)
33
- click.secho(f" Scan Results - {scan_type}", fg="cyan", bold=True)
34
- click.echo("=" * 60 + "\n")
35
-
36
- # Display repository information
37
- if "repository" in results:
38
- click.secho("Repository:", fg="yellow", bold=True)
39
- click.echo(f" {results['repository']}\n")
40
-
41
- # Display scan status
42
- if "status" in results:
43
- status_color = "green" if results["status"] == "success" else "red"
44
- click.secho(f"Status: ", fg="yellow", bold=True, nl=False)
45
- click.secho(results["status"], fg=status_color)
46
- click.echo()
47
-
48
- # Display scan results based on type
49
- if "scan_results" in results:
50
- scan_data = results["scan_results"]
51
-
52
- # Vulnerability scan results
53
- if "vulnerabilities" in scan_data or "Results" in scan_data:
54
- click.secho("Vulnerabilities Found:", fg="yellow", bold=True)
55
- vuln_data = scan_data.get("vulnerabilities", scan_data.get("Results", []))
56
- if isinstance(vuln_data, list):
57
- click.echo(f" Total: {len(vuln_data)}")
58
- else:
59
- click.echo(f" Data available in detailed output")
60
- click.echo()
61
-
62
- # Secret scan results
63
- if "secrets" in scan_data or "findings_summary" in scan_data:
64
- click.secho("Secrets Detected:", fg="yellow", bold=True)
65
- secrets = scan_data.get("secrets", scan_data.get("findings_summary", {}))
66
- if isinstance(secrets, dict):
67
- for key, value in secrets.items():
68
- click.echo(f" {key}: {value}")
69
- else:
70
- click.echo(f" {len(secrets) if isinstance(secrets, list) else 'Data available'}")
71
- click.echo()
72
-
73
- # SBOM results
74
- if "sbom" in scan_data or "artifacts" in scan_data:
75
- click.secho("SBOM Generated:", fg="yellow", bold=True)
76
- sbom_data = scan_data.get("sbom", scan_data.get("artifacts", []))
77
- if isinstance(sbom_data, list):
78
- click.echo(f" Total artifacts: {len(sbom_data)}")
79
- else:
80
- click.echo(f" SBOM data available")
81
- click.echo()
82
-
83
- # Display result URLs if available
84
- if "public_url" in results:
85
- click.secho("Results URL:", fg="yellow", bold=True)
86
- click.echo(f" {results['public_url']}\n")
87
-
88
- if "presigned_url" in results:
89
- click.secho("Download URL:", fg="yellow", bold=True)
90
- click.echo(f" {results['presigned_url']}\n")
91
-
92
- click.echo("=" * 60 + "\n")
93
-
94
-
95
- @click.group(invoke_without_command=True)
96
- @click.option(
97
- "--scan",
98
- type=str,
99
- help="Repository to scan (username/repo or full GitHub URL)"
100
- )
101
- @click.option("--vex", is_flag=True, help="Perform vulnerability scanning")
102
- @click.option("--sbom", is_flag=True, help="Generate Software Bill of Materials")
103
- @click.option("--detect-secret", is_flag=True, help="Detect secrets in repository")
104
- @click.option("--fullscan", is_flag=True, help="Perform all scan types")
105
- @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
106
- @click.version_option(version="0.1.0", prog_name="prismor")
107
- @click.pass_context
108
- def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
109
- fullscan: bool, output_json: bool):
110
- """Prismor CLI - Security scanning tool for GitHub repositories.
111
-
112
- Examples:
113
- prismor --scan username/repo --vex
114
- prismor --scan username/repo --fullscan
115
- prismor --scan https://github.com/username/repo --detect-secret
116
- """
117
- # If no command and no scan option, show help
118
- if ctx.invoked_subcommand is None and not scan:
119
- click.echo(ctx.get_help())
120
- return
121
-
122
- # If scan option is provided, perform the scan
123
- if scan:
124
- # Check if at least one scan type is selected
125
- if not any([vex, sbom, detect_secret, fullscan]):
126
- print_error("Please specify at least one scan type: --vex, --sbom, --detect-secret, or --fullscan")
127
- sys.exit(1)
128
-
129
- try:
130
- # Initialize API client
131
- print_info(f"Initializing Prismor scan for: {scan}")
132
- client = PrismorClient()
133
-
134
- # Determine scan type for display
135
- scan_types = []
136
- if fullscan:
137
- scan_types.append("Full Scan (VEX + SBOM + Secret Detection)")
138
- else:
139
- if vex:
140
- scan_types.append("VEX")
141
- if sbom:
142
- scan_types.append("SBOM")
143
- if detect_secret:
144
- scan_types.append("Secret Detection")
145
-
146
- print_info(f"Scan type: {', '.join(scan_types)}")
147
- print_info("Starting scan... (this may take a few minutes)")
148
-
149
- # Perform scan
150
- results = client.scan(
151
- repo=scan,
152
- vex=vex,
153
- sbom=sbom,
154
- detect_secret=detect_secret,
155
- fullscan=fullscan
156
- )
157
-
158
- # Output results
159
- if output_json:
160
- click.echo(json.dumps(results, indent=2))
161
- else:
162
- print_success("Scan completed successfully!")
163
- format_scan_results(results, ', '.join(scan_types))
164
-
165
- except PrismorAPIError as e:
166
- print_error(str(e))
167
- sys.exit(1)
168
- except Exception as e:
169
- print_error(f"Unexpected error: {str(e)}")
170
- sys.exit(1)
171
-
172
-
173
- @cli.command()
174
- def version():
175
- """Display the version of Prismor CLI."""
176
- click.echo("Prismor CLI v0.1.0")
177
-
178
-
179
- @cli.command()
180
- def config():
181
- """Display current configuration."""
182
- import os
183
-
184
- click.echo("\n" + "=" * 60)
185
- click.secho(" Prismor CLI Configuration", fg="cyan", bold=True)
186
- click.echo("=" * 60 + "\n")
187
-
188
- # Check API key
189
- api_key = os.environ.get("PRISMOR_API_KEY")
190
- if api_key:
191
- # Show only first and last 4 characters
192
- masked_key = f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else "***"
193
- print_success(f"PRISMOR_API_KEY: {masked_key}")
194
- else:
195
- print_error("PRISMOR_API_KEY: Not set")
196
- click.echo("\nTo set your API key, run:")
197
- click.echo(" export PRISMOR_API_KEY=your_api_key")
198
-
199
- click.echo("\nAPI Endpoint: https://api.prismor.dev")
200
- click.echo("=" * 60 + "\n")
201
-
202
-
203
- def main():
204
- """Entry point for the CLI."""
205
- cli()
206
-
207
-
208
- if __name__ == "__main__":
209
- main()
210
-
File without changes
File without changes
File without changes
File without changes
File without changes