prelude-cli-beta 1446__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.
@@ -0,0 +1,438 @@
1
+ import asyncio
2
+ import click
3
+ import yaml
4
+
5
+ from datetime import datetime, time, timedelta, timezone
6
+ from dateutil.parser import parse
7
+ from pathlib import Path, PurePath
8
+
9
+ from prelude_cli_beta.views.shared import Spinner, pretty_print
10
+ from prelude_sdk_beta.controllers.detect_controller import DetectController
11
+ from prelude_sdk_beta.controllers.iam_controller import IAMAccountController
12
+ from prelude_sdk_beta.models.codes import Control, RunCode
13
+
14
+
15
+ @click.group()
16
+ @click.pass_context
17
+ def detect(ctx):
18
+ """Continuous security testing"""
19
+ ctx.obj = DetectController(account=ctx.obj)
20
+
21
+
22
+ @detect.command("create-endpoint")
23
+ @click.option("-h", "--host", help="hostname of this machine", type=str, required=True)
24
+ @click.option(
25
+ "-s", "--serial_num", help="serial number of this machine", type=str, required=True
26
+ )
27
+ @click.option(
28
+ "-r",
29
+ "--reg_string",
30
+ help="registration string, in the format of <account_id>/<service_user_token>",
31
+ type=str,
32
+ required=True,
33
+ )
34
+ @click.option(
35
+ "-t",
36
+ "--tags",
37
+ help="a comma-separated list of tags for this endpoint",
38
+ type=str,
39
+ default=None,
40
+ )
41
+ @click.pass_obj
42
+ @pretty_print
43
+ def register_endpoint(controller, host, serial_num, reg_string, tags):
44
+ """Register a new endpoint"""
45
+ with Spinner(description="Registering endpoint"):
46
+ token = controller.register_endpoint(
47
+ host=host, serial_num=serial_num, reg_string=reg_string, tags=tags
48
+ )
49
+ return dict(token=token)
50
+
51
+
52
+ @detect.command("update-endpoint")
53
+ @click.argument("endpoint_id")
54
+ @click.option(
55
+ "-t",
56
+ "--tags",
57
+ help="a comma-separated list of tags for this endpoint",
58
+ type=str,
59
+ default=None,
60
+ )
61
+ @click.pass_obj
62
+ @pretty_print
63
+ def update_endpoint(controller, endpoint_id, tags):
64
+ """Update an existing endpoint"""
65
+ with Spinner(description="Updating endpoint"):
66
+ return controller.update_endpoint(endpoint_id=endpoint_id, tags=tags)
67
+
68
+
69
+ @detect.command("tests")
70
+ @click.option("--techniques", help="comma-separated list of techniques", type=str)
71
+ @click.pass_obj
72
+ @pretty_print
73
+ def list_tests(controller, techniques):
74
+ """List all security tests"""
75
+ with Spinner(description="Fetching all security tests"):
76
+ filters = dict()
77
+ if techniques:
78
+ filters["techniques"] = techniques
79
+ return controller.list_tests(filters=filters)
80
+
81
+
82
+ @detect.command("test")
83
+ @click.argument("test_id")
84
+ @click.pass_obj
85
+ @pretty_print
86
+ def get_test(controller, test_id):
87
+ """List properties for a test"""
88
+ with Spinner(description="Fetching data for test"):
89
+ return controller.get_test(test_id=test_id)
90
+
91
+
92
+ @detect.command("techniques")
93
+ @click.pass_obj
94
+ @pretty_print
95
+ def list_techniquess(controller):
96
+ """List techniques"""
97
+ with Spinner(description="Fetching techniques"):
98
+ return controller.list_techniques()
99
+
100
+
101
+ @detect.command("threats")
102
+ @click.pass_obj
103
+ @pretty_print
104
+ def list_threats(controller):
105
+ """List all threats"""
106
+ with Spinner(description="Fetching threats"):
107
+ return controller.list_threats()
108
+
109
+
110
+ @detect.command("threat")
111
+ @click.argument("threat_id")
112
+ @click.pass_obj
113
+ @pretty_print
114
+ def get_threat(controller, threat_id):
115
+ """List properties for a threat"""
116
+ with Spinner(description="Fetching data for threat"):
117
+ return controller.get_threat(threat_id=threat_id)
118
+
119
+
120
+ @detect.command("detections")
121
+ @click.pass_obj
122
+ @pretty_print
123
+ def list_detections(controller):
124
+ """List all Prelude detections"""
125
+ with Spinner(description="Fetching detections"):
126
+ return controller.list_detections()
127
+
128
+
129
+ @detect.command("detection")
130
+ @click.argument("detection_id")
131
+ @click.option(
132
+ "-o",
133
+ "--output_file",
134
+ help="write the sigma rule to a file in yaml format",
135
+ type=click.Path(writable=True),
136
+ )
137
+ @click.pass_obj
138
+ @pretty_print
139
+ def get_detection(controller, detection_id, output_file):
140
+ """List properties for a detection"""
141
+ with Spinner(description="Fetching data for detection"):
142
+ data = controller.get_detection(detection_id=detection_id)
143
+ if output_file:
144
+ with open(output_file, "w") as f:
145
+ f.write(yaml.safe_dump(data["rule"]))
146
+ return data
147
+
148
+
149
+ @detect.command("threat-hunts")
150
+ @click.option("--tests", help="comma-separated list of tests", type=str)
151
+ @click.pass_obj
152
+ @pretty_print
153
+ def list_threat_hunts(controller, tests):
154
+ """List threat hunts"""
155
+ with Spinner(description="Fetching threat hunts"):
156
+ filters = dict()
157
+ if tests:
158
+ filters["tests"] = tests
159
+ return controller.list_threat_hunts(filters)
160
+
161
+
162
+ @detect.command("threat-hunt")
163
+ @click.argument("threat_hunt_id")
164
+ @click.pass_obj
165
+ @pretty_print
166
+ def get_threat_hunt(controller, threat_hunt_id):
167
+ """List properties for a threat hunt"""
168
+ with Spinner(description="Fetching data for threat hunt"):
169
+ return controller.get_threat_hunt(threat_hunt_id=threat_hunt_id)
170
+
171
+
172
+ @detect.command("do-threat-hunt")
173
+ @click.argument("threat_hunt_id")
174
+ @click.pass_obj
175
+ @pretty_print
176
+ def do_threat_hunt(controller, threat_hunt_id):
177
+ """Run a threat hunt query"""
178
+ with Spinner(description="Running threat hunt"):
179
+ return controller.do_threat_hunt(threat_hunt_id=threat_hunt_id)
180
+
181
+
182
+ @detect.command("download")
183
+ @click.argument("test")
184
+ @click.pass_obj
185
+ @pretty_print
186
+ def download(controller, test):
187
+ """Download a test to your local environment"""
188
+ Path(test).mkdir(parents=True, exist_ok=True)
189
+ with Spinner(description="Downloading test"):
190
+ attachments = controller.get_test(test_id=test).get("attachments")
191
+
192
+ for attach in attachments:
193
+ code = controller.download(test_id=test, filename=attach)
194
+ with open(PurePath(test, attach), "wb") as f:
195
+ f.write(code)
196
+
197
+
198
+ @detect.command("schedule")
199
+ @click.argument("id")
200
+ @click.option(
201
+ "-t",
202
+ "--type",
203
+ help="whether you are scheduling a test or threat",
204
+ required=True,
205
+ type=click.Choice(["TEST", "THREAT"], case_sensitive=False),
206
+ )
207
+ @click.option(
208
+ "--tags",
209
+ help="only enable for these tags (comma-separated list)",
210
+ type=str,
211
+ default="",
212
+ )
213
+ @click.option(
214
+ "-r",
215
+ "--run_code",
216
+ help="provide a run_code",
217
+ default=RunCode.DAILY.name,
218
+ show_default=True,
219
+ type=click.Choice(
220
+ [r.name for r in RunCode if r != RunCode.INVALID], case_sensitive=False
221
+ ),
222
+ )
223
+ @click.pass_obj
224
+ @pretty_print
225
+ def schedule(controller, id, type, run_code, tags):
226
+ """Add test or threat to your queue"""
227
+ with Spinner(description=f"Scheduling {type.lower()}"):
228
+ if type == "TEST":
229
+ return controller.schedule([dict(test_id=id, run_code=run_code, tags=tags)])
230
+ else:
231
+ return controller.schedule(
232
+ [dict(threat_id=id, run_code=run_code, tags=tags)]
233
+ )
234
+
235
+
236
+ @detect.command("unschedule")
237
+ @click.argument("id")
238
+ @click.option(
239
+ "-t",
240
+ "--type",
241
+ help="whether you are unscheduling a test or threat",
242
+ required=True,
243
+ type=click.Choice(["TEST", "THREAT"], case_sensitive=False),
244
+ )
245
+ @click.option(
246
+ "--tags",
247
+ help="only disable for these tags (comma-separated list)",
248
+ type=str,
249
+ default="",
250
+ )
251
+ @click.confirmation_option(prompt="Are you sure?")
252
+ @click.pass_obj
253
+ @pretty_print
254
+ def unschedule(controller, id, type, tags):
255
+ """Remove test or threat from your queue"""
256
+ with Spinner(description=f"Unscheduling {type.lower()}"):
257
+ if type == "TEST":
258
+ return controller.unschedule([dict(test_id=id, tags=tags)])
259
+ else:
260
+ return controller.unschedule([dict(threat_id=id, tags=tags)])
261
+
262
+
263
+ @detect.command("delete-endpoint")
264
+ @click.argument("endpoint_id")
265
+ @click.confirmation_option(prompt="Are you sure?")
266
+ @click.pass_obj
267
+ @pretty_print
268
+ def delete_endpoint(controller, endpoint_id):
269
+ """Delete a probe/endpoint"""
270
+ with Spinner(description="Deleting endpoint"):
271
+ return controller.delete_endpoint(ident=endpoint_id)
272
+
273
+
274
+ @detect.command("queue")
275
+ @click.pass_obj
276
+ @pretty_print
277
+ def queue(controller):
278
+ """List all tests in your active queue"""
279
+ with Spinner(description="Fetching active tests from queue"):
280
+ iam = IAMAccountController(account=controller.account)
281
+ return iam.get_account().get("queue")
282
+
283
+
284
+ @detect.command("endpoints")
285
+ @click.option(
286
+ "-d",
287
+ "--days",
288
+ help="only show endpoints that have run at least once in the past DAYS days",
289
+ default=90,
290
+ type=int,
291
+ )
292
+ @click.pass_obj
293
+ @pretty_print
294
+ def endpoints(controller, days):
295
+ """List all active endpoints associated to your account"""
296
+ with Spinner(description="Fetching endpoints"):
297
+ return controller.list_endpoints(days=days)
298
+
299
+
300
+ @detect.command("clone")
301
+ @click.pass_obj
302
+ @pretty_print
303
+ def clone(controller):
304
+ """Download all tests to your local environment"""
305
+
306
+ async def fetch(test):
307
+ Path(test["id"]).mkdir(parents=True, exist_ok=True)
308
+
309
+ for attach in controller.get_test(test_id=test["id"]).get("attachments"):
310
+ code = controller.download(test_id=test["id"], filename=attach)
311
+ with open(PurePath(test["id"], attach), "wb") as f:
312
+ f.write(code)
313
+
314
+ async def start_cloning():
315
+ await asyncio.gather(*[fetch(test) for test in controller.list_tests()])
316
+
317
+ with Spinner(description="Downloading all tests"):
318
+ asyncio.run(start_cloning())
319
+
320
+
321
+ @detect.command("activity")
322
+ @click.option(
323
+ "--view",
324
+ help="retrieve a specific result view",
325
+ default="logs",
326
+ show_default=True,
327
+ type=click.Choice(
328
+ [
329
+ "endpoints",
330
+ "findings",
331
+ "logs",
332
+ "metrics",
333
+ "protected",
334
+ "techniques",
335
+ "tests",
336
+ "threats",
337
+ ]
338
+ ),
339
+ )
340
+ @click.option(
341
+ "--control", type=click.Choice([c.name for c in Control], case_sensitive=False)
342
+ )
343
+ @click.option("--dos", help="comma-separated list of DOS", type=str)
344
+ @click.option("--endpoints", help="comma-separated list of endpoint IDs", type=str)
345
+ @click.option("--finish", help="end date of activity (end of day)", type=str)
346
+ @click.option("--os", help="comma-separated list of OS", type=str)
347
+ @click.option("--policy", help="comma-separated list of policies", type=str)
348
+ @click.option(
349
+ "--social",
350
+ help="whether to fetch account-specific or social stats. Applicable to the following views: protected",
351
+ is_flag=True,
352
+ )
353
+ @click.option("--start", help="start date of activity (beginning of day)", type=str)
354
+ @click.option("--statuses", help="comma-separated list of statuses", type=str)
355
+ @click.option("--techniques", help="comma-separated list of techniques", type=str)
356
+ @click.option("--tests", help="comma-separated list of test IDs", type=str)
357
+ @click.option("--threats", help="comma-separated list of threat IDs", type=str)
358
+ @click.pass_obj
359
+ @pretty_print
360
+ def describe_activity(
361
+ controller,
362
+ control,
363
+ dos,
364
+ endpoints,
365
+ finish,
366
+ os,
367
+ policy,
368
+ social,
369
+ start,
370
+ statuses,
371
+ techniques,
372
+ tests,
373
+ threats,
374
+ view,
375
+ ):
376
+ """View my Detect results"""
377
+ start = parse(start) if start else datetime.now(timezone.utc) - timedelta(days=29)
378
+ finish = parse(finish) if finish else datetime.now(timezone.utc)
379
+ filters = dict(
380
+ start=datetime.combine(start, time.min),
381
+ finish=datetime.combine(finish, time.max),
382
+ )
383
+ if control:
384
+ filters["control"] = Control[control.upper()].value
385
+ if dos:
386
+ filters["dos"] = dos
387
+ if endpoints:
388
+ filters["endpoints"] = endpoints
389
+ if os:
390
+ filters["os"] = os
391
+ if policy:
392
+ filters["policy"] = policy
393
+ if social:
394
+ filters["impersonate"] = "social"
395
+ if statuses:
396
+ filters["statuses"] = statuses
397
+ if techniques:
398
+ filters["techniques"] = techniques
399
+ if tests:
400
+ filters["tests"] = tests
401
+ if threats:
402
+ filters["threats"] = threats
403
+
404
+ with Spinner(description="Fetching activity"):
405
+ return controller.describe_activity(view=view, filters=filters)
406
+
407
+
408
+ @detect.command("threat-hunt-activity")
409
+ @click.argument("id")
410
+ @click.option(
411
+ "-t",
412
+ "--type",
413
+ help="whether you are getting activity for a threat hunt, test, or threat",
414
+ required=True,
415
+ type=click.Choice(["THREAT_HUNT", "TEST", "THREAT"], case_sensitive=False),
416
+ )
417
+ @click.pass_obj
418
+ @pretty_print
419
+ def threat_hunt_activity(controller, id, type):
420
+ """Get threat hunt activity"""
421
+ with Spinner(description="Fetching threat hunt activity"):
422
+ if type == "THREAT_HUNT":
423
+ return controller.threat_hunt_activity(threat_hunt_id=id)
424
+ elif type == "TEST":
425
+ return controller.threat_hunt_activity(test_id=id)
426
+ else:
427
+ return controller.threat_hunt_activity(threat_id=id)
428
+
429
+
430
+ @detect.command("accept-terms", hidden=True)
431
+ @click.argument("name", type=str, required=True)
432
+ @click.option("-v", "--version", type=str, required=True)
433
+ @click.pass_obj
434
+ @pretty_print
435
+ def accept_terms(controller, name, version):
436
+ """Accept terms and conditions"""
437
+ with Spinner(description="Accepting terms and conditions"):
438
+ return controller.accept_terms(name=name, version=version)
@@ -0,0 +1,130 @@
1
+ import json
2
+ import os
3
+
4
+ import click
5
+
6
+ from prelude_cli_beta.views.shared import Spinner, pretty_print
7
+ from prelude_sdk_beta.controllers.generate_controller import GenerateController
8
+ from prelude_sdk_beta.models.codes import Control
9
+
10
+
11
+ @click.group()
12
+ @click.pass_context
13
+ def generate(ctx):
14
+ """Generate tests"""
15
+ ctx.obj = GenerateController(account=ctx.obj)
16
+
17
+
18
+ def _process_results(result: dict, output_dir: str, job_id: str) -> dict:
19
+ if result["status"] == "COMPLETE":
20
+ for technique in result["output"]:
21
+ if technique["status"] == "SUCCEEDED":
22
+ technique_directory = technique["technique"].replace(".", "_")
23
+ os.makedirs(f"{output_dir}/{technique_directory}")
24
+ content = technique.get("ai_generated") or technique.get(
25
+ "existing_test"
26
+ )
27
+ with open(f"{output_dir}/{technique_directory}/test.go", "w") as f:
28
+ f.write(content["go_code"])
29
+ for i, sigma_rule in enumerate(content["sigma_rules"]):
30
+ with open(
31
+ f"{output_dir}/{technique_directory}/sigma_{i}.yaml", "w"
32
+ ) as f:
33
+ f.write(sigma_rule)
34
+ for i, query in enumerate(content.get("threat_hunt_queries", [])):
35
+ with open(
36
+ f"{output_dir}/{technique_directory}/query_{i}.json", "w"
37
+ ) as f:
38
+ json.dump(
39
+ dict(name=query["name"], query=query["query"]), f, indent=4
40
+ )
41
+ with open(f"{output_dir}/{technique_directory}/config.json", "w") as f:
42
+ json.dump(
43
+ dict(
44
+ technique=technique["technique"],
45
+ name=technique["name"],
46
+ unit="response",
47
+ ),
48
+ f,
49
+ indent=4,
50
+ )
51
+ if content["readme"]:
52
+ with open(
53
+ f"{output_dir}/{technique_directory}/README.md", "w"
54
+ ) as f:
55
+ f.write(content["readme"])
56
+ return dict(
57
+ output_dir=output_dir,
58
+ successfully_generated=[
59
+ t["technique"] for t in result["output"] if t["status"] == "SUCCEEDED"
60
+ ],
61
+ failed=[
62
+ t["technique"] for t in result["output"] if t["status"] == "FAILED"
63
+ ],
64
+ job_id=job_id,
65
+ )
66
+ else:
67
+ raise Exception(
68
+ "Failed to generate threat intel: %s (ref: %s)", result["reason"], job_id
69
+ )
70
+
71
+
72
+ @generate.command("threat-intel")
73
+ @click.argument(
74
+ "threat_pdf",
75
+ type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
76
+ )
77
+ @click.argument(
78
+ "output_dir", type=click.Path(dir_okay=True, file_okay=False, writable=True)
79
+ )
80
+ @click.pass_obj
81
+ @pretty_print
82
+ def generate_threat_intel(
83
+ controller: GenerateController, threat_pdf: str, output_dir: str
84
+ ):
85
+ with Spinner("Uploading") as spinner:
86
+ job_id = controller.upload_threat_intel(threat_pdf)["job_id"]
87
+ spinner.update(spinner.task_ids[-1], description="Parsing PDF")
88
+ while (result := controller.get_threat_intel(job_id)) and result[
89
+ "status"
90
+ ] == "RUNNING":
91
+ if result["step"] == "GENERATE":
92
+ spinner.update(
93
+ spinner.task_ids[-1],
94
+ description=f'Generating ({result["completed_tasks"]}/{result["num_tasks"]})',
95
+ )
96
+ return _process_results(result, output_dir, job_id)
97
+
98
+
99
+ @generate.command("from-advisory")
100
+ @click.argument(
101
+ "partner", type=click.Choice([Control.CROWDSTRIKE.name], case_sensitive=False)
102
+ )
103
+ @click.option(
104
+ "-a", "--advisory_id", required=True, type=str, help="Partner advisory ID"
105
+ )
106
+ @click.option(
107
+ "-o",
108
+ "--output_dir",
109
+ required=True,
110
+ type=click.Path(dir_okay=True, file_okay=False, writable=True),
111
+ )
112
+ @click.pass_obj
113
+ @pretty_print
114
+ def generate_from_partner_advisory(
115
+ controller: GenerateController, partner: Control, advisory_id: str, output_dir: str
116
+ ):
117
+ with Spinner("Uploading") as spinner:
118
+ job_id = controller.generate_from_partner_advisory(
119
+ partner=Control[partner], advisory_id=advisory_id
120
+ )["job_id"]
121
+ spinner.update(spinner.task_ids[-1], description="Parsing PDF")
122
+ while (result := controller.get_threat_intel(job_id)) and result[
123
+ "status"
124
+ ] == "RUNNING":
125
+ if result["step"] == "GENERATE":
126
+ spinner.update(
127
+ spinner.task_ids[-1],
128
+ description=f'Generating ({result["completed_tasks"]}/{result["num_tasks"]})',
129
+ )
130
+ return _process_results(result, output_dir, job_id)