prelude-sdk-beta 1406__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.

Potentially problematic release.


This version of prelude-sdk-beta might be problematic. Click here for more details.

@@ -0,0 +1,424 @@
1
+ from prelude_sdk_beta.controllers.http_controller import HttpController
2
+ from prelude_sdk_beta.models.account import verify_credentials
3
+ from prelude_sdk_beta.models.codes import Control, ControlCategory, PartnerEvents, RunCode
4
+
5
+
6
+ class ScmController(HttpController):
7
+ default = -1
8
+
9
+ def __init__(self, account):
10
+ super().__init__(account)
11
+
12
+ @verify_credentials
13
+ def endpoints(self, filter: str = None, orderby: str = None, top: int = None):
14
+ """List endpoints with SCM analysis"""
15
+ params = {"$filter": filter, "$orderby": orderby, "$top": top}
16
+ res = self.get(
17
+ f"{self.account.hq}/scm/endpoints",
18
+ headers=self.account.headers,
19
+ params=params,
20
+ timeout=30,
21
+ )
22
+ return res.json()
23
+
24
+ @verify_credentials
25
+ def inboxes(self, filter: str = None, orderby: str = None, top: int = None):
26
+ """List inboxes with SCM analysis"""
27
+ params = {"$filter": filter, "$orderby": orderby, "$top": top}
28
+ res = self.get(
29
+ f"{self.account.hq}/scm/inboxes",
30
+ headers=self.account.headers,
31
+ params=params,
32
+ timeout=30,
33
+ )
34
+ return res.json()
35
+
36
+ @verify_credentials
37
+ def users(self, filter: str = None, orderby: str = None, top: int = None):
38
+ """List users with SCM analysis"""
39
+ params = {"$filter": filter, "$orderby": orderby, "$top": top}
40
+ res = self.get(
41
+ f"{self.account.hq}/scm/users",
42
+ headers=self.account.headers,
43
+ params=params,
44
+ timeout=30,
45
+ )
46
+ return res.json()
47
+
48
+ @verify_credentials
49
+ def technique_summary(self, techniques: str):
50
+ """Get policy evaluation summary by technique"""
51
+ res = self.get(
52
+ f"{self.account.hq}/scm/technique_summary",
53
+ params=dict(techniques=techniques),
54
+ headers=self.account.headers,
55
+ timeout=30,
56
+ )
57
+ return res.json()
58
+
59
+ @verify_credentials
60
+ def evaluation_summary(
61
+ self,
62
+ endpoint_filter: str = None,
63
+ inbox_filter: str = None,
64
+ user_filter: str = None,
65
+ techniques: str = None,
66
+ ):
67
+ """Get policy evaluation summary"""
68
+ params = dict(
69
+ endpoints_filter=endpoint_filter,
70
+ inboxes_filter=inbox_filter,
71
+ users_filter=user_filter,
72
+ )
73
+ if techniques:
74
+ params["techniques"] = techniques
75
+ res = self.get(
76
+ f"{self.account.hq}/scm/evaluation_summary",
77
+ params=params,
78
+ headers=self.account.headers,
79
+ timeout=30,
80
+ )
81
+ return res.json()
82
+
83
+ @verify_credentials
84
+ def evaluation(
85
+ self,
86
+ partner: Control,
87
+ instance_id: str,
88
+ filter: str = None,
89
+ techniques: str = None,
90
+ ):
91
+ """Get policy evaluations for given partner"""
92
+ params = {"$filter": filter}
93
+ if techniques:
94
+ params["techniques"] = techniques
95
+ res = self.get(
96
+ f"{self.account.hq}/scm/evaluations/{partner.name}/{instance_id}",
97
+ params=params,
98
+ headers=self.account.headers,
99
+ timeout=30,
100
+ )
101
+ return res.json()
102
+
103
+ @verify_credentials
104
+ def update_evaluation(self, partner: Control, instance_id: str):
105
+ """Update policy evaluations for given partner"""
106
+ res = self.post(
107
+ f"{self.account.hq}/scm/evaluations/{partner.name}/{instance_id}",
108
+ headers=self.account.headers,
109
+ timeout=60,
110
+ )
111
+ return res.json()
112
+
113
+ @verify_credentials
114
+ def list_partner_groups(self, filter: str = None, orderby: str = None):
115
+ """List groups"""
116
+ params = {"$filter": filter, "$orderby": orderby}
117
+ res = self.get(
118
+ f"{self.account.hq}/scm/groups",
119
+ headers=self.account.headers,
120
+ params=params,
121
+ timeout=10,
122
+ )
123
+ return res.json()
124
+
125
+ @verify_credentials
126
+ def update_partner_groups(
127
+ self, partner: Control, instance_id: str, group_ids: list[str]
128
+ ):
129
+ """Update groups"""
130
+ body = dict(group_ids=group_ids)
131
+ res = self.post(
132
+ f"{self.account.hq}/scm/groups/{partner.name}/{instance_id}",
133
+ headers=self.account.headers,
134
+ json=body,
135
+ timeout=10,
136
+ )
137
+ return res.json()
138
+
139
+ @verify_credentials
140
+ def list_object_exceptions(self):
141
+ """List object exceptions"""
142
+ res = self.get(
143
+ f"{self.account.hq}/scm/exceptions/objects",
144
+ headers=self.account.headers,
145
+ timeout=10,
146
+ )
147
+ return res.json()
148
+
149
+ @verify_credentials
150
+ def create_object_exception(
151
+ self, category: ControlCategory, filter, name=None, expires: str = None
152
+ ):
153
+ """Create an object exception"""
154
+ body = dict(category=category.name, filter=filter)
155
+ if name:
156
+ body["name"] = name
157
+ if expires:
158
+ body["expires"] = expires
159
+ res = self.post(
160
+ f"{self.account.hq}/scm/exceptions/objects",
161
+ json=body,
162
+ headers=self.account.headers,
163
+ timeout=10,
164
+ )
165
+ return res.json()
166
+
167
+ @verify_credentials
168
+ def update_object_exception(
169
+ self, exception_id, expires=default, filter=None, name=None
170
+ ):
171
+ """Update an object exception"""
172
+ body = dict()
173
+ if expires != self.default:
174
+ body["expires"] = expires
175
+ if filter:
176
+ body["filter"] = filter
177
+ if name:
178
+ body["name"] = name
179
+ res = self.post(
180
+ f"{self.account.hq}/scm/exceptions/objects/{exception_id}",
181
+ json=body,
182
+ headers=self.account.headers,
183
+ timeout=10,
184
+ )
185
+ return res.json()
186
+
187
+ @verify_credentials
188
+ def delete_object_exception(self, exception_id):
189
+ """Delete an object exception"""
190
+ res = self.delete(
191
+ f"{self.account.hq}/scm/exceptions/objects/{exception_id}",
192
+ headers=self.account.headers,
193
+ timeout=10,
194
+ )
195
+ return res.json()
196
+
197
+ @verify_credentials
198
+ def list_policy_exceptions(self):
199
+ """List policy exceptions"""
200
+ res = self.get(
201
+ f"{self.account.hq}/scm/exceptions/policies",
202
+ headers=self.account.headers,
203
+ timeout=10,
204
+ )
205
+ return res.json()
206
+
207
+ @verify_credentials
208
+ def put_policy_exceptions(
209
+ self, partner: Control, expires, instance_id: str, policy_id, setting_names
210
+ ):
211
+ """Put policy exceptions"""
212
+ body = dict(
213
+ control=partner.name,
214
+ expires=expires,
215
+ instance_id=instance_id,
216
+ policy_id=policy_id,
217
+ setting_names=setting_names,
218
+ )
219
+ res = self.put(
220
+ f"{self.account.hq}/scm/exceptions/policies",
221
+ json=body,
222
+ headers=self.account.headers,
223
+ timeout=10,
224
+ )
225
+ return res.json()
226
+
227
+ @verify_credentials
228
+ def list_views(self):
229
+ """List views"""
230
+ res = self.get(
231
+ f"{self.account.hq}/scm/views",
232
+ headers=self.account.headers,
233
+ timeout=10,
234
+ )
235
+ return res.json()
236
+
237
+ @verify_credentials
238
+ def create_view(self, category: ControlCategory, filter: str, name: str):
239
+ """Create a view"""
240
+ body = dict(category=category.name, filter=filter, name=name)
241
+ res = self.post(
242
+ f"{self.account.hq}/scm/views",
243
+ json=body,
244
+ headers=self.account.headers,
245
+ timeout=10,
246
+ )
247
+ return res.json()
248
+
249
+ @verify_credentials
250
+ def update_view(
251
+ self, view_id, category: ControlCategory = None, filter=None, name=None
252
+ ):
253
+ """Update a view"""
254
+ body = dict()
255
+ if category:
256
+ body["category"] = category.name
257
+ if filter:
258
+ body["filter"] = filter
259
+ if name:
260
+ body["name"] = name
261
+ res = self.post(
262
+ f"{self.account.hq}/scm/views/{view_id}",
263
+ json=body,
264
+ headers=self.account.headers,
265
+ timeout=10,
266
+ )
267
+ return res.json()
268
+
269
+ @verify_credentials
270
+ def delete_view(self, view_id):
271
+ """Delete a view"""
272
+ res = self.delete(
273
+ f"{self.account.hq}/scm/views/{view_id}",
274
+ headers=self.account.headers,
275
+ timeout=10,
276
+ )
277
+ return res.json()
278
+
279
+ @verify_credentials
280
+ def create_threat(
281
+ self,
282
+ name,
283
+ description=None,
284
+ id=None,
285
+ generated=None,
286
+ published=None,
287
+ source=None,
288
+ source_id=None,
289
+ techniques=None,
290
+ ):
291
+ """Create an scm threat"""
292
+ body = dict(name=name)
293
+ if description:
294
+ body["description"] = description
295
+ if id:
296
+ body["id"] = id
297
+ if generated:
298
+ body["generated"] = generated
299
+ if published:
300
+ body["published"] = published
301
+ if source:
302
+ body["source"] = source
303
+ if source_id:
304
+ body["source_id"] = source_id
305
+ if techniques:
306
+ body["techniques"] = techniques
307
+
308
+ res = self.post(
309
+ f"{self.account.hq}/scm/threats",
310
+ json=body,
311
+ headers=self.account.headers,
312
+ timeout=10,
313
+ )
314
+ return res.json()
315
+
316
+ @verify_credentials
317
+ def delete_threat(self, id):
318
+ """Delete an existing scm threat"""
319
+ res = self.delete(
320
+ f"{self.account.hq}/scm/threats/{id}",
321
+ headers=self.account.headers,
322
+ timeout=10,
323
+ )
324
+ return res.json()
325
+
326
+ @verify_credentials
327
+ def get_threat(self, id):
328
+ """Get specific scm threat"""
329
+ res = self.get(
330
+ f"{self.account.hq}/scm/threats/{id}",
331
+ headers=self.account.headers,
332
+ timeout=10,
333
+ )
334
+ return res.json()
335
+
336
+ @verify_credentials
337
+ def list_threats(self):
338
+ """List all scm threats"""
339
+ res = self.get(
340
+ f"{self.account.hq}/scm/threats", headers=self.account.headers, timeout=10
341
+ )
342
+ return res.json()
343
+
344
+ @verify_credentials
345
+ def parse_threat_intel(self, file: str):
346
+ with open(file, "rb") as f:
347
+ body = f.read()
348
+ res = self.post(
349
+ f"{self.account.hq}/scm/threat-intel",
350
+ data=body,
351
+ headers=self.account.headers | {"Content-Type": "application/pdf"},
352
+ timeout=30,
353
+ )
354
+ return res.json()
355
+
356
+ @verify_credentials
357
+ def parse_from_partner_advisory(self, partner: Control, advisory_id: str):
358
+ params = dict(advisory_id=advisory_id)
359
+ res = self.post(
360
+ f"{self.account.hq}/scm/partner-advisories/{partner.name}",
361
+ headers=self.account.headers,
362
+ json=params,
363
+ timeout=30,
364
+ )
365
+ return res.json()
366
+
367
+ @verify_credentials
368
+ def list_notifications(self):
369
+ res = self.get(
370
+ f"{self.account.hq}/scm/notifications",
371
+ headers=self.account.headers,
372
+ timeout=10,
373
+ )
374
+ return res.json()
375
+
376
+ @verify_credentials
377
+ def delete_notification(self, notification_id: str):
378
+ res = self.delete(
379
+ f"{self.account.hq}/scm/notifications/{notification_id}",
380
+ headers=self.account.headers,
381
+ timeout=10,
382
+ )
383
+ return res.json()
384
+
385
+ @verify_credentials
386
+ def upsert_notification(
387
+ self,
388
+ control_category: ControlCategory,
389
+ event: PartnerEvents,
390
+ run_code: RunCode,
391
+ scheduled_hour: int,
392
+ emails: list[str] = None,
393
+ filter: str = None,
394
+ id: str = None,
395
+ message: str = "",
396
+ slack_urls: list[str] = None,
397
+ suppress_empty: bool = True,
398
+ teams_urls: list[str] = None,
399
+ title: str = "SCM Notification",
400
+ ):
401
+ body = dict(
402
+ control_category=control_category.name,
403
+ event=event.name,
404
+ run_code=run_code.name,
405
+ scheduled_hour=scheduled_hour,
406
+ suppress_empty=suppress_empty,
407
+ )
408
+ if id:
409
+ body["id"] = id
410
+ if filter:
411
+ body["filter"] = filter
412
+ if emails:
413
+ body["email"] = dict(emails=emails, message=message, subject=title)
414
+ if slack_urls:
415
+ body["slack"] = dict(hook_urls=slack_urls, message=message)
416
+ if teams_urls:
417
+ body["teams"] = dict(hook_urls=teams_urls, message=message)
418
+ res = self.put(
419
+ f"{self.account.hq}/scm/notifications",
420
+ json=body,
421
+ headers=self.account.headers,
422
+ timeout=10,
423
+ )
424
+ return res.json()
File without changes
@@ -0,0 +1,264 @@
1
+ import configparser
2
+ import json
3
+ import os
4
+ from functools import wraps
5
+ from pathlib import Path
6
+
7
+ import requests
8
+
9
+
10
+ class Keychain:
11
+
12
+ def __init__(
13
+ self,
14
+ keychain_location: str | None = os.path.join(
15
+ Path.home(), ".prelude", "keychain.ini"
16
+ ),
17
+ ):
18
+ self.keychain_location = keychain_location
19
+ if self.keychain_location and not os.path.exists(self.keychain_location):
20
+ head, _ = os.path.split(Path(self.keychain_location))
21
+ Path(head).mkdir(parents=True, exist_ok=True)
22
+ open(self.keychain_location, "x").close()
23
+ self.configure_keychain("", "")
24
+
25
+ def read_keychain(self):
26
+ cfg = configparser.ConfigParser()
27
+ cfg.read(self.keychain_location)
28
+ return cfg
29
+
30
+ def configure_keychain(
31
+ self,
32
+ account,
33
+ handle,
34
+ hq="https://api.us1.preludesecurity.com",
35
+ oidc=None,
36
+ profile="default",
37
+ slug=None,
38
+ ):
39
+ cfg = self.read_keychain()
40
+ cfg[profile] = {"account": account, "handle": handle, "hq": hq}
41
+ if oidc:
42
+ cfg[profile]["oidc"] = oidc
43
+ if slug:
44
+ cfg[profile]["slug"] = slug
45
+ with open(self.keychain_location, "w") as f:
46
+ cfg.write(f)
47
+
48
+ def get_profile(self, profile="default") -> dict:
49
+ try:
50
+ cfg = self.read_keychain()
51
+ profile = next(s for s in cfg.sections() if s == profile)
52
+ return dict(cfg[profile].items())
53
+ except StopIteration:
54
+ raise Exception(
55
+ "Could not find profile %s for account in %s"
56
+ % (profile, self.keychain_location)
57
+ )
58
+
59
+
60
+ def exchange_token(
61
+ account: str, handle: str, hq: str, auth_flow: str, auth_params: dict
62
+ ):
63
+ """
64
+ Two token exchange auth flows:
65
+ 1) Password auth: auth_flow = "password", auth_params = {"password": "your_password"}
66
+ 2) Refresh token auth: auth_flow = "refresh", auth_params = {"refresh_token": "your_refresh_token"}
67
+ 3) Exchange an OIDC authorization code for tokens:
68
+ auth_flow = "oauth_code", auth_params = {"code": "your_authorization_code", "verifier": "your_verifier", "source": "cli"}
69
+ """
70
+ res = requests.post(
71
+ f"{hq}/iam/token",
72
+ headers=dict(account=account, _product="py-sdk"),
73
+ json=dict(auth_flow=auth_flow, handle=handle, **auth_params),
74
+ timeout=10,
75
+ )
76
+ if res.status_code == 401:
77
+ raise Exception("Error logging in: Unauthorized")
78
+ if not res.ok:
79
+ raise Exception("Error logging in: %s" % res.text)
80
+ return res.json()
81
+
82
+
83
+ class Account:
84
+
85
+ @staticmethod
86
+ def from_keychain(profile: str = "default"):
87
+ """
88
+ Create an account object from a pre-configured profile in your keychain file
89
+ """
90
+ keychain = Keychain()
91
+ profile_items = keychain.get_profile(profile)
92
+ if any([item not in profile_items for item in ["account", "handle", "hq"]]):
93
+ raise ValueError(
94
+ "Please make sure you are using an up-to-date profile with the following fields: account, handle, hq"
95
+ )
96
+ return _Account(
97
+ account=profile_items["account"],
98
+ handle=profile_items["handle"],
99
+ hq=profile_items["hq"],
100
+ oidc=profile_items.get("oidc"),
101
+ profile=profile,
102
+ slug=profile_items.get("slug"),
103
+ )
104
+
105
+ @staticmethod
106
+ def from_token(
107
+ account: str,
108
+ handle: str,
109
+ token: str | None = None,
110
+ refresh_token: str | None = None,
111
+ hq: str = "https://api.us1.preludesecurity.com",
112
+ oidc: str | None = None,
113
+ slug: str | None = None,
114
+ ):
115
+ """
116
+ Create an account object from an access token or a refresh token
117
+ """
118
+ if not any([token, refresh_token]):
119
+ raise ValueError("Please provide either an access token or a refresh token")
120
+ if refresh_token:
121
+ res = exchange_token(
122
+ account, handle, hq, "refresh", dict(refresh_token=refresh_token)
123
+ )
124
+ token = res["token"]
125
+ return _Account(
126
+ account,
127
+ handle,
128
+ hq,
129
+ keychain_location=None,
130
+ oidc=oidc,
131
+ slug=slug,
132
+ token=token,
133
+ token_location=None,
134
+ )
135
+
136
+
137
+ class _Account:
138
+
139
+ def __init__(
140
+ self,
141
+ account: str,
142
+ handle: str,
143
+ hq: str,
144
+ oidc: str | None = None,
145
+ profile: str | None = None,
146
+ slug: str | None = None,
147
+ token: str | None = None,
148
+ keychain_location: str | None = os.path.join(
149
+ Path.home(), ".prelude", "keychain.ini"
150
+ ),
151
+ token_location: str | None = os.path.join(
152
+ Path.home(), ".prelude", "tokens.json"
153
+ ),
154
+ ):
155
+ if token is None and token_location is None:
156
+ raise ValueError(
157
+ "Please provide either an access token or a token location"
158
+ )
159
+
160
+ super().__init__()
161
+ self.account = account
162
+ self.handle = handle
163
+ self.headers = dict(account=account, _product="py-sdk")
164
+ self.hq = hq
165
+ self.keychain = Keychain(keychain_location)
166
+ self.oidc = oidc
167
+ self.profile = profile
168
+ self.slug = slug
169
+ self.token = token
170
+ self.token_location = token_location
171
+ if self.token_location and not os.path.exists(self.token_location):
172
+ head, _ = os.path.split(Path(self.token_location))
173
+ Path(head).mkdir(parents=True, exist_ok=True)
174
+ with open(self.token_location, "x") as f:
175
+ json.dump({}, f)
176
+ self.source = "cli" if self.oidc else "main"
177
+
178
+ @property
179
+ def token_key(self):
180
+ return f"{self.handle}/{self.oidc}" if self.oidc else self.handle
181
+
182
+ def _read_tokens(self):
183
+ with open(self.token_location, "r") as f:
184
+ return json.load(f)
185
+
186
+ def save_new_token(self, new_tokens):
187
+ existing_tokens = self._read_tokens()
188
+ if self.token_key not in existing_tokens:
189
+ existing_tokens[self.token_key] = dict()
190
+ existing_tokens[self.token_key][self.hq] = new_tokens
191
+ with open(self.token_location, "w") as f:
192
+ json.dump(existing_tokens, f)
193
+
194
+ def _verify(self):
195
+ if not self.token_location:
196
+ raise ValueError("Please provide a token location to continue")
197
+ if self.profile and not any([self.handle, self.account]):
198
+ raise ValueError(
199
+ "Please configure your %s profile to continue" % self.profile
200
+ )
201
+
202
+ def password_login(self, password, new_password=None):
203
+ self._verify()
204
+ tokens = exchange_token(
205
+ self.account,
206
+ self.handle,
207
+ self.hq,
208
+ "password_change" if new_password else "password",
209
+ dict(password=password, new_password=new_password),
210
+ )
211
+ self.save_new_token(tokens)
212
+ return tokens
213
+
214
+ def refresh_tokens(self):
215
+ self._verify()
216
+ existing_tokens = self._read_tokens().get(self.token_key, {}).get(self.hq, {})
217
+ if not (refresh_token := existing_tokens.get("refresh_token")):
218
+ raise Exception("No refresh token found, please login first to continue")
219
+ tokens = exchange_token(
220
+ self.account,
221
+ self.handle,
222
+ self.hq,
223
+ "refresh",
224
+ dict(refresh_token=refresh_token, source=self.source),
225
+ )
226
+ tokens = existing_tokens | tokens
227
+ self.save_new_token(tokens)
228
+ return tokens
229
+
230
+ def exchange_authorization_code(self, authorization_code: str, verifier: str):
231
+ self._verify()
232
+ tokens = exchange_token(
233
+ self.account,
234
+ self.handle,
235
+ self.hq,
236
+ "oauth_code",
237
+ dict(code=authorization_code, verifier=verifier, source=self.source),
238
+ )
239
+ existing_tokens = self._read_tokens().get(self.token_key, {}).get(self.hq, {})
240
+ tokens = existing_tokens | tokens
241
+ self.save_new_token(tokens)
242
+ return tokens
243
+
244
+ def get_token(self):
245
+ if self.token:
246
+ return self.token
247
+
248
+ tokens = self._read_tokens().get(self.token_key, {}).get(self.hq, {})
249
+ if "token" not in tokens:
250
+ raise Exception("Please login to continue")
251
+ return tokens["token"]
252
+
253
+ def update_auth_header(self):
254
+ self.headers |= dict(authorization=f"Bearer {self.get_token()}")
255
+
256
+
257
+ def verify_credentials(func):
258
+ @wraps(verify_credentials)
259
+ def handler(*args, **kwargs):
260
+ args[0].account.update_auth_header()
261
+ return func(*args, **kwargs)
262
+
263
+ handler.__wrapped__ = func
264
+ return handler