prismor 0.1.2__py3-none-any.whl → 1.0.5__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """Prismor CLI - Security scanning tool for GitHub repositories."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "1.0.4"
4
4
  __author__ = "Prismor"
5
5
  __description__ = "A CLI tool for scanning GitHub repositories for vulnerabilities, secrets, and generating SBOMs"
6
6
 
prismor/api.py CHANGED
@@ -1,8 +1,10 @@
1
1
  """API client for Prismor security scanning service."""
2
2
 
3
3
  import os
4
+ import re
4
5
  import requests
5
6
  from typing import Optional, Dict, Any
7
+ from urllib.parse import urlparse
6
8
 
7
9
 
8
10
  class PrismorAPIError(Exception):
@@ -10,6 +12,137 @@ class PrismorAPIError(Exception):
10
12
  pass
11
13
 
12
14
 
15
+ def parse_github_repo(repo_input: str) -> str:
16
+ """Extract user/repo_name from various GitHub URL formats or return as-is if already in correct format.
17
+
18
+ This function handles multiple GitHub URL formats:
19
+ - user/repo_name (already in correct format)
20
+ - https://github.com/user/repo_name
21
+ - https://www.github.com/user/repo_name
22
+ - http://github.com/user/repo_name
23
+ - http://www.github.com/user/repo_name
24
+ - github.com/user/repo_name
25
+ - www.github.com/user/repo_name
26
+ - git@github.com:user/repo_name.git
27
+ - https://github.com/user/repo_name.git
28
+ - https://github.com/user/repo_name/
29
+ - https://github.com/user/repo_name#branch
30
+ - https://github.com/user/repo_name/tree/branch
31
+ - https://github.com/user/repo_name/blob/branch/file
32
+
33
+ Args:
34
+ repo_input: Repository input in any of the supported formats
35
+
36
+ Returns:
37
+ Repository in "user/repo_name" format
38
+
39
+ Raises:
40
+ PrismorAPIError: If the input format is not recognized or invalid
41
+ """
42
+ if not repo_input or not isinstance(repo_input, str):
43
+ raise PrismorAPIError("Repository input cannot be empty")
44
+
45
+ repo_input = repo_input.strip()
46
+
47
+ # If it's already in user/repo format (no protocol, no domain), return as-is
48
+ if "/" in repo_input and not any(repo_input.startswith(prefix) for prefix in
49
+ ["http://", "https://", "git@", "github.com", "www.github.com"]):
50
+ # Validate it has exactly one slash and both parts are non-empty
51
+ parts = repo_input.split("/")
52
+ if len(parts) == 2 and parts[0] and parts[1]:
53
+ return repo_input
54
+ else:
55
+ raise PrismorAPIError(f"Invalid repository format: {repo_input}. Expected 'user/repo_name'")
56
+
57
+ # Handle SSH format: git@github.com:user/repo.git
58
+ if repo_input.startswith("git@github.com:"):
59
+ repo_part = repo_input[15:] # Remove "git@github.com:"
60
+ # Remove .git suffix if present
61
+ if repo_part.endswith(".git"):
62
+ repo_part = repo_part[:-4]
63
+ if "/" in repo_part:
64
+ return repo_part
65
+ else:
66
+ raise PrismorAPIError(f"Invalid SSH repository format: {repo_input}")
67
+
68
+ # Handle HTTP/HTTPS URLs
69
+ if repo_input.startswith(("http://", "https://")):
70
+ try:
71
+ parsed = urlparse(repo_input)
72
+ hostname = parsed.hostname.lower()
73
+
74
+ # Check if it's a GitHub URL
75
+ if hostname in ["github.com", "www.github.com"]:
76
+ path = parsed.path.strip("/")
77
+
78
+ # Remove .git suffix if present
79
+ if path.endswith(".git"):
80
+ path = path[:-4]
81
+
82
+ # Split path and extract user/repo
83
+ path_parts = path.split("/")
84
+ if len(path_parts) >= 2:
85
+ user = path_parts[0]
86
+ repo = path_parts[1]
87
+
88
+ # Handle special GitHub paths like /tree/branch, /blob/branch/file
89
+ if len(path_parts) > 2 and path_parts[2] in ["tree", "blob"]:
90
+ # This is a branch/file reference, just take user/repo
91
+ pass
92
+
93
+ if user and repo:
94
+ return f"{user}/{repo}"
95
+ else:
96
+ raise PrismorAPIError(f"Invalid GitHub URL format: {repo_input}")
97
+ else:
98
+ raise PrismorAPIError(f"Invalid GitHub URL format: {repo_input}")
99
+ else:
100
+ raise PrismorAPIError(f"Not a GitHub URL: {repo_input}")
101
+
102
+ except Exception as e:
103
+ raise PrismorAPIError(f"Failed to parse URL: {repo_input}. Error: {str(e)}")
104
+
105
+ # Handle bare domain formats: github.com/user/repo or www.github.com/user/repo
106
+ if repo_input.startswith(("github.com/", "www.github.com/")):
107
+ # Remove domain prefix
108
+ if repo_input.startswith("github.com/"):
109
+ repo_part = repo_input[11:] # Remove "github.com/"
110
+ else: # www.github.com/
111
+ repo_part = repo_input[15:] # Remove "www.github.com/"
112
+
113
+ # Remove .git suffix if present
114
+ if repo_part.endswith(".git"):
115
+ repo_part = repo_part[:-4]
116
+
117
+ # Remove trailing slash
118
+ repo_part = repo_part.rstrip("/")
119
+
120
+ # Split and validate
121
+ parts = repo_part.split("/")
122
+ if len(parts) >= 2:
123
+ user = parts[0]
124
+ repo = parts[1]
125
+
126
+ # Handle special GitHub paths like /tree/branch, /blob/branch/file
127
+ if len(parts) > 2 and parts[2] in ["tree", "blob"]:
128
+ # This is a branch/file reference, just take user/repo
129
+ pass
130
+
131
+ if user and repo:
132
+ return f"{user}/{repo}"
133
+ else:
134
+ raise PrismorAPIError(f"Invalid repository format: {repo_input}")
135
+ else:
136
+ raise PrismorAPIError(f"Invalid repository format: {repo_input}")
137
+
138
+ # If we get here, the format is not recognized
139
+ raise PrismorAPIError(
140
+ f"Unrecognized repository format: {repo_input}. "
141
+ "Supported formats: 'user/repo', 'https://github.com/user/repo', "
142
+ "'git@github.com:user/repo.git', or 'github.com/user/repo'"
143
+ )
144
+
145
+
13
146
  class PrismorClient:
14
147
  """Client for interacting with Prismor API."""
15
148
 
@@ -23,7 +156,8 @@ class PrismorClient:
23
156
  if not self.api_key:
24
157
  raise PrismorAPIError(
25
158
  "PRISMOR_API_KEY environment variable is not set. "
26
- "Please set it with: export PRISMOR_API_KEY=your_api_key"
159
+ "Please specify your API key. You can generate one for free at https://www.prismor.dev/cli\n"
160
+ "Set it with: export PRISMOR_API_KEY=your_api_key"
27
161
  )
28
162
 
29
163
  # self.base_url = "http://localhost:3000"
@@ -76,27 +210,21 @@ class PrismorClient:
76
210
  """Normalize repository input to a full GitHub URL.
