-
Notifications
You must be signed in to change notification settings - Fork 171
Expand file tree
/
Copy pathupdate-attack.py
More file actions
314 lines (276 loc) · 10.3 KB
/
Copy pathupdate-attack.py
File metadata and controls
314 lines (276 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import time
from enum import Enum
from types import SimpleNamespace
from typing import Annotated
import typer
from dotenv import load_dotenv
from loguru import logger
import modules
from modules import site_config
load_dotenv()
# argument defaults and options for the CLI
module_choices = [
"clean",
# NOTE: While datasources are deprecated starting in ATT&CK v18, we still include them for legacy support.
# All Datasource objects should be individually deprecated, but still have pages generated.
"datasources",
"groups",
"search",
"matrices",
"mitigations",
"software",
"tactics",
"techniques",
"campaigns",
"assets",
"datacomponents",
"detectionstrategies",
"analytics",
"tour",
"website_build",
"random_page",
"redirections",
"subdirectory",
"tests",
]
extras = ["resources", "versions", "blog", "stixtests", "benefactors", "contribute"]
test_choices = ["size", "links", "external_links", "citations"]
ModuleChoice = Enum("ModuleChoice", {choice.upper(): choice for choice in module_choices}, type=str)
ExtraChoice = Enum("ExtraChoice", {choice.upper(): choice for choice in extras}, type=str)
TestChoice = Enum("TestChoice", {choice.upper(): choice for choice in test_choices}, type=str)
DESCRIPTION = (
"Build the ATT&CK website.\n"
"All flags are optional. If you run the build without flags, "
"the modules that pertain to the ATT&CK dataset will be ran. "
"If you would like to run extra modules, opt-in these modules with the "
"--extras or --all-extras flag."
)
app = typer.Typer(
add_completion=False,
context_settings={"help_option_names": ["-h", "--help"]},
pretty_exceptions_enable=False,
)
def validate_subdirectory_string(subdirectory_str: str | None) -> str | None:
"""Validate subdirectory string."""
if subdirectory_str is None:
return None
if not subdirectory_str.isascii():
raise typer.BadParameter(f"{subdirectory_str} contains non ascii characters")
# Remove leading and trailing /
if subdirectory_str.startswith("/"):
subdirectory_str = subdirectory_str[1:]
if subdirectory_str.endswith("/"):
subdirectory_str = subdirectory_str[:-1]
site_config.set_subdirectory(subdirectory_str)
return subdirectory_str
@app.command(help=DESCRIPTION)
def cli(
ctx: typer.Context,
no_stix_link_replacement: Annotated[
bool,
typer.Option(
"--no-stix-link-replacement",
help=(
"If this flag is absent, links to attack.mitre.org/[page] in the STIX data will be replaced "
"with /[page]. Add this flag to preserve links to attack.mitre.org."
),
),
] = False,
modules_to_run: Annotated[
list[ModuleChoice] | None,
typer.Option(
"--modules",
"-m",
help=(
"Run a specific module. Repeat the option to select multiple modules, for example: "
"'-m clean -m techniques -m tactics'. Runs all modules when the option is not used."
),
),
] = None,
extra_modules: Annotated[
list[ExtraChoice] | None,
typer.Option(
"--extras",
"-e",
help=(
"Run an extra module that does not pertain to the ATT&CK dataset. Repeat the option to select "
"multiple extras, for example: '-e resources -e blog'. Use --all-extras to run every extra module."
),
),
] = None,
all_extras: Annotated[
bool,
typer.Option(
"--all-extras",
envvar="ATTACK_WEBSITE_UPDATE_ATTACK_ALL_EXTRAS",
show_envvar=True,
help="Run every extra module. Cannot be combined with --extras.",
),
] = False,
selected_tests: Annotated[
list[TestChoice] | None,
typer.Option(
"--test",
"-t",
help=(
"Run a specific test. Repeat the option to select multiple tests. Tests: size (output size against "
"the GitHub Pages limit); links (dead internal and relative links); external_links (dead external "
"links); citations (unparsed citation text)."
),
),
] = None,
attack_brand: Annotated[
bool,
typer.Option(
"--attack-brand/--no-attack-brand",
envvar="ATTACK_WEBSITE_ATTACK_BRAND",
show_envvar=True,
help="Apply ATT&CK brand colors; false uses custom-instance styling.",
),
] = False,
proxy: Annotated[str | None, typer.Option("--proxy", help="Set proxy.")] = None,
subdirectory: Annotated[
str | None,
typer.Option(
"--subdirectory",
callback=validate_subdirectory_string,
help="Host the site from the specified subdirectory.",
),
] = None,
print_tests: Annotated[
bool,
typer.Option("--print-tests", help="Print test output to stdout even if the results are very long."),
] = False,
test_exitstatus: Annotated[
bool,
typer.Option(
"--test-exitstatus/--no-test-exitstatus",
envvar="ATTACK_WEBSITE_TEST_EXITSTATUS",
show_envvar=True,
help="Preserve failing site-test exit codes; disable to force a successful process status.",
),
] = True,
version_archive_dir: Annotated[
str | None,
typer.Option(
"--version-archive-dir",
help="Set the ATT&CK version archive directory. Defaults to attack-version-archives.",
),
] = None,
banner: Annotated[
str | None,
typer.Option(
"--banner",
help=(
"Set the site banner text. Otherwise use modules/site_config.py BANNER_MESSAGE or the "
"ATTACK_WEBSITE_BANNER_MESSAGE environment variable."
),
),
] = None,
banner_enabled: Annotated[
bool,
typer.Option(
"--banner-enable/--banner-disable",
envvar="ATTACK_WEBSITE_BANNER_ENABLED",
show_envvar=True,
help="Enable or disable the site banner.",
),
] = True,
google_analytics: Annotated[
str | None,
typer.Option("--google-analytics", help="Include the provided Google Analytics ID on all pages."),
] = None,
google_site_verification: Annotated[
str | None,
typer.Option(
"--google-site-verification",
help="Include the provided Google site verification code on all pages.",
),
] = None,
include_osano: Annotated[
bool,
typer.Option(
"--include-osano/--no-include-osano",
envvar="ATTACK_WEBSITE_INCLUDE_OSANO",
show_envvar=True,
help="Include or exclude the Osano privacy compliance script.",
),
] = False,
) -> None:
"""Build the ATT&CK website."""
if all_extras and extra_modules is not None:
raise typer.BadParameter("--all-extras cannot be combined with --extras")
args = SimpleNamespace(
no_stix_link_replacement=no_stix_link_replacement,
modules=[value.value for value in modules_to_run] if modules_to_run else module_choices,
extras=extras
if all_extras
else ([value.value for value in extra_modules] if extra_modules is not None else None),
tests=[value.value for value in selected_tests] if selected_tests is not None else None,
attack_brand=attack_brand,
proxy=proxy,
subdirectory=subdirectory,
print_tests=print_tests,
override_exit_status=not test_exitstatus,
version_archive_dir=version_archive_dir,
banner=banner,
banner_enabled=banner_enabled,
banner_enabled_explicit=ctx.get_parameter_source("banner_enabled").name != "DEFAULT",
google_analytics=google_analytics,
google_site_verification=google_site_verification,
include_osano=include_osano,
)
site_config.args = args
run_build(args)
def remove_from_build(arg_modules, arg_extras):
"""Given a list of modules from command line, remove modules that appear in module directory that are not in list."""
def remove_from_running_pool():
"""Remove modules from running pool if they are not in modules list from argument."""
copy_of_modules = []
for module in modules.run_ptr:
if module["module_name"].lower() in arg_modules:
copy_of_modules.append(module)
modules.run_ptr = copy_of_modules
def remove_from_menu():
"""Remove modules from menu if they are not in modules list from argument."""
copy_of_menu = []
for module in modules.menu_ptr:
if module["module_name"].lower() in arg_modules:
copy_of_menu.append(module)
modules.menu_ptr = copy_of_menu
# Only add extra modules if argument flag was used
if arg_extras:
arg_modules = arg_modules + arg_extras
remove_from_running_pool()
remove_from_menu()
def run_build(args):
"""Run the website build with parsed command-line arguments."""
# Remove modules from build
remove_from_build(args.modules, args.extras)
# Print only the modules that will be run, marking extras
logger.info("Building website using the following modules in this order:")
for m in modules.run_ptr:
mod_name = m["module_name"]
if mod_name.lower() in extras:
logger.info(f"{mod_name} [extra]")
else:
logger.info(f"{mod_name}")
# Arguments used for pelican
site_config.send_to_pelican("no_stix_link_replacement", args.no_stix_link_replacement)
# Start time of update
update_start = time.time()
# Get running modules and priorities
for ptr in modules.run_ptr:
logger.info(f"RUNNING MODULE: {ptr['module_name']}")
start_time = time.time()
ptr["run_module"]()
end_time = time.time()
logger.info(f"FINISHED MODULE: {ptr['module_name']} in {end_time - start_time:.2f} seconds")
# Print end of module
update_end = time.time()
logger.info(f"TOTAL Update Time: {update_end - update_start:.2f} seconds")
def main():
"""Entry point for the update script."""
app()
if __name__ == "__main__":
main()