Skip to content

Commit 1d84138

Browse files
Add --force-overwrite flag to force overwriting the file we specify in --output-filename (#124)
* Add --force-overwrite flag to force overwriting the file we specify in --output-filename * Add use_existing_filename() to improve readability and handle duplicate extension issues * Make use_existing_filename() code more consistent with get_non_existing_filename() * local stix path for refresh script * fixes for PR #123 --------- Co-authored-by: Ruben Bouman <ruben@siriussecurity.nl> Co-authored-by: Ruben Bouman <11735227+rubinatorz@users.noreply.github.com>
1 parent d1a065e commit 1d84138

File tree

5 files changed

+104
-42
lines changed

5 files changed

+104
-42
lines changed

data_source_mapping.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,12 @@ def _get_technique_yaml_obj(techniques, tech_id):
196196
return tech
197197

198198

199-
def generate_data_sources_layer(filename, output_filename, layer_name, layer_settings):
199+
def generate_data_sources_layer(filename, output_filename, output_overwrite, layer_name, layer_settings):
200200
"""
201201
Generates a generic layer for data sources.
202202
:param filename: the filename of the YAML file containing the data sources administration
203203
:param output_filename: the output filename defined by the user
204+
:param output_overwrite: boolean flag indicating whether we're in overwrite mode
204205
:param layer_name: the name of the Navigator layer
205206
:param layer_settings: settings for the Navigator layer
206207
:return:
@@ -220,14 +221,15 @@ def generate_data_sources_layer(filename, output_filename, layer_name, layer_set
220221
json_string = simplejson.dumps(layer).replace('}, ', '},\n')
221222
if not output_filename:
222223
output_filename = create_output_filename('data_sources', name)
223-
write_file(output_filename, json_string)
224+
write_file(output_filename, output_overwrite, json_string)
224225

225226

226-
def plot_data_sources_graph(filename, output_filename):
227+
def plot_data_sources_graph(filename, output_filename, output_overwrite):
227228
"""
228229
Generates a line graph which shows the improvements on numbers of data sources through time.
229230
:param filename: the filename of the YAML file containing the data sources administration
230231
:param output_filename: the output filename defined by the user
232+
:param output_overwrite: boolean flag indicating whether we're in overwrite mode
231233
:return:
232234
"""
233235
my_data_sources, name, _, _, _ = load_data_sources(filename)
@@ -247,7 +249,11 @@ def plot_data_sources_graph(filename, output_filename):
247249
output_filename = 'graph_data_sources'
248250
elif output_filename.endswith('.html'):
249251
output_filename = output_filename.replace('.html', '')
250-
output_filename = get_non_existing_filename('output/' + output_filename, 'html')
252+
253+
if not output_overwrite:
254+
output_filename = get_non_existing_filename('output/' + output_filename, 'html')
255+
else:
256+
output_filename = use_existing_filename('output/' + output_filename, 'html')
251257

252258
import plotly.graph_objs as go
253259
import plotly.offline as offline
@@ -259,23 +265,30 @@ def plot_data_sources_graph(filename, output_filename):
259265
print("File written: " + output_filename)
260266

261267

262-
def export_data_source_list_to_excel(filename, output_filename, eql_search=False):
268+
def export_data_source_list_to_excel(filename, output_filename, output_overwrite, eql_search=False):
263269
"""
264270
Makes an overview of all MITRE ATT&CK data sources (via techniques) and lists which data sources are present
265271
in the YAML administration including all properties and data quality score.
266272
:param filename: the filename of the YAML file containing the data sources administration
267273
:param output_filename: the output filename defined by the user
274+
:param output_overwrite: boolean flag indicating whether we're in overwrite mode
268275
:param eql_search: specify if an EQL search was performed which may have resulted in missing ATT&CK data sources
269276
:return:
270277
"""
271278
# pylint: disable=unused-variable
272279
my_data_sources, name, systems, _, domain = load_data_sources(filename, filter_empty_scores=False)
273280
my_data_sources = dict(sorted(my_data_sources.items(), key=lambda kv: kv[0], reverse=False))
281+
274282
if not output_filename:
275283
output_filename = 'data_sources'
276284
elif output_filename.endswith('.xlsx'):
277285
output_filename = output_filename.replace('.xlsx', '')
278-
excel_filename = get_non_existing_filename('output/' + output_filename, 'xlsx')
286+
287+
if not output_overwrite:
288+
excel_filename = get_non_existing_filename('output/' + output_filename, 'xlsx')
289+
else:
290+
excel_filename = use_existing_filename('output/' + output_filename, 'xlsx')
291+
279292
workbook = xlsxwriter.Workbook(excel_filename)
280293
worksheet = workbook.add_worksheet('Data sources')
281294

@@ -868,11 +881,12 @@ def update_technique_administration_file(file_data_sources, file_tech_admin):
868881
# pylint: disable=redefined-outer-name
869882

870883

871-
def generate_technique_administration_file(filename, output_filename, write_file=True, all_techniques=False):
884+
def generate_technique_administration_file(filename, output_filename, output_overwrite, write_file=True, all_techniques=False):
872885
"""
873886
Generate a technique administration file based on the data source administration YAML file
874887
:param filename: the filename of the YAML file containing the data sources administration
875888
:param output_filename: the output filename defined by the user
889+
:param output_overwrite: boolean flag indicating whether we're in overwrite mode
876890
:param write_file: by default the file is written to disk
877891
:param all_techniques: include all ATT&CK techniques in the generated YAML file that are applicable to the
878892
platform(s) specified in the data source YAML file
@@ -985,7 +999,12 @@ def generate_technique_administration_file(filename, output_filename, write_file
985999
output_filename = 'techniques-administration-' + normalize_name_to_filename(name)
9861000
elif output_filename.endswith('.yaml'):
9871001
output_filename = output_filename.replace('.yaml', '')
988-
output_filename = get_non_existing_filename('output/' + output_filename, 'yaml')
1002+
1003+
if not output_overwrite:
1004+
output_filename = get_non_existing_filename(f'output/{output_filename}', 'yaml')
1005+
else:
1006+
output_filename = use_existing_filename(f'output/{output_filename}', 'yaml')
1007+
9891008
with open(output_filename, 'w') as f:
9901009
f.writelines(yaml_file_lines)
9911010
print("File written: " + output_filename)

dettect.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def _init_menu():
7070
'scores are calculated in the same way as with the option: '
7171
'-y, --yaml', action='store_true')
7272
parser_data_sources.add_argument('-of', '--output-filename', help='set the output filename')
73+
parser_data_sources.add_argument('--force-overwrite', help='force overwriting the output file if it already exists',
74+
action='store_true')
7375
parser_data_sources.add_argument('-ln', '--layer-name', help='set the name of the Navigator layer')
7476
parser_data_sources.add_argument('--health', help='check the YAML file(s) for errors', action='store_true')
7577
parser_data_sources.add_argument('--local-stix-path', help='path to a local STIX repository to use DeTT&CT offline '
@@ -109,6 +111,8 @@ def _init_menu():
109111
parser_visibility.add_argument('-g', '--graph', help='generate a graph with visibility added through time',
110112
action='store_true')
111113
parser_visibility.add_argument('-of', '--output-filename', help='set the output filename')
114+
parser_visibility.add_argument('--force-overwrite', help='force overwriting the output file if it already exists',
115+
action='store_true')
112116
parser_visibility.add_argument('-ln', '--layer-name', help='set the name of the Navigator layer')
113117
parser_visibility.add_argument('-cd', '--count-detections', help='Show the number of detections instead of listing '
114118
'all detection locations in Layer metadata (when using '
@@ -155,6 +159,8 @@ def _init_menu():
155159
parser_detection.add_argument('-g', '--graph', help='generate a graph with detections added through time',
156160
action='store_true')
157161
parser_detection.add_argument('-of', '--output-filename', help='set the output filename')
162+
parser_detection.add_argument('--force-overwrite', help='force overwriting the output file if it already exists',
163+
action='store_true')
158164
parser_detection.add_argument('-ln', '--layer-name', help='set the name of the Navigator layer')
159165
parser_detection.add_argument('-cd', '--count-detections', help='Show the number of detections instead of listing '
160166
'all detection locations in Layer metadata. Location '
@@ -224,6 +230,8 @@ def _init_menu():
224230
'most recent \'score\' objects',
225231
action='store_true', default=False)
226232
parser_group.add_argument('-of', '--output-filename', help='set the output filename')
233+
parser_group.add_argument('--force-overwrite', help='force overwriting the output file if it already exists',
234+
action='store_true')
227235
parser_group.add_argument('-ln', '--layer-name', help='set the name of the Navigator layer')
228236
parser_group.add_argument('-cd', '--count-detections', help='Show the number of detections instead of listing '
229237
'all detection locations in Layer metadata (when using an overlay with detection). Location '
@@ -301,13 +309,13 @@ def _menu(menu_parser):
301309
if args.update and check_file(args.file_tech, FILE_TYPE_TECHNIQUE_ADMINISTRATION, args.health):
302310
update_technique_administration_file(file_ds, args.file_tech)
303311
if args.layer:
304-
generate_data_sources_layer(file_ds, args.output_filename, args.layer_name, layer_settings)
312+
generate_data_sources_layer(file_ds, args.output_filename, args.force_overwrite, args.layer_name, layer_settings)
305313
if args.excel:
306-
export_data_source_list_to_excel(file_ds, args.output_filename, eql_search=args.search)
314+
export_data_source_list_to_excel(file_ds, args.output_filename, args.force_overwrite, eql_search=args.search)
307315
if args.graph:
308-
plot_data_sources_graph(file_ds, args.output_filename)
316+
plot_data_sources_graph(file_ds, args.output_filename, args.force_overwrite)
309317
if args.yaml:
310-
generate_technique_administration_file(file_ds, args.output_filename, all_techniques=args.yaml_all_techniques)
318+
generate_technique_administration_file(file_ds, args.output_filename, args.force_overwrite, all_techniques=args.yaml_all_techniques)
311319

312320
elif args.subparser in ['visibility', 'v']:
313321
if check_file(args.file_tech, FILE_TYPE_TECHNIQUE_ADMINISTRATION, args.health):
@@ -323,22 +331,22 @@ def _menu(menu_parser):
323331
if not file_tech:
324332
quit() # something went wrong in executing the search or 0 results where returned
325333
if args.layer:
326-
generate_visibility_layer(file_tech, False, args.output_filename, args.layer_name,
334+
generate_visibility_layer(file_tech, False, args.output_filename, args.force_overwrite, args.layer_name,
327335
layer_settings, args.platform, args.count_detections)
328336
if args.overlay:
329-
generate_visibility_layer(file_tech, True, args.output_filename, args.layer_name,
337+
generate_visibility_layer(file_tech, True, args.output_filename, args.force_overwrite, args.layer_name,
330338
layer_settings, args.platform, args.count_detections)
331339
if args.graph:
332-
plot_graph(file_tech, 'visibility', args.output_filename)
340+
plot_graph(file_tech, 'visibility', args.output_filename, args.force_overwrite)
333341
if args.excel:
334-
export_techniques_list_to_excel(file_tech, args.output_filename)
342+
export_techniques_list_to_excel(file_tech, args.output_filename, args.force_overwrite)
335343

336344
# TODO add Group EQL search capabilities
337345
elif args.subparser in ['group', 'g']:
338346
layer_settings = _parse_layer_settings(args.layer_settings)
339347
generate_group_heat_map(args.groups, args.campaigns, args.overlay, args.overlay_type, args.platform,
340348
args.software, args.include_software, args.search_visibility, args.search_detection, args.health,
341-
args.output_filename, args.layer_name, args.domain, layer_settings,
349+
args.output_filename, args.force_overwrite, args.layer_name, args.domain, layer_settings,
342350
args.all_scores, args.count_detections)
343351

344352
elif args.subparser in ['detection', 'd']:
@@ -355,14 +363,15 @@ def _menu(menu_parser):
355363
if not file_tech:
356364
quit() # something went wrong in executing the search or 0 results where returned
357365
if args.layer:
358-
generate_detection_layer(file_tech, False, args.output_filename, args.layer_name,
366+
generate_detection_layer(file_tech, False, args.output_filename, args.force_overwrite, args.layer_name,
359367
layer_settings, args.platform, args.count_detections)
360368
if args.overlay:
361-
generate_detection_layer(file_tech, True, args.output_filename, args.layer_name, layer_settings, args.platform, args.count_detections)
369+
generate_detection_layer(file_tech, True, args.output_filename, args.force_overwrite, args.layer_name,
370+
layer_settings, args.platform, args.count_detections)
362371
if args.graph:
363-
plot_graph(file_tech, 'detection', args.output_filename)
372+
plot_graph(file_tech, 'detection', args.output_filename, args.force_overwrite)
364373
if args.excel:
365-
export_techniques_list_to_excel(file_tech, args.output_filename)
374+
export_techniques_list_to_excel(file_tech, args.output_filename, args.force_overwrite)
366375

367376
elif args.subparser in ['generic', 'ge']:
368377
if args.datasources:

file_output.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@ def _clean_filename(filename):
1111
return filename.replace('/', '').replace('\\', '').replace(':', '')[:200]
1212

1313

14-
def write_file(filename, content):
14+
def write_file(filename, overwrite_mode, content):
1515
"""
1616
Writes content to a file and ensures if the file already exists it won't be overwritten by appending a number
1717
as suffix.
1818
:param filename: filename
19+
:param overwrite_mode: defines whether we want to force overwriting existing file
1920
:param content: the content of the file that needs to be written to the file
2021
:return:
2122
"""
2223
output_filename = 'output/%s' % _clean_filename(filename)
23-
output_filename = get_non_existing_filename(output_filename, 'json')
24+
25+
if not overwrite_mode:
26+
output_filename = get_non_existing_filename(output_filename, 'json')
27+
else:
28+
output_filename = use_existing_filename(output_filename, 'json')
2429

2530
with open(output_filename, 'w') as f:
2631
f.write(content)
@@ -57,9 +62,9 @@ def create_output_filename(filename_prefix, filename):
5762
def get_non_existing_filename(filename, extension):
5863
"""
5964
Generates a filename that doesn't exist based on the given filename by appending a number as suffix.
60-
:param filename:
61-
:param extension:
62-
:return:
65+
:param filename: input filename
66+
:param extension: input extension
67+
:return: unique filename
6368
"""
6469
if filename.endswith('.' + extension):
6570
filename = filename.replace('.' + extension, '')
@@ -73,6 +78,20 @@ def get_non_existing_filename(filename, extension):
7378
return output_filename
7479

7580

81+
def use_existing_filename(filename, extension):
82+
"""
83+
Generates a filename that preserves the file extension if present.
84+
If no extension is present, adds the provided extension.
85+
:param filename: input filename
86+
:param extension: input extension
87+
:return: filename and extension, without duplicating extensions
88+
"""
89+
if filename.endswith('.' + extension):
90+
filename = filename.replace('.' + extension, '')
91+
output_filename = '%s.%s' % (filename, extension)
92+
return output_filename
93+
94+
7695
def normalize_name_to_filename(name):
7796
"""
7897
Normalize the input filename to a lowercase filename and replace spaces with dashes.

group_mapping.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,8 @@ def _get_group_list(groups, file_type):
604604

605605

606606
def generate_group_heat_map(groups, campaigns, overlay, overlay_type, platform, overlay_software, include_software,
607-
search_visibility, search_detection, health_is_called, output_filename, layer_name, domain,
608-
layer_settings, include_all_score_objs, count_detections):
607+
search_visibility, search_detection, health_is_called, output_filename, output_overwrite,
608+
layer_name, domain, layer_settings, include_all_score_objs, count_detections):
609609
"""
610610
Calls all functions that are necessary for the generation of the heat map and write a json layer to disk.
611611
:param groups: threat actor groups
@@ -620,6 +620,7 @@ def generate_group_heat_map(groups, campaigns, overlay, overlay_type, platform,
620620
:param search_detection: detection EQL search query
621621
:param health_is_called: boolean that specifies if detailed errors in the file will be printed
622622
:param output_filename: output filename defined by the user
623+
:param output_overwrite: boolean flag indicating whether we're in overwrite mode
623624
:param layer_name: the name of the Navigator layer
624625
:param domain: the specified domain
625626
:param layer_settings: settings for the Navigator layer
@@ -800,6 +801,6 @@ def generate_group_heat_map(groups, campaigns, overlay, overlay_type, platform,
800801
filename += '-overlay_' + '_'.join(overlay_list)
801802

802803
filename = create_output_filename('attack', filename)
803-
write_file(filename, json_string)
804+
write_file(filename, output_overwrite, json_string)
804805
else:
805-
write_file(output_filename, json_string)
806+
write_file(output_filename, output_overwrite, json_string)

0 commit comments

Comments
 (0)