77
211
 
78
212
  Args:
79
- repo: Repository in format 'username/repo' or full GitHub URL
213
+ repo: Repository in various formats (username/repo, GitHub URL, etc.)
80
214
 
81
215
  Returns:
82
216
  Full GitHub repository URL
83
217
  """
84
- if repo.startswith("http://") or repo.startswith("https://"):
85
- return repo
218
+ # Use the comprehensive parser to extract user/repo_name
219
+ repo_name = parse_github_repo(repo)
86
220
 
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
- )
221
+ # Convert to full GitHub URL
222
+ return f"https://github.com/{repo_name}"
95
223
 
96
224
  def scan(
97
225
  self,
98
226
  repo: str,
99
- vex: bool = False,
227
+ scan: bool = False,
100
228
  sbom: bool = False,
101
229
  detect_secret: bool = False,
102
230
  fullscan: bool = False,
@@ -106,7 +234,7 @@ class PrismorClient:
106
234
 
107
235
  Args:
108
236
  repo: Repository URL or username/repo format
109
- vex: Enable vulnerability scanning
237
+ scan: Enable vulnerability scanning
110
238
  sbom: Enable SBOM generation
111
239
  detect_secret: Enable secret detection
112
240
  fullscan: Enable all scan types
@@ -125,7 +253,7 @@ class PrismorClient:
125
253
  payload = {
126
254
  "repo_url": repo_url,
127
255
  "api_key": self.api_key,
128
- "vex": vex or fullscan,
256
+ "scan": scan or fullscan,
129
257
  "sbom": sbom or fullscan,
130
258
  "detect_secret": detect_secret or fullscan,
131
259
  "fullscan": fullscan,
@@ -133,11 +261,13 @@ class PrismorClient:
133
261
  }
134
262
 
135
263
  try:
264
+ # Note: Vulnerability scans now run asynchronously and can take up to 10 minutes
265
+ # The web API handles polling internally, so we just need a longer timeout
136
266
  response = requests.post(
137
267
  f"{self.base_url}/api/cli/scan",
138
268
  json=payload,
139
269
  headers={"Content-Type": "application/json"},
140
- timeout=300 # 5 minute timeout
270
+ timeout=600 # 10 minute timeout to accommodate async vulnerability scans
141
271
  )
142
272
 
143
273
  if response.status_code == 401:
@@ -166,8 +296,123 @@ class PrismorClient:
166
296
 
167
297
  except requests.exceptions.Timeout:
168
298
  raise PrismorAPIError(
169
- "Request timed out. The repository scan is taking longer than expected."
299
+ "Request timed out. The repository scan is taking longer than expected. "
300
+ "Large repositories may require more time. Please try again or check the dashboard for results."
301
+ )
302
+ except requests.exceptions.ConnectionError:
303
+ raise PrismorAPIError(
304
+ "Failed to connect to Prismor API. Please check your internet connection."
305
+ )
306
+ except requests.exceptions.RequestException as e:
307
+ raise PrismorAPIError(f"Request failed: {str(e)}")
308
+
309
+ def start_vulnerability_scan(
310
+ self,
311
+ repo: str,
312
+ branch: Optional[str] = None,
313
+ github_token: Optional[str] = None
314
+ ) -> Dict[str, Any]:
315
+ """Start a vulnerability scan and return immediately with a job_id.
316
+
317
+ This method directly calls the backend API to start an async scan.
318
+ Use check_scan_status() to poll for completion.
319
+
320
+ Args:
321
+ repo: Repository URL or username/repo format
322
+ branch: Specific branch to scan (defaults to main)
323
+ github_token: Optional GitHub token. If not provided, will try to get from env var GITHUB_TOKEN
324
+
325
+ Returns:
326
+ Dictionary containing job_id and status information
327
+
328
+ Raises:
329
+ PrismorAPIError: If request fails
330
+ """
331
+ # Directly call the backend API
332
+ backend_url = os.environ.get(
333
+ "PRISMOR_BACKEND_URL",
334
+ "https://2dlxuia6i5.execute-api.us-east-1.amazonaws.com/prod"
335
+ )
336
+
337
+ repo_url = self.normalize_repo_url(repo)
338
+
339
+ # Get GitHub token from parameter, env var, or raise error
340
+ gh_token = github_token or os.environ.get("GITHUB_TOKEN")
341
+
342
+ if not gh_token:
343
+ raise PrismorAPIError(
344
+ "GitHub token required. Provide it as a parameter, set GITHUB_TOKEN environment variable, "
345
+ "or use 'prismor --scan <repo> --scan' which handles authentication automatically."
346
+ )
347
+
348
+ try:
349
+ response = requests.post(
350
+ f"{backend_url}/scan",
351
+ json={
352
+ "repo_url": repo_url,
353
+ "token": gh_token,
354
+ "branch": branch or "main"
355
+ },
356
+ headers={"Content-Type": "application/json"},
357
+ timeout=30
358
+ )
359
+
360
+ if response.status_code >= 400:
361
+ error_msg = response.json().get("error", "Unknown error")
362
+ raise PrismorAPIError(f"API error: {error_msg}")
363
+
364
+ response.raise_for_status()
365
+ return response.json()
366
+
367
+ except requests.exceptions.Timeout:
368
+ raise PrismorAPIError("Request timed out.")
369
+ except requests.exceptions.ConnectionError:
370
+ raise PrismorAPIError(
371
+ "Failed to connect to Prismor API. Please check your internet connection."
372
+ )
373
+ except requests.exceptions.RequestException as e:
374
+ raise PrismorAPIError(f"Request failed: {str(e)}")
375
+
376
+ def check_scan_status(self, job_id: str) -> Dict[str, Any]:
377
+ """Check the status of a vulnerability scan job.
378
+
379
+ This method directly calls the backend API to check scan status.
380
+ Use this when you have a job_id from starting an async scan.
381
+
382
+ Args:
383
+ job_id: The job ID returned from starting a scan
384
+
385
+ Returns:
386
+ Dictionary containing scan status and results if completed
387
+
388
+ Raises:
389
+ PrismorAPIError: If request fails
390
+ """
391
+ # Directly call the backend API for status check
392
+ backend_url = os.environ.get(
393
+ "PRISMOR_BACKEND_URL",
394
+ "https://2dlxuia6i5.execute-api.us-east-1.amazonaws.com/prod"
395
+ )
396
+
397
+ try:
398
+ response = requests.get(
399
+ f"{backend_url}/scan/status/{job_id}",
400
+ headers={"Content-Type": "application/json"},
401
+ timeout=30
170
402
  )
403
+
404
+ if response.status_code == 404:
405
+ raise PrismorAPIError(f"Scan job '{job_id}' not found.")
406
+
407
+ if response.status_code >= 400:
408
+ error_msg = response.json().get("error", "Unknown error")
409
+ raise PrismorAPIError(f"API error: {error_msg}")
410
+
411
+ response.raise_for_status()
412
+ return response.json()
413
+
414
+ except requests.exceptions.Timeout:
415
+ raise PrismorAPIError("Request timed out.")
171
416
  except requests.exceptions.ConnectionError:
172
417
  raise PrismorAPIError(
173
418
  "Failed to connect to Prismor API. Please check your internet connection."
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,71 +156,85 @@ def format_scan_results(results: dict, scan_type: str):
117
156
 
118
157
  @click.group(invoke_without_command=True)
119
158
  @click.option(
120
- "--scan",
159
+ "--repo",
160
+ "scan_repo",
121
161
  type=str,
122
- help="Repository to scan (username/repo or full GitHub URL)"
162
+ help="Repository to scan (username/repo, GitHub URL, SSH URL, etc.)"
123
163
  )
124
- @click.option("--vex", is_flag=True, help="Perform vulnerability scanning")
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.version_option(version="0.1.2", prog_name="prismor")
170
+ @click.version_option(version="1.0.5", prog_name="prismor")
131
171
  @click.pass_context
132
- def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
172
+ def cli(ctx, scan_repo: Optional[str], scan: bool, sbom: bool, detect_secret: bool,
133
173
  fullscan: bool, branch: Optional[str], output_json: bool):
134
174
  """Prismor CLI - Security scanning tool for GitHub repositories.
