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
|
File without changes
|
prelude_cli_beta/cli.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from prelude_cli_beta.views.auth import auth
|
|
4
|
+
from prelude_cli_beta.views.build import build
|
|
5
|
+
from prelude_cli_beta.views.configure import configure
|
|
6
|
+
from prelude_cli_beta.views.detect import detect
|
|
7
|
+
from prelude_cli_beta.views.generate import generate
|
|
8
|
+
from prelude_cli_beta.views.iam import iam
|
|
9
|
+
from prelude_cli_beta.views.jobs import jobs
|
|
10
|
+
from prelude_cli_beta.views.partner import partner
|
|
11
|
+
from prelude_cli_beta.views.scm import scm
|
|
12
|
+
from prelude_sdk_beta.models.account import Account, Keychain
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def complete_profile(ctx, param, incomplete):
|
|
16
|
+
return [x for x in Keychain().read_keychain() if x.startswith(incomplete)]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group(invoke_without_command=True)
|
|
20
|
+
@click.version_option()
|
|
21
|
+
@click.pass_context
|
|
22
|
+
@click.option(
|
|
23
|
+
"--profile",
|
|
24
|
+
default="default",
|
|
25
|
+
help="The prelude keychain profile to use",
|
|
26
|
+
show_default=True,
|
|
27
|
+
shell_complete=complete_profile,
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--resolve_enums",
|
|
31
|
+
is_flag=True,
|
|
32
|
+
help="Resolve enum values to their string representation",
|
|
33
|
+
)
|
|
34
|
+
def cli(ctx, profile, resolve_enums):
|
|
35
|
+
ctx.obj = Account.from_keychain(profile=profile, resolve_enums=resolve_enums)
|
|
36
|
+
if ctx.invoked_subcommand is None:
|
|
37
|
+
click.echo(ctx.get_help())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
cli.add_command(auth)
|
|
41
|
+
cli.add_command(build)
|
|
42
|
+
cli.add_command(configure)
|
|
43
|
+
cli.add_command(detect)
|
|
44
|
+
cli.add_command(generate)
|
|
45
|
+
cli.add_command(iam)
|
|
46
|
+
cli.add_command(jobs)
|
|
47
|
+
cli.add_command(partner)
|
|
48
|
+
cli.add_command(scm)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
cli()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import webbrowser
|
|
3
|
+
|
|
4
|
+
from prelude_cli_beta.views.shared import Spinner, pretty_print
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
@click.pass_context
|
|
9
|
+
def auth(ctx):
|
|
10
|
+
"""Authentication"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@auth.command("login")
|
|
15
|
+
@click.option("-p", "--password", type=str, help="password for login")
|
|
16
|
+
@click.option(
|
|
17
|
+
"-t",
|
|
18
|
+
"--temp_password",
|
|
19
|
+
type=str,
|
|
20
|
+
help="temporary password for login (if set, `password` will become your new password)",
|
|
21
|
+
)
|
|
22
|
+
@click.pass_obj
|
|
23
|
+
@pretty_print
|
|
24
|
+
def login(account, password, temp_password):
|
|
25
|
+
"""Login using password or SSO"""
|
|
26
|
+
if not account.oidc:
|
|
27
|
+
password = password or click.prompt("Password", type=str, hide_input=True)
|
|
28
|
+
with Spinner(description="Logging in and saving tokens"):
|
|
29
|
+
new_password = password if temp_password else None
|
|
30
|
+
password = temp_password or password
|
|
31
|
+
return (
|
|
32
|
+
account.password_login(password, new_password),
|
|
33
|
+
"Login with password successful",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
url = f"{account.hq.replace('api', 'platform')}/cli-auth?handle={account.handle}&provider={account.oidc}"
|
|
37
|
+
if account.oidc == "custom":
|
|
38
|
+
slug = account.slug or click.prompt("Please enter your account slug")
|
|
39
|
+
url += f"&slug={slug}"
|
|
40
|
+
webbrowser.open(url)
|
|
41
|
+
code = click.prompt(
|
|
42
|
+
f"Launching browser for authentication:\n\n{url}\n\nPlease enter your authorization code here"
|
|
43
|
+
)
|
|
44
|
+
verifier, authorization_code = code.split("/")
|
|
45
|
+
with Spinner(description="Logging in and saving tokens"):
|
|
46
|
+
tokens = account.exchange_authorization_code(authorization_code, verifier)
|
|
47
|
+
return tokens, "Login with SSO successful"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@auth.command("refresh")
|
|
51
|
+
@click.pass_obj
|
|
52
|
+
@pretty_print
|
|
53
|
+
def refresh(account):
|
|
54
|
+
"""Refresh your tokens"""
|
|
55
|
+
with Spinner(description="Refreshing tokens"):
|
|
56
|
+
return account.refresh_tokens(), "New access tokens saved"
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import importlib.resources as pkg_resources
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path, PurePath
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
import prelude_cli_beta.templates as templates
|
|
12
|
+
from prelude_cli_beta.views.shared import Spinner, pretty_print
|
|
13
|
+
from prelude_sdk_beta.controllers.build_controller import BuildController
|
|
14
|
+
from prelude_sdk_beta.models.codes import Control, EDRResponse
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
UUID = re.compile(
|
|
18
|
+
"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group()
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def build(ctx):
|
|
25
|
+
"""Custom security tests"""
|
|
26
|
+
ctx.obj = BuildController(account=ctx.obj)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@build.command("clone-test")
|
|
30
|
+
@click.argument("source-test-id")
|
|
31
|
+
@click.pass_obj
|
|
32
|
+
@pretty_print
|
|
33
|
+
def clone_test(controller, source_test_id):
|
|
34
|
+
"""Clone a security test"""
|
|
35
|
+
|
|
36
|
+
with Spinner(description="Creating new test"):
|
|
37
|
+
return controller.clone_test(
|
|
38
|
+
source_test_id=source_test_id,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@build.command("create-test")
|
|
43
|
+
@click.argument("name")
|
|
44
|
+
@click.option("-u", "--unit", required=True, help="unit identifier", type=str)
|
|
45
|
+
@click.option("-t", "--test", help="test identifier", default=None, type=str)
|
|
46
|
+
@click.option(
|
|
47
|
+
"-q", "--technique", help="MITRE ATT&CK code [e.g. T1557]", default=None, type=str
|
|
48
|
+
)
|
|
49
|
+
@click.pass_obj
|
|
50
|
+
@pretty_print
|
|
51
|
+
def create_test(controller, name, unit, test, technique):
|
|
52
|
+
"""Create a security test"""
|
|
53
|
+
|
|
54
|
+
def create_template(template, name):
|
|
55
|
+
template_body = pkg_resources.read_text(templates, template)
|
|
56
|
+
template_body = template_body.replace("$ID", res["id"])
|
|
57
|
+
template_body = template_body.replace("$NAME", res["name"])
|
|
58
|
+
template_body = template_body.replace("$UNIT", res["unit"])
|
|
59
|
+
template_body = template_body.replace("$TECHNIQUE", res["technique"] or "")
|
|
60
|
+
template_body = template_body.replace("$TIME", str(datetime.now(timezone.utc)))
|
|
61
|
+
|
|
62
|
+
with Spinner(description="Applying default template to new test"):
|
|
63
|
+
controller.upload(
|
|
64
|
+
test_id=res["id"], filename=name, data=template_body.encode("utf-8")
|
|
65
|
+
)
|
|
66
|
+
res["attachments"] += [name]
|
|
67
|
+
|
|
68
|
+
dir = PurePath(res["id"], name)
|
|
69
|
+
|
|
70
|
+
with open(dir, "w", encoding="utf8") as code:
|
|
71
|
+
code.write(template_body)
|
|
72
|
+
|
|
73
|
+
with Spinner(description="Creating new test"):
|
|
74
|
+
res = controller.create_test(
|
|
75
|
+
name=name, unit=unit, test_id=test, technique=technique
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not test:
|
|
79
|
+
Path(res["id"]).mkdir(parents=True, exist_ok=True)
|
|
80
|
+
create_template(template="README.md", name="README.md")
|
|
81
|
+
create_template(template="template.go", name=f'{res["id"]}.go')
|
|
82
|
+
return res
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@build.command("update-test")
|
|
86
|
+
@click.argument("test")
|
|
87
|
+
@click.option(
|
|
88
|
+
"-c",
|
|
89
|
+
"--crowdstrike_expected",
|
|
90
|
+
help="Crowdstrike expected outcome",
|
|
91
|
+
type=click.Choice(
|
|
92
|
+
[c.name for c in EDRResponse if c != EDRResponse.INVALID], case_sensitive=False
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
@click.option("-n", "--name", help="test name", default=None, type=str)
|
|
96
|
+
@click.option("-u", "--unit", help="unit identifier", default=None, type=str)
|
|
97
|
+
@click.option(
|
|
98
|
+
"-q", "--technique", help="MITRE ATT&CK code [e.g. T1557]", default=None, type=str
|
|
99
|
+
)
|
|
100
|
+
@click.pass_obj
|
|
101
|
+
@pretty_print
|
|
102
|
+
def update_test(controller, test, crowdstrike_expected, name, unit, technique):
|
|
103
|
+
"""Update a security test"""
|
|
104
|
+
with Spinner(description="Updating test"):
|
|
105
|
+
return controller.update_test(
|
|
106
|
+
test_id=test,
|
|
107
|
+
crowdstrike_expected_outcome=(
|
|
108
|
+
EDRResponse[crowdstrike_expected] if crowdstrike_expected else None
|
|
109
|
+
),
|
|
110
|
+
name=name,
|
|
111
|
+
unit=unit,
|
|
112
|
+
technique=technique,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@build.command("delete-test")
|
|
117
|
+
@click.argument("test")
|
|
118
|
+
@click.option("-p", "--purge", is_flag=True, help="purge test and associated files")
|
|
119
|
+
@click.confirmation_option(prompt="Are you sure?")
|
|
120
|
+
@click.pass_obj
|
|
121
|
+
@pretty_print
|
|
122
|
+
def delete_test(controller, test, purge):
|
|
123
|
+
"""Delete a test"""
|
|
124
|
+
with Spinner(description="Removing test"):
|
|
125
|
+
return controller.delete_test(test_id=test, purge=purge)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@build.command("compile-code-file")
|
|
129
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
130
|
+
@click.option("-s", "--source-test-id", help="Include source test attachments")
|
|
131
|
+
@click.pass_obj
|
|
132
|
+
@pretty_print
|
|
133
|
+
def compile_code_file(controller, path, source_test_id):
|
|
134
|
+
"""Test compile a go file, with test attachments if needed."""
|
|
135
|
+
|
|
136
|
+
with Spinner(description="Compiling code test") as spinner:
|
|
137
|
+
with open(path, "rb") as data:
|
|
138
|
+
data = controller.compile_code_string(
|
|
139
|
+
code=data.read(),
|
|
140
|
+
source_test_id=source_test_id,
|
|
141
|
+
)
|
|
142
|
+
if compile_job_id := data.get("job_id"):
|
|
143
|
+
spinner.update(spinner.task_ids[-1], description="Compiling")
|
|
144
|
+
while (
|
|
145
|
+
result := controller.get_compile_status(compile_job_id)
|
|
146
|
+
) and result["status"] == "RUNNING":
|
|
147
|
+
time.sleep(2)
|
|
148
|
+
if result["status"] == "FAILED":
|
|
149
|
+
result["error"] = "Failed to compile"
|
|
150
|
+
data |= result
|
|
151
|
+
return data
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@build.command("undelete-test")
|
|
155
|
+
@click.argument("test")
|
|
156
|
+
@click.pass_obj
|
|
157
|
+
@pretty_print
|
|
158
|
+
def undelete_test(controller, test):
|
|
159
|
+
"""Undelete a test"""
|
|
160
|
+
with Spinner(description="Restoring test"):
|
|
161
|
+
return controller.undelete_test(test_id=test)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@build.command("upload")
|
|
165
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
166
|
+
@click.option("-t", "--test", help="test identifier", default=None, type=str)
|
|
167
|
+
@click.pass_obj
|
|
168
|
+
@pretty_print
|
|
169
|
+
def upload_attachment(controller, path, test):
|
|
170
|
+
"""Upload a test attachment from disk"""
|
|
171
|
+
|
|
172
|
+
def test_id():
|
|
173
|
+
match = UUID.search(path)
|
|
174
|
+
if match:
|
|
175
|
+
return match.group(0)
|
|
176
|
+
raise FileNotFoundError("You must supply a test ID or include it in the path")
|
|
177
|
+
|
|
178
|
+
def upload(p: Path, skip_compile=False):
|
|
179
|
+
if not p.is_file():
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
with open(p, "rb") as data:
|
|
183
|
+
with Spinner(description="Uploading to test") as spinner:
|
|
184
|
+
data = controller.upload(
|
|
185
|
+
test_id=identifier,
|
|
186
|
+
filename=p.name,
|
|
187
|
+
data=data.read(),
|
|
188
|
+
skip_compile=skip_compile,
|
|
189
|
+
)
|
|
190
|
+
if data.get("compile_job_id"):
|
|
191
|
+
spinner.update(spinner.task_ids[-1], description="Compiling")
|
|
192
|
+
while (
|
|
193
|
+
result := controller.get_compile_status(data["compile_job_id"])
|
|
194
|
+
) and result["status"] == "RUNNING":
|
|
195
|
+
time.sleep(2)
|
|
196
|
+
if result["status"] == "FAILED":
|
|
197
|
+
result["error"] = "Failed to compile"
|
|
198
|
+
data |= result
|
|
199
|
+
res.append(data)
|
|
200
|
+
|
|
201
|
+
res = []
|
|
202
|
+
identifier = test or test_id()
|
|
203
|
+
|
|
204
|
+
if Path(path).is_file():
|
|
205
|
+
upload(p=Path(path))
|
|
206
|
+
else:
|
|
207
|
+
objs = list(Path(path).glob("*"))
|
|
208
|
+
for ind, obj in enumerate(objs):
|
|
209
|
+
try:
|
|
210
|
+
upload(p=Path(obj), skip_compile=ind != len(objs) - 1)
|
|
211
|
+
except ValueError as e:
|
|
212
|
+
res.append(dict(status="FAILED", reason=e.args[0]))
|
|
213
|
+
return res
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@build.command("create-threat")
|
|
217
|
+
@click.argument("name")
|
|
218
|
+
@click.option(
|
|
219
|
+
"-p", "--published", help="date the threat was published", required=True, type=str
|
|
220
|
+
)
|
|
221
|
+
@click.option("--id", help="identifier", type=str)
|
|
222
|
+
@click.option(
|
|
223
|
+
"-s", "--source", help="source of threat (ex. www.cisa.gov)", default=None, type=str
|
|
224
|
+
)
|
|
225
|
+
@click.option(
|
|
226
|
+
"-i",
|
|
227
|
+
"--source_id",
|
|
228
|
+
help="ID of the threat, per the source (ex. aa23-075a)",
|
|
229
|
+
default=None,
|
|
230
|
+
type=str,
|
|
231
|
+
)
|
|
232
|
+
@click.option(
|
|
233
|
+
"-t", "--tests", help="comma-separated list of test IDs", default=None, type=str
|
|
234
|
+
)
|
|
235
|
+
@click.option(
|
|
236
|
+
"-d",
|
|
237
|
+
"--directory",
|
|
238
|
+
help="directory containing tests, detections, and hunt queries generated from threat_intel",
|
|
239
|
+
default=None,
|
|
240
|
+
type=click.Path(exists=True, dir_okay=True, file_okay=False),
|
|
241
|
+
)
|
|
242
|
+
@click.pass_obj
|
|
243
|
+
@pretty_print
|
|
244
|
+
def create_threat(controller, name, published, id, source_id, source, tests, directory):
|
|
245
|
+
"""Create a security threat"""
|
|
246
|
+
with Spinner(description="Creating new threat"):
|
|
247
|
+
try:
|
|
248
|
+
created_tests = []
|
|
249
|
+
test_uploads = []
|
|
250
|
+
created_detections = []
|
|
251
|
+
created_queries = []
|
|
252
|
+
threat = None
|
|
253
|
+
if directory:
|
|
254
|
+
for technique_dir in os.listdir(directory):
|
|
255
|
+
with open(f"{directory}/{technique_dir}/config.json", "r") as f:
|
|
256
|
+
config = json.load(f)
|
|
257
|
+
test = controller.create_test(
|
|
258
|
+
name=config["name"],
|
|
259
|
+
unit=config["unit"],
|
|
260
|
+
technique=config["technique"],
|
|
261
|
+
)
|
|
262
|
+
created_tests.append(test)
|
|
263
|
+
with open(f"{directory}/{technique_dir}/test.go", "r") as f:
|
|
264
|
+
go_code = f.read()
|
|
265
|
+
test_uploads.append(
|
|
266
|
+
controller.upload(
|
|
267
|
+
test_id=test["id"],
|
|
268
|
+
filename=f'{test["id"]}.go',
|
|
269
|
+
data=go_code.encode(),
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
for sigma_file in Path(f"{directory}/{technique_dir}").glob(
|
|
273
|
+
"sigma*"
|
|
274
|
+
):
|
|
275
|
+
with open(sigma_file, "r") as f:
|
|
276
|
+
rule = f.read()
|
|
277
|
+
created_detections.append(
|
|
278
|
+
controller.create_detection(
|
|
279
|
+
rule=rule, test_id=test["id"]
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
for query_file in Path(f"{directory}/{technique_dir}").glob(
|
|
283
|
+
"query*"
|
|
284
|
+
):
|
|
285
|
+
with open(query_file, "r") as f:
|
|
286
|
+
query = json.load(f)
|
|
287
|
+
created_queries.append(
|
|
288
|
+
controller.create_threat_hunt(
|
|
289
|
+
name=query["name"],
|
|
290
|
+
query=query["query"],
|
|
291
|
+
test_id=test["id"],
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
tests = ",".join([t["id"] for t in created_tests])
|
|
295
|
+
threat = controller.create_threat(
|
|
296
|
+
name=name,
|
|
297
|
+
threat_id=id,
|
|
298
|
+
source_id=source_id,
|
|
299
|
+
source=source,
|
|
300
|
+
published=published,
|
|
301
|
+
tests=tests,
|
|
302
|
+
)
|
|
303
|
+
except FileNotFoundError as e:
|
|
304
|
+
raise Exception(e)
|
|
305
|
+
finally:
|
|
306
|
+
return dict(
|
|
307
|
+
threat=threat,
|
|
308
|
+
created_tests=created_tests,
|
|
309
|
+
test_uploads=test_uploads,
|
|
310
|
+
created_detections=created_detections,
|
|
311
|
+
created_threat_hunt_queries=created_queries,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@build.command("update-threat")
|
|
316
|
+
@click.argument("threat")
|
|
317
|
+
@click.option("-n", "--name", help="test name", default=None, type=str)
|
|
318
|
+
@click.option(
|
|
319
|
+
"-s", "--source", help="source of threat (ex. www.cisa.gov)", default=None, type=str
|
|
320
|
+
)
|
|
321
|
+
@click.option(
|
|
322
|
+
"-i",
|
|
323
|
+
"--source_id",
|
|
324
|
+
help="ID of the threat, per the source (ex. aa23-075a)",
|
|
325
|
+
default=None,
|
|
326
|
+
type=str,
|
|
327
|
+
)
|
|
328
|
+
@click.option(
|
|
329
|
+
"-p", "--published", help="date the threat was published", default=None, type=str
|
|
330
|
+
)
|
|
331
|
+
@click.option(
|
|
332
|
+
"-t", "--tests", help="comma-separated list of test IDs", default=None, type=str
|
|
333
|
+
)
|
|
334
|
+
@click.pass_obj
|
|
335
|
+
@pretty_print
|
|
336
|
+
def update_threat(controller, threat, name, source_id, source, published, tests):
|
|
337
|
+
"""Create or update a security threat"""
|
|
338
|
+
with Spinner(description="Updating threat"):
|
|
339
|
+
return controller.update_threat(
|
|
340
|
+
threat_id=threat,
|
|
341
|
+
source_id=source_id,
|
|
342
|
+
name=name,
|
|
343
|
+
source=source,
|
|
344
|
+
published=published,
|
|
345
|
+
tests=tests,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@build.command("delete-threat")
|
|
350
|
+
@click.argument("threat")
|
|
351
|
+
@click.option("-p", "--purge", is_flag=True, help="purge threat")
|
|
352
|
+
@click.confirmation_option(prompt="Are you sure?")
|
|
353
|
+
@click.pass_obj
|
|
354
|
+
@pretty_print
|
|
355
|
+
def delete_threat(controller, threat, purge):
|
|
356
|
+
"""Delete a threat"""
|
|
357
|
+
with Spinner(description="Removing threat"):
|
|
358
|
+
return controller.delete_threat(threat_id=threat, purge=purge)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@build.command("undelete-threat")
|
|
362
|
+
@click.argument("threat")
|
|
363
|
+
@click.pass_obj
|
|
364
|
+
@pretty_print
|
|
365
|
+
def undelete_threat(controller, threat):
|
|
366
|
+
"""Undelete a threat"""
|
|
367
|
+
with Spinner(description="Restoring threat"):
|
|
368
|
+
return controller.undelete_threat(threat_id=threat)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@build.command("create-detection")
|
|
372
|
+
@click.argument("sigma_rule_file", type=click.Path(exists=True, dir_okay=False))
|
|
373
|
+
@click.option(
|
|
374
|
+
"-t", "--test", help="ID of the test this detection is for", required=True, type=str
|
|
375
|
+
)
|
|
376
|
+
@click.option("--detection_id", help="detection ID", default=None, type=str)
|
|
377
|
+
@click.option("--rule_id", help="rule ID", default=None, type=str)
|
|
378
|
+
@click.pass_obj
|
|
379
|
+
@pretty_print
|
|
380
|
+
def create_detection(controller, sigma_rule_file, test, detection_id, rule_id):
|
|
381
|
+
"""Create a detection rule"""
|
|
382
|
+
with Spinner(description="Creating new detection"):
|
|
383
|
+
with open(sigma_rule_file, "r") as f:
|
|
384
|
+
rule = f.read()
|
|
385
|
+
return controller.create_detection(
|
|
386
|
+
rule=rule, test_id=test, detection_id=detection_id, rule_id=rule_id
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@build.command("update-detection")
|
|
391
|
+
@click.argument("detection")
|
|
392
|
+
@click.option(
|
|
393
|
+
"--sigma_rule_file",
|
|
394
|
+
help="Sigma rule, from a yaml file",
|
|
395
|
+
default=None,
|
|
396
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
397
|
+
)
|
|
398
|
+
@click.option(
|
|
399
|
+
"-t", "--test", help="ID of the test this detection is for", default=None, type=str
|
|
400
|
+
)
|
|
401
|
+
@click.pass_obj
|
|
402
|
+
@pretty_print
|
|
403
|
+
def update_detection(controller, detection, sigma_rule_file, test):
|
|
404
|
+
"""Update a detection"""
|
|
405
|
+
with Spinner(description="Updating detection"):
|
|
406
|
+
with open(sigma_rule_file, "r") as f:
|
|
407
|
+
rule = f.read()
|
|
408
|
+
return controller.update_detection(
|
|
409
|
+
rule=rule,
|
|
410
|
+
test_id=test,
|
|
411
|
+
detection_id=detection,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@build.command("delete-detection")
|
|
416
|
+
@click.argument("detection")
|
|
417
|
+
@click.confirmation_option(prompt="Are you sure?")
|
|
418
|
+
@click.pass_obj
|
|
419
|
+
@pretty_print
|
|
420
|
+
def delete_detection(controller, detection):
|
|
421
|
+
"""Delete a detection"""
|
|
422
|
+
with Spinner(description="Removing detection"):
|
|
423
|
+
return controller.delete_detection(detection_id=detection)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@build.command("create-threat-hunt")
|
|
427
|
+
@click.argument("name")
|
|
428
|
+
@click.option(
|
|
429
|
+
"-c",
|
|
430
|
+
"--control",
|
|
431
|
+
help="",
|
|
432
|
+
required=True,
|
|
433
|
+
type=click.Choice(
|
|
434
|
+
[Control.CROWDSTRIKE.name, Control.DEFENDER.name], case_sensitive=False
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
@click.option("-q", "--query", help="Threat hunt query", required=True, type=str)
|
|
438
|
+
@click.option(
|
|
439
|
+
"-t",
|
|
440
|
+
"--test",
|
|
441
|
+
help="ID of the test this threat hunt query is for",
|
|
442
|
+
required=True,
|
|
443
|
+
type=str,
|
|
444
|
+
)
|
|
445
|
+
@click.option("--id", default=None, type=str)
|
|
446
|
+
@click.pass_obj
|
|
447
|
+
@pretty_print
|
|
448
|
+
def create_threat_hunt(controller, name, control, query, test, id):
|
|
449
|
+
"""Create a threat hunt query"""
|
|
450
|
+
with Spinner(description="Creating new threat hunt"):
|
|
451
|
+
return controller.create_threat_hunt(
|
|
452
|
+
control=Control[control],
|
|
453
|
+
name=name,
|
|
454
|
+
query=query,
|
|
455
|
+
test_id=test,
|
|
456
|
+
threat_hunt_id=id,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@build.command("update-threat-hunt")
|
|
461
|
+
@click.argument("threat_hunt")
|
|
462
|
+
@click.option("-n", "--name", help="Name of this threat hunt query", type=str)
|
|
463
|
+
@click.option("-q", "--query", help="Threat hunt query", type=str)
|
|
464
|
+
@click.option(
|
|
465
|
+
"-t", "--test", help="ID of the test this threat hunt query is for", type=str
|
|
466
|
+
)
|
|
467
|
+
@click.pass_obj
|
|
468
|
+
@pretty_print
|
|
469
|
+
def update_threat_hunt(controller, threat_hunt, name, query, test):
|
|
470
|
+
"""Update a threat hunt"""
|
|
471
|
+
with Spinner(description="Updating threat hunt"):
|
|
472
|
+
return controller.update_threat_hunt(
|
|
473
|
+
name=name,
|
|
474
|
+
query=query,
|
|
475
|
+
test_id=test,
|
|
476
|
+
threat_hunt_id=threat_hunt,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@build.command("delete-threat-hunt")
|
|
481
|
+
@click.argument("threat_hunt")
|
|
482
|
+
@click.confirmation_option(prompt="Are you sure?")
|
|
483
|
+
@click.pass_obj
|
|
484
|
+
@pretty_print
|
|
485
|
+
def delete_threat_hunt(controller, threat_hunt):
|
|
486
|
+
"""Delete a threat hunt"""
|
|
487
|
+
with Spinner(description="Removing threat hunt"):
|
|
488
|
+
return controller.delete_threat_hunt(threat_hunt_id=threat_hunt)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@click.command()
|
|
5
|
+
@click.pass_obj
|
|
6
|
+
def configure(account):
|
|
7
|
+
"""Configure your local keychain"""
|
|
8
|
+
profile = click.prompt(
|
|
9
|
+
"Enter the profile name",
|
|
10
|
+
default=account.profile or "default",
|
|
11
|
+
show_default=True,
|
|
12
|
+
)
|
|
13
|
+
hq = click.prompt("Enter the Prelude API", default=account.hq, show_default=True)
|
|
14
|
+
account_id = click.prompt("Enter your account ID")
|
|
15
|
+
handle = click.prompt("Enter your user handle (email)")
|
|
16
|
+
oidc = click.prompt(
|
|
17
|
+
"If authenticating via OIDC, enter the provider name",
|
|
18
|
+
default="none",
|
|
19
|
+
type=click.Choice(["google", "custom", "none"], case_sensitive=False),
|
|
20
|
+
).lower()
|
|
21
|
+
slug = None
|
|
22
|
+
if oidc == "none":
|
|
23
|
+
oidc = None
|
|
24
|
+
elif oidc == "custom":
|
|
25
|
+
slug = click.prompt("Please enter your account slug")
|
|
26
|
+
account.keychain.configure_keychain(
|
|
27
|
+
account=account_id, handle=handle, hq=hq, oidc=oidc, profile=profile, slug=slug
|
|
28
|
+
)
|
|
29
|
+
click.secho("Credentials saved", fg="green")
|