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.
- prelude_cli_beta/__init__.py +0 -0
- prelude_cli_beta/cli.py +52 -0
- prelude_cli_beta/templates/__init__.py +0 -0
- prelude_cli_beta/views/__init__.py +0 -0
- prelude_cli_beta/views/auth.py +56 -0
- prelude_cli_beta/views/build.py +488 -0
- prelude_cli_beta/views/configure.py +29 -0
- prelude_cli_beta/views/detect.py +438 -0
- prelude_cli_beta/views/generate.py +130 -0
- prelude_cli_beta/views/iam.py +368 -0
- prelude_cli_beta/views/jobs.py +50 -0
- prelude_cli_beta/views/partner.py +181 -0
- prelude_cli_beta/views/scm.py +881 -0
- prelude_cli_beta/views/shared.py +37 -0
- prelude_cli_beta-1446.dist-info/METADATA +38 -0
- prelude_cli_beta-1446.dist-info/RECORD +20 -0
- prelude_cli_beta-1446.dist-info/WHEEL +5 -0
- prelude_cli_beta-1446.dist-info/entry_points.txt +3 -0
- prelude_cli_beta-1446.dist-info/licenses/LICENSE +9 -0
- prelude_cli_beta-1446.dist-info/top_level.txt +1 -0
|
@@ -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)
|