135
175
 
136
176
  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
177
+ prismor --repo username/repo --scan
178
+ prismor --repo username/repo --fullscan
179
+ prismor --repo https://github.com/username/repo --detect-secret
180
+ prismor --repo git@github.com:username/repo.git --sbom
181
+ prismor --repo github.com/username/repo --fullscan --branch develop
141
182
  prismor status
142
183
  prismor repos
143
184
  """
144
185
  # If no command and no scan option, show help
145
- if ctx.invoked_subcommand is None and not scan:
186
+ if ctx.invoked_subcommand is None and not scan_repo:
146
187
  click.echo(ctx.get_help())
147
188
  return
148
189
 
149
190
  # If scan option is provided, perform the scan
150
- if scan:
191
+ if scan_repo:
151
192
  # 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")
193
+ if not any([scan, sbom, detect_secret, fullscan]):
194
+ print_error("Please specify at least one scan type: --scan, --sbom, --detect-secret, or --fullscan")
154
195
  sys.exit(1)
155
196
 
156
197
  try:
157
198
  # Initialize API client
158
- print_info(f"Initializing Prismor scan for: {scan}")
199
+ print_info(f"Initializing Prismor scan for: {scan_repo}")
159
200
  client = PrismorClient()
160
201
 
161
202
  # Determine scan type for display
162
203
  scan_types = []
163
204
  if fullscan:
164
- scan_types.append("Full Scan (VEX + SBOM + Secret Detection)")
205
+ scan_types.append("Full Scan (scan + SBOM + Secret Detection)")
165
206
  else:
166
- if vex:
167
- scan_types.append("VEX")
207
+ if scan:
208
+ scan_types.append("scan")
168
209
  if sbom:
169
210
  scan_types.append("SBOM")
170
211
  if detect_secret:
171
212
  scan_types.append("Secret Detection")
172
213
 
173
214
  print_info(f"Scan type: {', '.join(scan_types)}")
174
- print_info("Starting scan... (this may take a few minutes)")
215
+ if scan or fullscan:
216
+ print_info("Starting scan... (vulnerability scans run asynchronously and may take up to 10 minutes)")
217
+ else:
218
+ print_info("Starting scan... (this may take a few minutes)")
219
+
220
+ # Show loading spinner during scan
221
+ spinner = Spinner("Scanning repository")
222
+ spinner.start()
175
223
 
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
- )
224
+ try:
225
+ # Perform scan
226
+ results = client.scan(
227
+ repo=scan_repo,
228
+ scan=scan,
229
+ sbom=sbom,
230
+ detect_secret=detect_secret,
231
+ fullscan=fullscan,
232
+ branch=branch
233
+ )
234
+ spinner.stop()
235
+ except Exception as e:
236
+ spinner.stop()
237
+ raise e
185
238
 
186
239
  # Output results
187
240
  if output_json:
@@ -192,12 +245,8 @@ def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
192
245
 
193
246
  # Try to get repository ID and display dashboard link
194
247
  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("/")
248
+ # Extract repo name from scan input using the comprehensive parser
249
+ repo_name = parse_github_repo(scan_repo)
201
250
 
202
251
  # Get repository ID
203
252
  repo_info = client.get_repository_by_name(repo_name)
@@ -236,7 +285,7 @@ def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
236
285
  @cli.command()
237
286
  def version():
238
287
  """Display the version of Prismor CLI."""
239
- click.echo("Prismor CLI v0.1.0")
288
+ click.echo("Prismor CLI v1.0.5")
240
289
 
241
290
 
242
291
  @cli.command()
@@ -256,6 +305,8 @@ def config():
256
305
  print_success(f"PRISMOR_API_KEY: {masked_key}")
257
306
  else:
258
307
  print_error("PRISMOR_API_KEY: Not set")
308
+ click.echo("\nPlease specify your API key. You can generate one for free at:")
309
+ click.secho(" https://www.prismor.dev/cli", fg="cyan", underline=True)
259
310
  click.echo("\nTo set your API key, run:")
260
311
  click.echo(" export PRISMOR_API_KEY=your_api_key")
261
312
 
@@ -268,7 +319,14 @@ def repos():
268
319
  """List your connected repositories."""
269
320
  try:
270
321
  client = PrismorClient()
271
- repos_data = client.get_repositories()
322
+ spinner = Spinner("Loading repositories")
323
+ spinner.start()
324
+ try:
325
+ repos_data = client.get_repositories()
326
+ spinner.stop()
327
+ except Exception as e:
328
+ spinner.stop()
329
+ raise e
272
330
 
273
331
  click.echo("\n" + "=" * 60)
274
332
  click.secho(" Your Repositories", fg="cyan", bold=True)
@@ -299,12 +357,216 @@ def repos():
299
357
  sys.exit(1)
300
358
 
301
359
 
360
+ @cli.command()
361
+ @click.argument("repo", type=str)
362
+ @click.option("--branch", type=str, help="Specific branch to scan (defaults to main)")
363
+ @click.option("--token", type=str, help="GitHub token (or set GITHUB_TOKEN env var)")
364
+ @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
365
+ def start_scan(repo: str, branch: Optional[str], token: Optional[str], output_json: bool):
366
+ """Start a vulnerability scan and return a job_id for status checking.
367
+
368
+ REPO is the repository to scan (username/repo, GitHub URL, SSH URL, etc.)
369
+
370
+ This command starts a scan asynchronously and returns immediately with a job_id.
371
+ Use 'prismor scan-status <job_id>' to check when the scan completes.
372
+
373
+ Note: Requires GitHub token. Set GITHUB_TOKEN environment variable or use --token option.
374
+
375
+ Examples:
376
+ prismor start-scan username/repo
377
+ prismor start-scan https://github.com/username/repo --branch develop
378
+ prismor start-scan username/repo --token ghp_xxxxx
379
+ prismor start-scan username/repo --json
380
+ """
381
+ try:
382
+ client = PrismorClient()
383
+ print_info(f"Starting vulnerability scan for: {repo}")
384
+ if branch:
385
+ print_info(f"Branch: {branch}")
386
+
387
+ spinner = Spinner("Starting scan")
388
+ spinner.start()
389
+ try:
390
+ result = client.start_vulnerability_scan(repo, branch, token)
391
+ spinner.stop()
392
+ except Exception as e:
393
+ spinner.stop()
394
+ raise e
395
+
396
+ if output_json:
397
+ click.echo(json.dumps(result, indent=2))
398
+ else:
399
+ click.echo("\n" + "=" * 60)
400
+ click.secho(" Scan Started", fg="cyan", bold=True)
401
+ click.echo("=" * 60 + "\n")
402
+
403
+ job_id = result.get("job_id")
404
+ if job_id:
405
+ print_success(f"Scan started successfully!")
406
+ click.echo()
407
+ click.secho(f"Job ID: {job_id}", fg="yellow", bold=True)
408
+ click.echo()
409
+ click.secho("Repository:", fg="yellow", bold=True)
410
+ click.echo(f" {result.get('repository', repo)}")
411
+ click.echo()
412
+ if "branch" in result:
413
+ click.secho("Branch:", fg="yellow", bold=True)
414
+ click.echo(f" {result['branch']}")
415
+ click.echo()
416
+ click.secho("Status:", fg="yellow", bold=True)
417
+ click.echo(f" {result.get('status', 'accepted')}")
418
+ click.echo()
419
+ click.secho("Next Steps:", fg="cyan", bold=True)
420
+ click.echo(f" Check scan status with:")
421
+ click.secho(f" prismor scan-status {job_id}", fg="green", bold=True)
422
+ click.echo()
423
+ else:
424
+ print_error("Failed to get job_id from response")
425
+ click.echo(json.dumps(result, indent=2))
426
+
427
+ click.echo("=" * 60 + "\n")
428
+
429
+ except PrismorAPIError as e:
430
+ print_error(str(e))
431
+ sys.exit(1)
432
+ except Exception as e:
433
+ print_error(f"Unexpected error: {str(e)}")
434
+ sys.exit(1)
435
+
436
+
437
+ @cli.command()
438
+ @click.argument("job_id", type=str)
439
+ @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
440
+ def scan_status(job_id: str, output_json: bool):
441
+ """Check the status of a vulnerability scan job.
442
+
443
+ JOB_ID is the job ID returned when starting a scan.
444
+
445
+ Examples:
446
+ prismor scan-status a724a4663cda4bf087ad171683cb726d
447
+ prismor scan-status 50cbe253e5634227b81fe744c2a0b3e7 --json
448
+ """
449
+ try:
450
+ client = PrismorClient()
451
+ print_info(f"Checking scan status for job: {job_id}")
452
+
453
+ spinner = Spinner("Checking status")
454
+ spinner.start()
455
+ try:
456
+ status_data = client.check_scan_status(job_id)
457
+ spinner.stop()
458
+ except Exception as e:
459
+ spinner.stop()
460
+ raise e
461
+
462
+ if output_json:
463
+ click.echo(json.dumps(status_data, indent=2))
464
+ else:
465
+ click.echo("\n" + "=" * 60)
466
+ click.secho(" Scan Status", fg="cyan", bold=True)
467
+ click.echo("=" * 60 + "\n")
468
+
469
+ click.secho(f"Job ID: {status_data.get('job_id', job_id)}", fg="yellow", bold=True)
470
+ click.echo()
471
+
472
+ status = status_data.get("status", "unknown")
473
+ if status == "completed":
474
+ print_success(f"Status: {status}")
475
+ click.echo()
476
+
477
+ if "repository" in status_data:
478
+ click.secho("Repository:", fg="yellow", bold=True)
479
+ click.echo(f" {status_data['repository']}")
480
+ click.echo()
481
+
482
+ if "branch" in status_data:
483
+ click.secho("Branch:", fg="yellow", bold=True)
484
+ click.echo(f" {status_data['branch']}")
485
+ click.echo()
486
+
487
+ if "duration" in status_data:
488
+ click.secho("Duration:", fg="yellow", bold=True)
489
+ click.echo(f" {status_data['duration']:.2f} seconds")
490
+ click.echo()
491
+
492
+ if "public_url" in status_data:
493
+ click.secho("Results URL:", fg="yellow", bold=True)
494
+ click.secho(f" {status_data['public_url']}", fg="green")
495
+ click.echo()
496
+
497
+ if "presigned_url" in status_data:
498
+ click.secho("Presigned URL (expires in 1 hour):", fg="yellow", bold=True)
499
+ click.secho(f" {status_data['presigned_url']}", fg="blue")
500
+ click.echo()
501
+
502
+ # Display vulnerability summary if available
503
+ if "summary" in status_data:
504
+ summary = status_data["summary"]
505
+ click.secho("Vulnerability Summary:", fg="yellow", bold=True)
506
+ click.echo(f" Total Vulnerabilities: {summary.get('total_vulnerabilities', 0)}")
507
+ click.echo(f" Total Targets Scanned: {summary.get('total_targets', 0)}")
508
+ click.echo()
509
+
510
+ severity_breakdown = summary.get('severity_breakdown', {})
511
+ if severity_breakdown:
512
+ click.secho(" Severity Breakdown:", fg="yellow")
513
+ if severity_breakdown.get('CRITICAL', 0) > 0:
514
+ click.secho(f" CRITICAL: {severity_breakdown['CRITICAL']}", fg="red", bold=True)
515
+ if severity_breakdown.get('HIGH', 0) > 0:
516
+ click.secho(f" HIGH: {severity_breakdown['HIGH']}", fg="red")
517
+ if severity_breakdown.get('MEDIUM', 0) > 0:
518
+ click.secho(f" MEDIUM: {severity_breakdown['MEDIUM']}", fg="yellow")
519
+ if severity_breakdown.get('LOW', 0) > 0:
520
+ click.secho(f" LOW: {severity_breakdown['LOW']}", fg="blue")
521
+ if severity_breakdown.get('UNKNOWN', 0) > 0:
522
+ click.secho(f" UNKNOWN: {severity_breakdown['UNKNOWN']}", fg="white")
523
+ click.echo()
524
+
525
+ if "scan_date" in status_data:
526
+ click.secho("Scan Date:", fg="yellow", bold=True)
527
+ click.echo(f" {status_data['scan_date']}")
528
+ click.echo()
529
+
530
+ elif status == "running":
531
+ print_info(f"Status: {status}")
532
+ if "message" in status_data:
533
+ click.echo(f" {status_data['message']}")
534
+ click.echo()
535
+ click.echo("The scan is still in progress. Check back in a few moments.")
536
+ click.echo()
537
+
538
+ elif status == "failed":
539
+ print_error(f"Status: {status}")
540
+ if "error" in status_data:
541
+ click.echo(f" Error: {status_data['error']}")
542
+ click.echo()
543
+ else:
544
+ click.secho(f"Status: {status}", fg="yellow")
545
+ click.echo()
546
+
547
+ click.echo("=" * 60 + "\n")
548
+
549
+ except PrismorAPIError as e:
550
+ print_error(str(e))
551
+ sys.exit(1)
552
+ except Exception as e:
553
+ print_error(f"Unexpected error: {str(e)}")
554
+ sys.exit(1)
555
+
556
+
302
557
  @cli.command()
303
558
  def status():
304
559
  """Check your account status and GitHub integration."""
305
560
  try:
306
561
  client = PrismorClient()
307
- auth_data = client.authenticate()
562
+ spinner = Spinner("Checking account status")
563
+ spinner.start()
564
+ try:
565
+ auth_data = client.authenticate()
566
+ spinner.stop()
567
+ except Exception as e:
568
+ spinner.stop()
569
+ raise e
308
570
 
309
571
  click.echo("\n" + "=" * 60)
310
572
  click.secho(" Account Status", fg="cyan", bold=True)
@@ -312,9 +574,7 @@ def status():
312
574
 
313
575
  user_info = auth_data.get("user", {})
314
576
  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
-
577
+
318
578
  repositories = user_info.get("repositories", [])
319
579
  click.secho(f"Connected Repositories: {len(repositories)}", fg="green")
320
580
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prismor
3
- Version: 0.1.2
3
+ Version: 1.0.5
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
@@ -48,12 +48,12 @@ A powerful command-line tool for scanning GitHub repositories for security vulne
48
48
 
49
49
  ## Features
50
50
 
51
- - 🔍 **Vulnerability Scanning (VEX)** - Detect security vulnerabilities in your codebase
51
+ - 🔍 **Vulnerability Scanning (scan)** - Detect security vulnerabilities in your codebase
52
52
  - 🔐 **Secret Detection** - Find exposed secrets, API keys, and credentials
53
53
  - 📦 **SBOM Generation** - Generate comprehensive Software Bill of Materials
54
54
  - ⚡ **Full Scan** - Run all security checks in one command
55
55
  - 🎨 **Beautiful CLI Output** - Colorful, easy-to-read results
56
- - 🔗 **Flexible Repository Input** - Support for `username/repo` or full GitHub URLs
56
+ - 🔗 **Flexible Repository Input** - Support for multiple GitHub URL formats including SSH, HTTPS, and bare domain formats
57
57
 
58
58
  ## Quick Start
59
59
 
@@ -61,7 +61,7 @@ A powerful command-line tool for scanning GitHub repositories for security vulne
61
61
  2. **Generate your API Key** from the dashboard
62
62
  3. **Install** the CLI: `pip install prismor`
63
63
  4. **Set your API key**: `export PRISMOR_API_KEY=your_api_key`
64
- 5. **Run your first scan**: `prismor --scan username/repo --fullscan`
64
+ 5. **Run your first scan**: `prismor --repo username/repo --fullscan`
65
65
 
66
66
  For the complete analysis with dashboards and reports, visit [Prismor.dev](https://prismor.dev) after running scans!
67
67
 
@@ -116,39 +116,91 @@ This allows Prismor to securely access and scan your private repositories.
116
116
  ### Basic Syntax
117
117
 
118
118
  ```bash
119
- prismor --scan <repository> [scan-type]
119
+ prismor --repo <repository> [scan-type]
120
120
  ```
121
121
 
122
+ **Note**: The `--scan` flag is used to enable vulnerability scanning, while `--repo` specifies the repository to scan.
123
+
122
124
  ### Repository Format
123
125
 
124
- You can specify repositories in two ways:
126
+ Prismor CLI supports multiple GitHub repository URL formats for maximum flexibility:
127
+
128
+ #### 1. **Username/Repository format** (recommended):
129
+ ```bash
130
+ prismor --repo Ar9av/trychai-web-revamped --fullscan
131
+ ```
132
+
133
+ #### 2. **HTTPS URLs**:
134
+ ```bash
135
+ prismor --repo https://github.com/Ar9av/trychai-web-revamped --fullscan
136
+ prismor --repo https://www.github.com/Ar9av/trychai-web-revamped --fullscan
137
+ prismor --repo https://github.com/Ar9av/trychai-web-revamped.git --fullscan
138
+ ```
139
+
140
+ #### 3. **HTTP URLs**:
141
+ ```bash
142
+ prismor --repo http://github.com/Ar9av/trychai-web-revamped --fullscan
143
+ prismor --repo http://www.github.com/Ar9av/trychai-web-revamped --fullscan
144
+ ```
145
+
146
+ #### 4. **Bare domain formats**:
147
+ ```bash
148
+ prismor --repo github.com/Ar9av/trychai-web-revamped --fullscan
149
+ prismor --repo www.github.com/Ar9av/trychai-web-revamped --fullscan
150
+ ```
151
+
152
+ #### 5. **SSH format**:
153
+ ```bash
154
+ prismor --repo git@github.com:Ar9av/trychai-web-revamped.git --fullscan
155
+ ```
156
+
157
+ #### 6. **URLs with paths and fragments**:
158
+ ```bash
159
+ prismor --repo https://github.com/Ar9av/trychai-web-revamped/tree/main --fullscan
160
+ prismor --repo https://github.com/Ar9av/trychai-web-revamped/blob/main/file.py --fullscan
161
+ prismor --repo https://github.com/Ar9av/trychai-web-revamped#branch --fullscan
162
+ ```
163
+
164
+ **All formats are automatically parsed and normalized to extract the `user/repo_name` format for processing.**
125
165
 
126
- 1. **Username/Repository format:**
127
- ```bash
128
- prismor --scan Ar9av/trychai-web-revamped --fullscan
129
- ```
166
+ ### Smart URL Parsing
130
167
 
131
- 2. **Full GitHub URL:**
132
- ```bash
133
- prismor --scan https://github.com/Ar9av/trychai-web-revamped --fullscan
134
- ```
168
+ Prismor CLI features intelligent GitHub URL parsing that automatically:
169
+
170
+ - **Detects and extracts** repository information from any supported format
171
+ - ✅ **Handles edge cases** like trailing slashes, `.git` suffixes, and branch references
172
+ - ✅ **Validates input** to ensure it's a valid GitHub repository
173
+ - ✅ **Provides clear error messages** for unsupported formats
174
+ - ✅ **Supports special characters** in repository names (hyphens, underscores, numbers)
175
+
176
+ **Examples of what gets automatically parsed:**
177
+ ```bash
178
+ # All of these resolve to "facebook/react":
179
+ prismor --repo facebook/react --scan
180
+ prismor --repo https://github.com/facebook/react --scan
181
+ prismor --repo git@github.com:facebook/react.git --scan
182
+ prismor --repo github.com/facebook/react --scan
183
+ prismor --repo https://github.com/facebook/react/tree/main --scan
184
+ ```
135
185
 
136
186
  ### Scan Types
137
187
 
138
- #### 1. Vulnerability Scanning (VEX)
188
+ #### 1. Vulnerability Scanning (scan)
139
189
 
140
190
  Scan for security vulnerabilities in your dependencies and code:
141
191
 
142
192
  ```bash
143
- prismor --scan myrepository --vex
193
+ prismor --repo myrepository --scan
144
194
  ```
145
195
 
196
+ **Note**: Vulnerability scans now run asynchronously for large repositories. The CLI will wait for completion automatically, but you can also use `prismor start-scan` to get a job ID and check status separately.
197
+
146
198
  #### 2. Secret Detection
147
199
 
148
200
  Detect exposed secrets, API keys, passwords, and other sensitive information:
149
201
 
150
202
  ```bash
151
- prismor --scan myrepository --detect-secret
203
+ prismor --repo myrepository --detect-secret
152
204
  ```
153
205
 
154
206
  #### 3. SBOM Generation
@@ -156,15 +208,15 @@ prismor --scan myrepository --detect-secret
156
208
  Generate a Software Bill of Materials for your repository:
157
209
 
158
210
  ```bash
159
- prismor --scan myrepository --sbom
211
+ prismor --repo myrepository --sbom
160
212
  ```
161
213
 
162
214
  #### 4. Full Scan
163
215
 
164
- Run all security checks (VEX + Secret Detection + SBOM):
216
+ Run all security checks (scan + Secret Detection + SBOM):
165
217
 
166
218
  ```bash
167
- prismor --scan myrepository --fullscan
219
+ prismor --repo myrepository --fullscan
168
220
  ```
169
221
 
170
222
  ### Multiple Scan Types
@@ -172,7 +224,7 @@ prismor --scan myrepository --fullscan
172
224
  You can combine multiple scan types:
173
225
 
174
226
  ```bash
175
- prismor --scan myrepository --vex --detect-secret
227
+ prismor --repo myrepository --scan --detect-secret
176
228
  ```
177
229
 
178
230
  ### JSON Output
@@ -180,37 +232,127 @@ prismor --scan myrepository --vex --detect-secret
180
232
  Get results in JSON format for automation and integration:
181
233
 
182
234
  ```bash
183
- prismor --scan myrepository --fullscan --json
235
+ prismor --repo myrepository --fullscan --json
184
236
  ```
185
237
 
186
238
  ## Examples
187
239
 
188
- ### Example 1: Quick Vulnerability Scan
240
+ ### Example 1: Quick Vulnerability Scan (Username/Repo format)
241
+
242
+ ```bash
243
+ prismor --repo facebook/react --scan
244
+ ```
245
+
246
+ ### Example 2: Comprehensive Security Audit (HTTPS URL)
247
+
248
+ ```bash
249
+ prismor --repo https://github.com/microsoft/vscode --fullscan
250
+ ```
251
+
252
+ ### Example 3: Secret Detection with SSH URL
253
+
254
+ ```bash
255
+ prismor --repo git@github.com:openai/gpt-3.git --detect-secret
256
+ ```
257
+
258
+ ### Example 4: SBOM Generation with Bare Domain
189
259
 
190
260
  ```bash
191
- prismor --scan facebook/react --vex
261
+ prismor --repo github.com/kubernetes/kubernetes --sbom --json > sbom-results.json
192
262
  ```
193
263
 
194
- ### Example 2: Comprehensive Security Audit
264
+ ### Example 5: Full Scan with Branch Reference
195
265
 
196
266
  ```bash
197
- prismor --scan https://github.com/microsoft/vscode --fullscan
267
+ prismor --repo https://github.com/tensorflow/tensorflow/tree/v2.13.0 --fullscan
198
268
  ```
199
269
 
200
- ### Example 3: Secret Detection Only
270
+ ### Example 6: Multiple Scan Types with Different URL Formats
201
271
 
202
272
  ```bash
203
- prismor --scan openai/gpt-3 --detect-secret
273
+ # Using HTTPS URL
274
+ prismor --repo https://github.com/pytorch/pytorch --scan --sbom
275
+
276
+ # Using SSH URL
277
+ prismor --repo git@github.com:nodejs/node.git --detect-secret --sbom
278
+
279
+ # Using bare domain
280
+ prismor --repo www.github.com/vercel/next.js --fullscan
204
281
  ```
205
282
 
206
- ### Example 4: SBOM Generation with JSON Output
283
+ ### Example 7: Async Scan with Status Checking
207
284
 
208
285
  ```bash
209
- prismor --scan kubernetes/kubernetes --sbom --json > sbom-results.json
286
+ # Start a scan and get job ID
287
+ prismor start-scan username/repo --branch main
288
+
289
+ # Check scan status (use job ID from previous command)
290
+ prismor scan-status <job_id>
291
+
292
+ # Check status with JSON output
293
+ prismor scan-status <job_id> --json
210
294
  ```
211
295
 
212
296
  ## Additional Commands
213
297
 
298
+ ### Start Async Vulnerability Scan
299
+
300
+ Start a vulnerability scan asynchronously and get a job ID for status checking:
301
+
302
+ ```bash
303
+ prismor start-scan username/repo
304
+ prismor start-scan username/repo --branch develop
305
+ prismor start-scan username/repo --token ghp_xxxxx
306
+ ```
307
+
308
+ **Note**: Requires GitHub token. Set `GITHUB_TOKEN` environment variable or use `--token` option.
309
+
310
+ ### Check Scan Status
311
+
312
+ Check the status of a running or completed vulnerability scan:
313
+
314
+ ```bash
315
+ prismor scan-status <job_id>
316
+ prismor scan-status <job_id> --json
317
+ ```
318
+
319
+ **Status Response Includes**:
320
+ - Job status (running/completed/failed)
321
+ - Repository and branch information
322
+ - Results URLs (public and presigned)
323
+ - Vulnerability summary with severity breakdown
324
+ - Scan date and duration
325
+
326
+ **Example Output**:
327
+ ```
328
+ ============================================================
329
+ Scan Status
330
+ ============================================================
331
+
332
+ Job ID: abc123def456...
333
+
334
+ Status: completed
335
+
336
+ Repository:
337
+ https://github.com/username/repo
338
+
339
+ Branch:
340
+ main
341
+
342
+ Vulnerability Summary:
343
+ Total Vulnerabilities: 15
344
+ Total Targets Scanned: 3
345
+
346
+ Severity Breakdown:
347
+ CRITICAL: 2
348
+ HIGH: 5
349
+ MEDIUM: 6
350
+ LOW: 2
351
+
352
+ Results URL:
353
+ https://prismor-sbom-public-dev.s3.amazonaws.com/...
354
+ ```
355
+
214
356
  ### Check Configuration
215
357
 
216
358
  View your current Prismor CLI configuration:
@@ -303,9 +445,24 @@ export PRISMOR_API_KEY=your_api_key_here
303
445
 
304
446
  ### Invalid Repository Format
305
447
 
306
- Ensure your repository is in one of these formats:
307
- - `username/repository`
448
+ Ensure your repository is in one of the supported formats:
449
+
450
+ **Supported formats:**
451
+ - `username/repository` (recommended)
308
452
  - `https://github.com/username/repository`
453
+ - `https://www.github.com/username/repository`
454
+ - `http://github.com/username/repository`
455
+ - `http://www.github.com/username/repository`
456
+ - `github.com/username/repository`
457
+ - `www.github.com/username/repository`
458
+ - `git@github.com:username/repository.git`
459
+ - `https://github.com/username/repository/tree/branch`
460
+ - `https://github.com/username/repository/blob/branch/file`
461
+
462
+ **Not supported:**
463
+ - Non-GitHub URLs (GitLab, Bitbucket, etc.)
464
+ - Invalid URL formats
465
+ - Empty or malformed repository names
309
466
 
310
467
  ### Connection Issues
311
468
 
@@ -0,0 +1,9 @@
1
+ prismor/__init__.py,sha256=_1mv0XdQk8KdVm_Fua-h0nBJJS3SrZIKOPJYhi8Vt00,230
2
+ prismor/api.py,sha256=ydcovvL5ior_dpQByoYfCySoO1jIbtjrwr8iCjnBa58,19020
3
+ prismor/cli.py,sha256=k_3IAIntTNyV8Gkw2gNwDoH_B_dFZXC_sZ9V98C_9MQ,24197
4
+ prismor-1.0.5.dist-info/licenses/LICENSE,sha256=qWFF8Eh6gpZOq_3effdd6hfeMN2WN9ZG4vOyFk2MyhU,1065
5
+ prismor-1.0.5.dist-info/METADATA,sha256=1TIjbE3iKGy0U46NWYsxKkS0lVtAjLR8eHXB5Wjaizo,14319
6
+ prismor-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ prismor-1.0.5.dist-info/entry_points.txt,sha256=Uiu0HW04eq2Gb6sQC9o-LqMKMyW1SKwkojxrkFeVfqg,45
8
+ prismor-1.0.5.dist-info/top_level.txt,sha256=nlJGoJ3fQXRL27RXQ5LJU2LX1kl1VSgKXyKjcSR28lw,8
9
+ prismor-1.0.5.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- prismor/__init__.py,sha256=6sRGygr6VrNie8Xd_B9Zeq6Q0ThWRftLBnVzZBdGEb4,230
2
- prismor/api.py,sha256=YrFnw1adT4ci6ehR4qB03kSOxw9l1o4fm0KSGUlDU4s,8886
3
- prismor/cli.py,sha256=K0aOxtbhE-gUoRw7selqT1a7BTr80A4Ogvvcspx5BUk,13582
4
- prismor-0.1.2.dist-info/licenses/LICENSE,sha256=qWFF8Eh6gpZOq_3effdd6hfeMN2WN9ZG4vOyFk2MyhU,1065
5
- prismor-0.1.2.dist-info/METADATA,sha256=sDwR9KP4wlNOdQON-j-42Sabowz2q7IhCXb4dss8s1I,9394
6
- prismor-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- prismor-0.1.2.dist-info/entry_points.txt,sha256=Uiu0HW04eq2Gb6sQC9o-LqMKMyW1SKwkojxrkFeVfqg,45
8
- prismor-0.1.2.dist-info/top_level.txt,sha256=nlJGoJ3fQXRL27RXQ5LJU2LX1kl1VSgKXyKjcSR28lw,8
9
- prismor-0.1.2.dist-info/RECORD,,