prismor 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prismor/__init__.py CHANGED
@@ -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.1"
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
@@ -26,12 +26,52 @@ class PrismorClient:
26
26
  "Please set it with: export PRISMOR_API_KEY=your_api_key"
27
27
  )
28
28
 
29
- self.base_url = "https://api.prismor.dev"
29
+ # self.base_url = "http://localhost:3000"
30
+ self.base_url = "https://prismor.dev"
30
31
  self.headers = {
31
32
  "Authorization": f"Bearer {self.api_key}",
32
33
  "Content-Type": "application/json"
33
34
  }
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
+
35
75
  def normalize_repo_url(self, repo: str) -> str:
36
76
  """Normalize repository input to a full GitHub URL.
37
77
 
@@ -59,7 +99,8 @@ class PrismorClient:
59
99
  vex: bool = False,
60
100
  sbom: bool = False,
61
101
  detect_secret: bool = False,
62
- fullscan: bool = False
102
+ fullscan: bool = False,
103
+ branch: Optional[str] = None
63
104
  ) -> Dict[str, Any]:
64
105
  """Perform security scan on a GitHub repository.
65
106
 
@@ -69,41 +110,59 @@ class PrismorClient:
69
110
  sbom: Enable SBOM generation
70
111
  detect_secret: Enable secret detection
71
112
  fullscan: Enable all scan types
113
+ branch: Specific branch to scan (defaults to main/master)
72
114
 
73
115
  Returns:
74
116
  Dictionary containing scan results
75
117
  """
76
118
  repo_url = self.normalize_repo_url(repo)
77
119
 
78
- # Prepare request payload
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
79
125
  payload = {
80
126
  "repo_url": repo_url,
127
+ "api_key": self.api_key,
81
128
  "vex": vex or fullscan,
82
129
  "sbom": sbom or fullscan,
83
130
  "detect_secret": detect_secret or fullscan,
84
- "fullscan": fullscan
131
+ "fullscan": fullscan,
132
+ "branch": branch
85
133
  }
86
134
 
87
135
  try:
88
136
  response = requests.post(
89
- f"{self.base_url}/scan",
137
+ f"{self.base_url}/api/cli/scan",
90
138
  json=payload,
91
- headers=self.headers,
139
+ headers={"Content-Type": "application/json"},
92
140
  timeout=300 # 5 minute timeout
93
141
  )
94
142
 
95
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
+ )
96
150
  raise PrismorAPIError("Invalid API key. Please check your PRISMOR_API_KEY.")
97
151
 
98
152
  if response.status_code == 404:
99
- raise PrismorAPIError("API endpoint not found. Please check the API URL.")
153
+ raise PrismorAPIError("CLI scan endpoint not found. Please check if CLI endpoints are available.")
100
154
 
101
155
  if response.status_code >= 400:
102
156
  error_msg = response.json().get("error", "Unknown error")
103
157
  raise PrismorAPIError(f"API error: {error_msg}")
104
158
 
105
159
  response.raise_for_status()
106
- return response.json()
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
107
166
 
108
167
  except requests.exceptions.Timeout:
109
168
  raise PrismorAPIError(
@@ -115,4 +174,24 @@ class PrismorClient:
115
174
  )
116
175
  except requests.exceptions.RequestException as e:
117
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
+ }
118
197
 
prismor/cli.py CHANGED
@@ -38,56 +38,79 @@ def format_scan_results(results: dict, scan_type: str):
38
38
  click.secho("Repository:", fg="yellow", bold=True)
39
39
  click.echo(f" {results['repository']}\n")
40
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()
41
+ if "branch" in results:
42
+ click.secho("Branch:", fg="yellow", bold=True)
43
+ click.echo(f" {results['branch']}\n")
47
44
 
48
- # Display scan results based on type
49
- if "scan_results" in results:
50
- scan_data = results["scan_results"]
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"]
51
52
 
52
53
  # 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")
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']}")
60
71
  click.echo()
61
72
 
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'}")
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']}")
71
89
  click.echo()
72
90
 
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")
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']}")
81
108
  click.echo()
82
109
 
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")
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")
91
114
 
92
115
  click.echo("=" * 60 + "\n")
93
116
 
@@ -102,17 +125,21 @@ def format_scan_results(results: dict, scan_type: str):
102
125
  @click.option("--sbom", is_flag=True, help="Generate Software Bill of Materials")
103
126
  @click.option("--detect-secret", is_flag=True, help="Detect secrets in repository")
104
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)")
105
129
  @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
106
130
  @click.version_option(version="0.1.0", prog_name="prismor")
107
131
  @click.pass_context
108
132
  def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
109
- fullscan: bool, output_json: bool):
133
+ fullscan: bool, branch: Optional[str], output_json: bool):
110
134
  """Prismor CLI - Security scanning tool for GitHub repositories.
