prelude-cli-beta 1391__tar.gz
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-cli-beta might be problematic. Click here for more details.
- prelude_cli_beta-1391/LICENSE +9 -0
- prelude_cli_beta-1391/MANIFEST.in +1 -0
- prelude_cli_beta-1391/PKG-INFO +38 -0
- prelude_cli_beta-1391/README.md +18 -0
- prelude_cli_beta-1391/prelude_cli_beta/__init__.py +0 -0
- prelude_cli_beta-1391/prelude_cli_beta/cli.py +47 -0
- prelude_cli_beta-1391/prelude_cli_beta/templates/__init__.py +0 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/__init__.py +0 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/auth.py +56 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/build.py +488 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/configure.py +29 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/detect.py +438 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/generate.py +125 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/iam.py +368 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/jobs.py +50 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/partner.py +192 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/scm.py +471 -0
- prelude_cli_beta-1391/prelude_cli_beta/views/shared.py +37 -0
- prelude_cli_beta-1391/prelude_cli_beta.egg-info/PKG-INFO +38 -0
- prelude_cli_beta-1391/prelude_cli_beta.egg-info/SOURCES.txt +25 -0
- prelude_cli_beta-1391/prelude_cli_beta.egg-info/dependency_links.txt +1 -0
- prelude_cli_beta-1391/prelude_cli_beta.egg-info/entry_points.txt +3 -0
- prelude_cli_beta-1391/prelude_cli_beta.egg-info/requires.txt +5 -0
- prelude_cli_beta-1391/prelude_cli_beta.egg-info/top_level.txt +1 -0
- prelude_cli_beta-1391/pyproject.toml +6 -0
- prelude_cli_beta-1391/setup.cfg +34 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright 2022, Prelude Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include prelude_cli/templates/*
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prelude-cli-beta
|
|
3
|
+
Version: 1391
|
|
4
|
+
Summary: For interacting with the Prelude SDK
|
|
5
|
+
Home-page: https://github.com/preludeorg
|
|
6
|
+
Author: Prelude Research
|
|
7
|
+
Author-email: support@preludesecurity.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: prelude-sdk-beta==1391
|
|
15
|
+
Requires-Dist: click>8
|
|
16
|
+
Requires-Dist: rich
|
|
17
|
+
Requires-Dist: python-dateutil
|
|
18
|
+
Requires-Dist: pyyaml
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Prelude CLI
|
|
22
|
+
|
|
23
|
+
Interact with the full range of features in Prelude Detect, organized by:
|
|
24
|
+
|
|
25
|
+
- IAM: manage your account
|
|
26
|
+
- Build: write and maintain your collection of security tests
|
|
27
|
+
- Detect: schedule security tests for your endpoints
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
```bash
|
|
31
|
+
pip install prelude-cli
|
|
32
|
+
prelude --help
|
|
33
|
+
prelude --interactive
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
https://docs.preludesecurity.com/docs/prelude-cli
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Prelude CLI
|
|
2
|
+
|
|
3
|
+
Interact with the full range of features in Prelude Detect, organized by:
|
|
4
|
+
|
|
5
|
+
- IAM: manage your account
|
|
6
|
+
- Build: write and maintain your collection of security tests
|
|
7
|
+
- Detect: schedule security tests for your endpoints
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
```bash
|
|
11
|
+
pip install prelude-cli
|
|
12
|
+
prelude --help
|
|
13
|
+
prelude --interactive
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
https://docs.preludesecurity.com/docs/prelude-cli
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
def cli(ctx, profile):
|
|
30
|
+
ctx.obj = Account.from_keychain(profile=profile)
|
|
31
|
+
if ctx.invoked_subcommand is None:
|
|
32
|
+
click.echo(ctx.get_help())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
cli.add_command(auth)
|
|
36
|
+
cli.add_command(build)
|
|
37
|
+
cli.add_command(configure)
|
|
38
|
+
cli.add_command(detect)
|
|
39
|
+
cli.add_command(generate)
|
|
40
|
+
cli.add_command(iam)
|
|
41
|
+
cli.add_command(jobs)
|
|
42
|
+
cli.add_command(partner)
|
|
43
|
+
cli.add_command(scm)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
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)
|