prismor 0.1.2__py3-none-any.whl → 1.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.
- prismor/__init__.py +1 -1
- prismor/api.py +347 -22
- prismor/cli.py +354 -72
- prismor-1.1.1.dist-info/METADATA +744 -0
- prismor-1.1.1.dist-info/RECORD +9 -0
- {prismor-0.1.2.dist-info → prismor-1.1.1.dist-info}/WHEEL +1 -1
- prismor-0.1.2.dist-info/METADATA +0 -371
- prismor-0.1.2.dist-info/RECORD +0 -9
- {prismor-0.1.2.dist-info → prismor-1.1.1.dist-info}/entry_points.txt +0 -0
- {prismor-0.1.2.dist-info → prismor-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {prismor-0.1.2.dist-info → prismor-1.1.1.dist-info}/top_level.txt +0 -0
prismor/cli.py
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import sys
|
|
4
4
|
import json
|
|
5
5
|
import click
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
6
8
|
from typing import Optional
|
|
7
|
-
from .api import PrismorClient, PrismorAPIError
|
|
9
|
+
from .api import PrismorClient, PrismorAPIError, parse_github_repo
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def print_success(message: str):
|
|
@@ -27,6 +29,43 @@ def print_warning(message: str):
|
|
|
27
29
|
click.secho(f"⚠ {message}", fg="yellow")
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
class Spinner:
|
|
33
|
+
"""Simple spinner for showing loading state."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str = "Processing"):
|
|
36
|
+
self.message = message
|
|
37
|
+
self.spinner_chars = "|/-\\"
|
|
38
|
+
self.spinner_index = 0
|
|
39
|
+
self.running = False
|
|
40
|
+
self.thread = None
|
|
41
|
+
|
|
42
|
+
def _spin(self):
|
|
43
|
+
"""Internal spinner loop."""
|
|
44
|
+
while self.running:
|
|
45
|
+
char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)]
|
|
46
|
+
sys.stdout.write(f"\r{char} {self.message}...")
|
|
47
|
+
sys.stdout.flush()
|
|
48
|
+
self.spinner_index += 1
|
|
49
|
+
time.sleep(0.1)
|
|
50
|
+
|
|
51
|
+
def start(self):
|
|
52
|
+
"""Start the spinner."""
|
|
53
|
+
self.running = True
|
|
54
|
+
self.thread = threading.Thread(target=self._spin, daemon=True)
|
|
55
|
+
self.thread.start()
|
|
56
|
+
|
|
57
|
+
def stop(self, message: str = None):
|
|
58
|
+
"""Stop the spinner and clear the line."""
|
|
59
|
+
self.running = False
|
|
60
|
+
if self.thread:
|
|
61
|
+
self.thread.join(timeout=0.2)
|
|
62
|
+
# Clear the spinner line
|
|
63
|
+
sys.stdout.write("\r" + " " * (len(self.message) + 15) + "\r")
|
|
64
|
+
sys.stdout.flush()
|
|
65
|
+
if message:
|
|
66
|
+
click.echo(message)
|
|
67
|
+
|
|
68
|
+
|
|
30
69
|
def format_scan_results(results: dict, scan_type: str):
|
|
31
70
|
"""Format and display scan results."""
|
|
32
71
|
click.echo("\n" + "=" * 60)
|
|
@@ -117,113 +156,145 @@ def format_scan_results(results: dict, scan_type: str):
|
|
|
117
156
|
|
|
118
157
|
@click.group(invoke_without_command=True)
|
|
119
158
|
@click.option(
|
|
120
|
-
"--
|
|
159
|
+
"--repo",
|
|
160
|
+
"scan_repo",
|
|
121
161
|
type=str,
|
|
122
|
-
help="Repository to scan (username/repo
|
|
162
|
+
help="Repository to scan (username/repo, GitHub URL, SSH URL, etc.)"
|
|
123
163
|
)
|
|
124
|
-
@click.option("--
|
|
164
|
+
@click.option("--scan", is_flag=True, help="Perform vulnerability scanning")
|
|
125
165
|
@click.option("--sbom", is_flag=True, help="Generate Software Bill of Materials")
|
|
126
166
|
@click.option("--detect-secret", is_flag=True, help="Detect secrets in repository")
|
|
127
167
|
@click.option("--fullscan", is_flag=True, help="Perform all scan types")
|
|
128
168
|
@click.option("--branch", type=str, help="Specific branch to scan (defaults to main/master)")
|
|
129
169
|
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
130
|
-
@click.
|
|
170
|
+
@click.option("--output", "-o", type=click.Path(), help="Save results to file (JSON format)")
|
|
171
|
+
@click.option("--quiet", "-q", is_flag=True, help="Minimal output (only errors and final results)")
|
|
172
|
+
@click.version_option(version="1.1.1", prog_name="prismor")
|
|
131
173
|
@click.pass_context
|
|
132
|
-
def cli(ctx,
|
|
133
|
-
fullscan: bool, branch: Optional[str], output_json: bool):
|
|
174
|
+
def cli(ctx, scan_repo: Optional[str], scan: bool, sbom: bool, detect_secret: bool,
|
|
175
|
+
fullscan: bool, branch: Optional[str], output_json: bool, output: Optional[str], quiet: bool):
|
|
134
176
|
"""Prismor CLI - Security scanning tool for GitHub repositories.
|
|
135
177
|
|
|
136
178
|
Examples:
|
|
137
|
-
prismor --
|
|
138
|
-
prismor --
|
|
139
|
-
prismor --
|
|
140
|
-
prismor --
|
|
179
|
+
prismor --repo username/repo --scan
|
|
180
|
+
prismor --repo username/repo --fullscan
|
|
181
|
+
prismor --repo https://github.com/username/repo --detect-secret
|
|
182
|
+
prismor --repo git@github.com:username/repo.git --sbom
|
|
183
|
+
prismor --repo github.com/username/repo --fullscan --branch develop
|
|
141
184
|
prismor status
|
|
142
185
|
prismor repos
|
|
143
186
|
"""
|
|
144
187
|
# If no command and no scan option, show help
|
|
145
|
-
if ctx.invoked_subcommand is None and not
|
|
188
|
+
if ctx.invoked_subcommand is None and not scan_repo:
|
|
146
189
|
click.echo(ctx.get_help())
|
|
147
190
|
return
|
|
148
191
|
|
|
149
192
|
# If scan option is provided, perform the scan
|
|
150
|
-
if
|
|
193
|
+
if scan_repo:
|
|
151
194
|
# Check if at least one scan type is selected
|
|
152
|
-
if not any([
|
|
153
|
-
print_error("Please specify at least one scan type: --
|
|
195
|
+
if not any([scan, sbom, detect_secret, fullscan]):
|
|
196
|
+
print_error("Please specify at least one scan type: --scan, --sbom, --detect-secret, or --fullscan")
|
|
154
197
|
sys.exit(1)
|
|
155
198
|
|
|
156
199
|
try:
|
|
157
200
|
# Initialize API client
|
|
158
|
-
|
|
201
|
+
if not quiet:
|
|
202
|
+
print_info(f"Initializing Prismor scan for: {scan_repo}")
|
|
159
203
|
client = PrismorClient()
|
|
160
204
|
|
|
161
205
|
# Determine scan type for display
|
|
162
206
|
scan_types = []
|
|
163
207
|
if fullscan:
|
|
164
|
-
scan_types.append("Full Scan (
|
|
208
|
+
scan_types.append("Full Scan (scan + SBOM + Secret Detection)")
|
|
165
209
|
else:
|
|
166
|
-
if
|
|
167
|
-
scan_types.append("
|
|
210
|
+
if scan:
|
|
211
|
+
scan_types.append("scan")
|
|
168
212
|
if sbom:
|
|
169
213
|
scan_types.append("SBOM")
|
|
170
214
|
if detect_secret:
|
|
171
215
|
scan_types.append("Secret Detection")
|
|
172
216
|
|
|
173
|
-
|
|
174
|
-
|
|
217
|
+
if not quiet:
|
|
218
|
+
print_info(f"Scan type: {', '.join(scan_types)}")
|
|
219
|
+
if scan or fullscan:
|
|
220
|
+
print_info("Starting scan... (vulnerability scans run asynchronously and may take up to 10 minutes)")
|
|
221
|
+
else:
|
|
222
|
+
print_info("Starting scan... (this may take a few minutes)")
|
|
175
223
|
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
# Show loading spinner during scan (unless quiet mode)
|
|
225
|
+
spinner = None
|
|
226
|
+
if not quiet and not output_json and not output:
|
|
227
|
+
spinner = Spinner("Scanning repository")
|
|
228
|
+
spinner.start()
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Perform scan
|
|
232
|
+
results = client.scan(
|
|
233
|
+
repo=scan_repo,
|
|
234
|
+
scan=scan,
|
|
235
|
+
sbom=sbom,
|
|
236
|
+
detect_secret=detect_secret,
|
|
237
|
+
fullscan=fullscan,
|
|
238
|
+
branch=branch
|
|
239
|
+
)
|
|
240
|
+
if spinner:
|
|
241
|
+
spinner.stop()
|
|
242
|
+
except Exception as e:
|
|
243
|
+
if spinner:
|
|
244
|
+
spinner.stop()
|
|
245
|
+
raise e
|
|
246
|
+
|
|
247
|
+
# Save to file if --output specified
|
|
248
|
+
if output:
|
|
249
|
+
try:
|
|
250
|
+
with open(output, 'w') as f:
|
|
251
|
+
json.dump(results, f, indent=2)
|
|
252
|
+
if not quiet:
|
|
253
|
+
print_success(f"Results saved to: {output}")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print_error(f"Failed to save results to file: {str(e)}")
|
|
256
|
+
sys.exit(1)
|
|
185
257
|
|
|
186
258
|
# Output results
|
|
187
|
-
if output_json:
|
|
188
|
-
|
|
259
|
+
if output_json or output:
|
|
260
|
+
if not output: # Only print to stdout if not saving to file
|
|
261
|
+
click.echo(json.dumps(results, indent=2))
|
|
189
262
|
else:
|
|
190
|
-
|
|
263
|
+
if not quiet:
|
|
264
|
+
print_success("Scan completed successfully!")
|
|
191
265
|
format_scan_results(results, ', '.join(scan_types))
|
|
192
266
|
|
|
193
|
-
# Try to get repository ID and display dashboard link
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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}"
|
|
267
|
+
# Try to get repository ID and display dashboard link (unless quiet mode)
|
|
268
|
+
if not quiet:
|
|
269
|
+
try:
|
|
270
|
+
# Extract repo name from scan input using the comprehensive parser
|
|
271
|
+
repo_name = parse_github_repo(scan_repo)
|
|
207
272
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
273
|
+
# Get repository ID
|
|
274
|
+
repo_info = client.get_repository_by_name(repo_name)
|
|
275
|
+
if repo_info.get("success") and "repository" in repo_info:
|
|
276
|
+
repo_id = repo_info["repository"]["id"]
|
|
277
|
+
dashboard_url = f"https://prismor.dev/repositories/{repo_id}"
|
|
278
|
+
|
|
279
|
+
click.echo("\n" + "=" * 60)
|
|
280
|
+
click.secho(" 📊 Dashboard Analysis", fg="cyan", bold=True)
|
|
281
|
+
click.echo("=" * 60)
|
|
282
|
+
click.secho(f"🔗 View detailed analysis and insights:", fg="blue")
|
|
283
|
+
click.secho(f" {dashboard_url}", fg="green", bold=True)
|
|
284
|
+
click.echo("\n💡 The dashboard provides:")
|
|
285
|
+
click.echo(" • Interactive visualizations and charts")
|
|
286
|
+
click.echo(" • Historical vulnerability trends")
|
|
287
|
+
click.echo(" • Detailed security reports")
|
|
288
|
+
click.echo(" • Team collaboration features")
|
|
289
|
+
click.echo(" • Export capabilities")
|
|
290
|
+
click.echo("=" * 60 + "\n")
|
|
291
|
+
|
|
292
|
+
except PrismorAPIError as e:
|
|
293
|
+
# Repository might not be found, continue without dashboard link
|
|
294
|
+
print_warning(f"Could not generate dashboard link: {str(e)}")
|
|
295
|
+
except Exception as e:
|
|
296
|
+
# Any other error, continue without dashboard link
|
|
297
|
+
print_warning(f"Could not generate dashboard link: {str(e)}")
|
|
227
298
|
|
|
228
299
|
except PrismorAPIError as e:
|
|
229
300
|
print_error(str(e))
|
|
@@ -236,7 +307,7 @@ def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
|
|
|
236
307
|
@cli.command()
|
|
237
308
|
def version():
|
|
238
309
|
"""Display the version of Prismor CLI."""
|
|
239
|
-
click.echo("Prismor CLI
|
|
310
|
+
click.echo("Prismor CLI v1.1.0")
|
|
240
311
|
|
|
241
312
|
|
|
242
313
|
@cli.command()
|
|
@@ -256,6 +327,8 @@ def config():
|
|
|
256
327
|
print_success(f"PRISMOR_API_KEY: {masked_key}")
|
|
257
328
|
else:
|
|
258
329
|
print_error("PRISMOR_API_KEY: Not set")
|
|
330
|
+
click.echo("\nPlease specify your API key. You can generate one for free at:")
|
|
331
|
+
click.secho(" https://www.prismor.dev/cli", fg="cyan", underline=True)
|
|
259
332
|
click.echo("\nTo set your API key, run:")
|
|
260
333
|
click.echo(" export PRISMOR_API_KEY=your_api_key")
|
|
261
334
|
|
|
@@ -268,7 +341,14 @@ def repos():
|
|
|
268
341
|
"""List your connected repositories."""
|
|
269
342
|
try:
|
|
270
343
|
client = PrismorClient()
|
|
271
|
-
|
|
344
|
+
spinner = Spinner("Loading repositories")
|
|
345
|
+
spinner.start()
|
|
346
|
+
try:
|
|
347
|
+
repos_data = client.get_repositories()
|
|
348
|
+
spinner.stop()
|
|
349
|
+
except Exception as e:
|
|
350
|
+
spinner.stop()
|
|
351
|
+
raise e
|
|
272
352
|
|
|
273
353
|
click.echo("\n" + "=" * 60)
|
|
274
354
|
click.secho(" Your Repositories", fg="cyan", bold=True)
|
|
@@ -299,12 +379,216 @@ def repos():
|
|
|
299
379
|
sys.exit(1)
|
|
300
380
|
|
|
301
381
|
|
|
382
|
+
@cli.command()
|
|
383
|
+
@click.argument("repo", type=str)
|
|
384
|
+
@click.option("--branch", type=str, help="Specific branch to scan (defaults to main)")
|
|
385
|
+
@click.option("--token", type=str, help="GitHub token (or set GITHUB_TOKEN env var)")
|
|
386
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
387
|
+
def start_scan(repo: str, branch: Optional[str], token: Optional[str], output_json: bool):
|
|
388
|
+
"""Start a vulnerability scan and return a job_id for status checking.
|
|
389
|
+
|
|
390
|
+
REPO is the repository to scan (username/repo, GitHub URL, SSH URL, etc.)
|
|
391
|
+
|
|
392
|
+
This command starts a scan asynchronously and returns immediately with a job_id.
|
|
393
|
+
Use 'prismor scan-status <job_id>' to check when the scan completes.
|
|
394
|
+
|
|
395
|
+
Note: Requires GitHub token. Set GITHUB_TOKEN environment variable or use --token option.
|
|
396
|
+
|
|
397
|
+
Examples:
|
|
398
|
+
prismor start-scan username/repo
|
|
399
|
+
prismor start-scan https://github.com/username/repo --branch develop
|
|
400
|
+
prismor start-scan username/repo --token ghp_xxxxx
|
|
401
|
+
prismor start-scan username/repo --json
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
client = PrismorClient()
|
|
405
|
+
print_info(f"Starting vulnerability scan for: {repo}")
|
|
406
|
+
if branch:
|
|
407
|
+
print_info(f"Branch: {branch}")
|
|
408
|
+
|
|
409
|
+
spinner = Spinner("Starting scan")
|
|
410
|
+
spinner.start()
|
|
411
|
+
try:
|
|
412
|
+
result = client.start_vulnerability_scan(repo, branch, token)
|
|
413
|
+
spinner.stop()
|
|
414
|
+
except Exception as e:
|
|
415
|
+
spinner.stop()
|
|
416
|
+
raise e
|
|
417
|
+
|
|
418
|
+
if output_json:
|
|
419
|
+
click.echo(json.dumps(result, indent=2))
|
|
420
|
+
else:
|
|
421
|
+
click.echo("\n" + "=" * 60)
|
|
422
|
+
click.secho(" Scan Started", fg="cyan", bold=True)
|
|
423
|
+
click.echo("=" * 60 + "\n")
|
|
424
|
+
|
|
425
|
+
job_id = result.get("job_id")
|
|
426
|
+
if job_id:
|
|
427
|
+
print_success(f"Scan started successfully!")
|
|
428
|
+
click.echo()
|
|
429
|
+
click.secho(f"Job ID: {job_id}", fg="yellow", bold=True)
|
|
430
|
+
click.echo()
|
|
431
|
+
click.secho("Repository:", fg="yellow", bold=True)
|
|
432
|
+
click.echo(f" {result.get('repository', repo)}")
|
|
433
|
+
click.echo()
|
|
434
|
+
if "branch" in result:
|
|
435
|
+
click.secho("Branch:", fg="yellow", bold=True)
|
|
436
|
+
click.echo(f" {result['branch']}")
|
|
437
|
+
click.echo()
|
|
438
|
+
click.secho("Status:", fg="yellow", bold=True)
|
|
439
|
+
click.echo(f" {result.get('status', 'accepted')}")
|
|
440
|
+
click.echo()
|
|
441
|
+
click.secho("Next Steps:", fg="cyan", bold=True)
|
|
442
|
+
click.echo(f" Check scan status with:")
|
|
443
|
+
click.secho(f" prismor scan-status {job_id}", fg="green", bold=True)
|
|
444
|
+
click.echo()
|
|
445
|
+
else:
|
|
446
|
+
print_error("Failed to get job_id from response")
|
|
447
|
+
click.echo(json.dumps(result, indent=2))
|
|
448
|
+
|
|
449
|
+
click.echo("=" * 60 + "\n")
|
|
450
|
+
|
|
451
|
+
except PrismorAPIError as e:
|
|
452
|
+
print_error(str(e))
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
except Exception as e:
|
|
455
|
+
print_error(f"Unexpected error: {str(e)}")
|
|
456
|
+
sys.exit(1)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@cli.command()
|
|
460
|
+
@click.argument("job_id", type=str)
|
|
461
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
462
|
+
def scan_status(job_id: str, output_json: bool):
|
|
463
|
+
"""Check the status of a vulnerability scan job.
|
|
464
|
+
|
|
465
|
+
JOB_ID is the job ID returned when starting a scan.
|
|
466
|
+
|
|
467
|
+
Examples:
|
|
468
|
+
prismor scan-status a724a4663cda4bf087ad171683cb726d
|
|
469
|
+
prismor scan-status 50cbe253e5634227b81fe744c2a0b3e7 --json
|
|
470
|
+
"""
|
|
471
|
+
try:
|
|
472
|
+
client = PrismorClient()
|
|
473
|
+
print_info(f"Checking scan status for job: {job_id}")
|
|
474
|
+
|
|
475
|
+
spinner = Spinner("Checking status")
|
|
476
|
+
spinner.start()
|
|
477
|
+
try:
|
|
478
|
+
status_data = client.check_scan_status(job_id)
|
|
479
|
+
spinner.stop()
|
|
480
|
+
except Exception as e:
|
|
481
|
+
spinner.stop()
|
|
482
|
+
raise e
|
|
483
|
+
|
|
484
|
+
if output_json:
|
|
485
|
+
click.echo(json.dumps(status_data, indent=2))
|
|
486
|
+
else:
|
|
487
|
+
click.echo("\n" + "=" * 60)
|
|
488
|
+
click.secho(" Scan Status", fg="cyan", bold=True)
|
|
489
|
+
click.echo("=" * 60 + "\n")
|
|
490
|
+
|
|
491
|
+
click.secho(f"Job ID: {status_data.get('job_id', job_id)}", fg="yellow", bold=True)
|
|
492
|
+
click.echo()
|
|
493
|
+
|
|
494
|
+
status = status_data.get("status", "unknown")
|
|
495
|
+
if status == "completed":
|
|
496
|
+
print_success(f"Status: {status}")
|
|
497
|
+
click.echo()
|
|
498
|
+
|
|
499
|
+
if "repository" in status_data:
|
|
500
|
+
click.secho("Repository:", fg="yellow", bold=True)
|
|
501
|
+
click.echo(f" {status_data['repository']}")
|
|
502
|
+
click.echo()
|
|
503
|
+
|
|
504
|
+
if "branch" in status_data:
|
|
505
|
+
click.secho("Branch:", fg="yellow", bold=True)
|
|
506
|
+
click.echo(f" {status_data['branch']}")
|
|
507
|
+
click.echo()
|
|
508
|
+
|
|
509
|
+
if "duration" in status_data:
|
|
510
|
+
click.secho("Duration:", fg="yellow", bold=True)
|
|
511
|
+
click.echo(f" {status_data['duration']:.2f} seconds")
|
|
512
|
+
click.echo()
|
|
513
|
+
|
|
514
|
+
if "public_url" in status_data:
|
|
515
|
+
click.secho("Results URL:", fg="yellow", bold=True)
|
|
516
|
+
click.secho(f" {status_data['public_url']}", fg="green")
|
|
517
|
+
click.echo()
|
|
518
|
+
|
|
519
|
+
if "presigned_url" in status_data:
|
|
520
|
+
click.secho("Presigned URL (expires in 1 hour):", fg="yellow", bold=True)
|
|
521
|
+
click.secho(f" {status_data['presigned_url']}", fg="blue")
|
|
522
|
+
click.echo()
|
|
523
|
+
|
|
524
|
+
# Display vulnerability summary if available
|
|
525
|
+
if "summary" in status_data:
|
|
526
|
+
summary = status_data["summary"]
|
|
527
|
+
click.secho("Vulnerability Summary:", fg="yellow", bold=True)
|
|
528
|
+
click.echo(f" Total Vulnerabilities: {summary.get('total_vulnerabilities', 0)}")
|
|
529
|
+
click.echo(f" Total Targets Scanned: {summary.get('total_targets', 0)}")
|
|
530
|
+
click.echo()
|
|
531
|
+
|
|
532
|
+
severity_breakdown = summary.get('severity_breakdown', {})
|
|
533
|
+
if severity_breakdown:
|
|
534
|
+
click.secho(" Severity Breakdown:", fg="yellow")
|
|
535
|
+
if severity_breakdown.get('CRITICAL', 0) > 0:
|
|
536
|
+
click.secho(f" CRITICAL: {severity_breakdown['CRITICAL']}", fg="red", bold=True)
|
|
537
|
+
if severity_breakdown.get('HIGH', 0) > 0:
|
|
538
|
+
click.secho(f" HIGH: {severity_breakdown['HIGH']}", fg="red")
|
|
539
|
+
if severity_breakdown.get('MEDIUM', 0) > 0:
|
|
540
|
+
click.secho(f" MEDIUM: {severity_breakdown['MEDIUM']}", fg="yellow")
|
|
541
|
+
if severity_breakdown.get('LOW', 0) > 0:
|
|
542
|
+
click.secho(f" LOW: {severity_breakdown['LOW']}", fg="blue")
|
|
543
|
+
if severity_breakdown.get('UNKNOWN', 0) > 0:
|
|
544
|
+
click.secho(f" UNKNOWN: {severity_breakdown['UNKNOWN']}", fg="white")
|
|
545
|
+
click.echo()
|
|
546
|
+
|
|
547
|
+
if "scan_date" in status_data:
|
|
548
|
+
click.secho("Scan Date:", fg="yellow", bold=True)
|
|
549
|
+
click.echo(f" {status_data['scan_date']}")
|
|
550
|
+
click.echo()
|
|
551
|
+
|
|
552
|
+
elif status == "running":
|
|
553
|
+
print_info(f"Status: {status}")
|
|
554
|
+
if "message" in status_data:
|
|
555
|
+
click.echo(f" {status_data['message']}")
|
|
556
|
+
click.echo()
|
|
557
|
+
click.echo("The scan is still in progress. Check back in a few moments.")
|
|
558
|
+
click.echo()
|
|
559
|
+
|
|
560
|
+
elif status == "failed":
|
|
561
|
+
print_error(f"Status: {status}")
|
|
562
|
+
if "error" in status_data:
|
|
563
|
+
click.echo(f" Error: {status_data['error']}")
|
|
564
|
+
click.echo()
|
|
565
|
+
else:
|
|
566
|
+
click.secho(f"Status: {status}", fg="yellow")
|
|
567
|
+
click.echo()
|
|
568
|
+
|
|
569
|
+
click.echo("=" * 60 + "\n")
|
|
570
|
+
|
|
571
|
+
except PrismorAPIError as e:
|
|
572
|
+
print_error(str(e))
|
|
573
|
+
sys.exit(1)
|
|
574
|
+
except Exception as e:
|
|
575
|
+
print_error(f"Unexpected error: {str(e)}")
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
|
|
578
|
+
|
|
302
579
|
@cli.command()
|
|
303
580
|
def status():
|
|
304
581
|
"""Check your account status and GitHub integration."""
|
|
305
582
|
try:
|
|
306
583
|
client = PrismorClient()
|
|
307
|
-
|
|
584
|
+
spinner = Spinner("Checking account status")
|
|
585
|
+
spinner.start()
|
|
586
|
+
try:
|
|
587
|
+
auth_data = client.authenticate()
|
|
588
|
+
spinner.stop()
|
|
589
|
+
except Exception as e:
|
|
590
|
+
spinner.stop()
|
|
591
|
+
raise e
|
|
308
592
|
|
|
309
593
|
click.echo("\n" + "=" * 60)
|
|
310
594
|
click.secho(" Account Status", fg="cyan", bold=True)
|
|
@@ -312,9 +596,7 @@ def status():
|
|
|
312
596
|
|
|
313
597
|
user_info = auth_data.get("user", {})
|
|
314
598
|
if user_info:
|
|
315
|
-
|
|
316
|
-
click.echo()
|
|
317
|
-
|
|
599
|
+
|
|
318
600
|
repositories = user_info.get("repositories", [])
|
|
319
601
|
click.secho(f"Connected Repositories: {len(repositories)}", fg="green")
|
|
320
602
|
|