111
135
 
112
136
  Examples:
113
137
  prismor --scan username/repo --vex
114
138
  prismor --scan username/repo --fullscan
115
139
  prismor --scan https://github.com/username/repo --detect-secret
140
+ prismor --scan username/repo --sbom --branch develop
141
+ prismor status
142
+ prismor repos
116
143
  """
117
144
  # If no command and no scan option, show help
118
145
  if ctx.invoked_subcommand is None and not scan:
@@ -152,7 +179,8 @@ def cli(ctx, scan: Optional[str], vex: bool, sbom: bool, detect_secret: bool,
152
179
  vex=vex,
153
180
  sbom=sbom,
154
181
  detect_secret=detect_secret,
155
- fullscan=fullscan
182
+ fullscan=fullscan,
183
+ branch=branch
156
184
  )
157
185
 
158
186
  # Output results
@@ -196,10 +224,88 @@ def config():
196
224
  click.echo("\nTo set your API key, run:")
197
225
  click.echo(" export PRISMOR_API_KEY=your_api_key")
198
226
 
199
- click.echo("\nAPI Endpoint: https://api.prismor.dev")
227
+ # click.echo("\nAPI Endpoint: http://localhost:3000")
200
228
  click.echo("=" * 60 + "\n")
201
229
 
202
230
 
231
+ @cli.command()
232
+ def repos():
233
+ """List your connected repositories."""
234
+ try:
235
+ client = PrismorClient()
236
+ repos_data = client.get_repositories()
237
+
238
+ click.echo("\n" + "=" * 60)
239
+ click.secho(" Your Repositories", fg="cyan", bold=True)
240
+ click.echo("=" * 60 + "\n")
241
+
242
+ user_info = repos_data.get("user", {})
243
+ if user_info:
244
+ click.secho(f"User: {user_info.get('name', 'Unknown')} ({user_info.get('email', 'No email')})", fg="yellow")
245
+ click.echo()
246
+
247
+ repositories = repos_data.get("repositories", [])
248
+ if repositories:
249
+ for repo in repositories:
250
+ click.secho(f"• {repo.get('name', 'Unknown')}", fg="green")
251
+ click.echo(f" URL: {repo.get('htmlUrl', 'No URL')}")
252
+ click.echo(f" Owner: {repo.get('githubOwner', 'Unknown')}")
253
+ click.echo()
254
+ else:
255
+ print_warning("No repositories found. Connect repositories through the web interface.")
256
+
257
+ click.echo("=" * 60 + "\n")
258
+
259
+ except PrismorAPIError as e:
260
+ print_error(str(e))
261
+ sys.exit(1)
262
+ except Exception as e:
263
+ print_error(f"Unexpected error: {str(e)}")
264
+ sys.exit(1)
265
+
266
+
267
+ @cli.command()
268
+ def status():
269
+ """Check your account status and GitHub integration."""
270
+ try:
271
+ client = PrismorClient()
272
+ auth_data = client.authenticate()
273
+
274
+ click.echo("\n" + "=" * 60)
275
+ click.secho(" Account Status", fg="cyan", bold=True)
276
+ click.echo("=" * 60 + "\n")
277
+
278
+ user_info = auth_data.get("user", {})
279
+ if user_info:
280
+ click.secho(f"User: {user_info.get('name', 'Unknown')} ({user_info.get('email', 'No email')})", fg="yellow")
281
+ click.echo()
282
+
283
+ repositories = user_info.get("repositories", [])
284
+ click.secho(f"Connected Repositories: {len(repositories)}", fg="green")
285
+
286
+ if repositories:
287
+ click.echo("\nRepository List:")
288
+ for repo in repositories:
289
+ click.echo(f" • {repo.get('name', 'Unknown')} ({repo.get('htmlUrl', 'No URL')})")
290
+ else:
291
+ print_warning("No repositories connected.")
292
+ click.echo("\nTo connect repositories:")
293
+ click.echo(" 1. Visit https://prismor.dev/dashboard")
294
+ click.echo(" 2. Connect your GitHub account")
295
+ click.echo(" 3. Select repositories to scan")
296
+ else:
297
+ print_error("Failed to retrieve account information.")
298
+
299
+ click.echo("\n" + "=" * 60 + "\n")
300
+
301
+ except PrismorAPIError as e:
302
+ print_error(str(e))
303
+ sys.exit(1)
304
+ except Exception as e:
305
+ print_error(f"Unexpected error: {str(e)}")
306
+ sys.exit(1)
307
+
308
+
203
309
  def main():
204
310
  """Entry point for the CLI."""
205
311
  cli()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prismor
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -0,0 +1,9 @@
1
+ prismor/__init__.py,sha256=Wz9TJXpg-R4rq3KkawmGdozbNPeLMl2L8dTaAJKgBZQ,230
2
+ prismor/api.py,sha256=Sx9YahTwGfO0cWXkVbjE0w_1hSrrhMYWzQw4tWxitQM,7149
3
+ prismor/cli.py,sha256=a5DZ-x3g-L7-6R4-uJXlP_-V-n0Rubh2C_7EMvWtV2A,11452
4
+ prismor-0.1.1.dist-info/licenses/LICENSE,sha256=qWFF8Eh6gpZOq_3effdd6hfeMN2WN9ZG4vOyFk2MyhU,1065
5
+ prismor-0.1.1.dist-info/METADATA,sha256=RVjWQzwXVGUbUEVzKk9ilXzJowLFVe1o1nKqCmBA-Zk,9394
6
+ prismor-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ prismor-0.1.1.dist-info/entry_points.txt,sha256=Uiu0HW04eq2Gb6sQC9o-LqMKMyW1SKwkojxrkFeVfqg,45
8
+ prismor-0.1.1.dist-info/top_level.txt,sha256=nlJGoJ3fQXRL27RXQ5LJU2LX1kl1VSgKXyKjcSR28lw,8
9
+ prismor-0.1.1.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- prismor/__init__.py,sha256=d_7a1UaXHGT7kAzI5fERetpR2vEj349becGSTKatQU0,230
2
- prismor/api.py,sha256=jnbxgzDJjYLGxC5viT18xVjcmbupFecvlb-ph8J0yik,3938
3
- prismor/cli.py,sha256=zjRJH9li_0_2CNquI4Y14alGpmiq1d-P_rZBTp4bS6I,7310
4
- prismor-0.1.0.dist-info/licenses/LICENSE,sha256=qWFF8Eh6gpZOq_3effdd6hfeMN2WN9ZG4vOyFk2MyhU,1065
5
- prismor-0.1.0.dist-info/METADATA,sha256=2boiqdtLo65Do3pxw_P7wWmU-FjVv95S43b3xfNT7Ps,9394
6
- prismor-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- prismor-0.1.0.dist-info/entry_points.txt,sha256=Uiu0HW04eq2Gb6sQC9o-LqMKMyW1SKwkojxrkFeVfqg,45
8
- prismor-0.1.0.dist-info/top_level.txt,sha256=nlJGoJ3fQXRL27RXQ5LJU2LX1kl1VSgKXyKjcSR28lw,8
9
- prismor-0.1.0.dist-info/RECORD,,