From b33756971e9942d0b0d5c04f9ab29d278fb378be Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Sun, 26 Apr 2020 19:47:15 -0400 Subject: [PATCH 1/8] Add new script that creates CSVs for technique mitigations or groups --- scripts/filter_techniques_by_category.py | 180 +++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 scripts/filter_techniques_by_category.py diff --git a/scripts/filter_techniques_by_category.py b/scripts/filter_techniques_by_category.py new file mode 100644 index 00000000..dc1b2bba --- /dev/null +++ b/scripts/filter_techniques_by_category.py @@ -0,0 +1,180 @@ +import argparse +import csv +import io + +import stix2 +import taxii2client + + +def build_taxii_source(collection_name): + """Downloads latest Enterprise ATT&CK content from GitHub.""" + # Establish TAXII2 Collection instance for Enterprise ATT&CK collection + collection_map = { + "enterprise_attack": "95ecc380-afe9-11e4-9b6c-751b66dd541e", + "mobile_attack": "2f669986-b40b-4423-b720-4396ca6a462b" + } + collection_url = "https://cti-taxii.mitre.org/stix/collections/" + collection_map[collection_name] + "/" + collection = taxii2client.Collection(collection_url) + taxii_ds = stix2.TAXIICollectionSource(collection) + + # Create an in-memory source (to prevent multiple web requests) + return stix2.MemorySource(stix_data=taxii_ds.query()) + + +def get_all_techniques(src): + """Filters data source by attack-pattern which extracts all ATT&CK Techniques""" + filters = [ + stix2.Filter('type', '=', 'attack-pattern'), + stix2.Filter('external_references.source_name', '=', 'mitre-attack'), + ] + results = src.query(filters) + return remove_deprecated(results) + + +def filter_for_term_relationships(src, relationship_type, object_id, source=True): + """Filters data source by relationship that matches type and source or target""" + filters = [ + stix2.Filter("type", "=", "relationship"), + stix2.Filter("relationship_type", "=", relationship_type), + ] + if source: + filters.append(stix2.Filter("source_ref", "=", object_id)) + else: + filters.append(stix2.Filter("target_ref", "=", object_id)) + + results = src.query(filters) + return remove_deprecated(results) + + +def filter_by_type_and_id(src, object_type, object_id): + """Filters data source by list of ids""" + filters = [ + stix2.Filter("type", "=", object_type), + stix2.Filter("id", "=", object_id), + ] + results = src.query(filters) + return remove_deprecated(results) + + +def grab_external_id(stix_object): + """Grab external id from STIX2 object""" + for external_reference in stix_object.get("external_references", []): + if external_reference.get("source_name") == "mitre-attack": + return external_reference["external_id"] + + +def remove_deprecated(stix_objects): + """Will remove any revoked or deprecated objects from queries made to the data source""" + # Note we use .get() because the property may not be present in the JSON data. The default is False + # if the property is not set. + return list( + filter( + lambda x: x.get("x_mitre_deprecated", False) is False and x.get("revoked", False) is False, + stix_objects + ) + ) + + +def escape_chars(a_string): + """Some characters create problems when written to file""" + return a_string.translate(str.maketrans({ + "\n": r"\\n", + })) + + +def arg_parse(): + """Function to handle script arguments.""" + parser = argparse.ArgumentParser(description="Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet matching Techniques with Mitigations or Groups.") + parser.add_argument("-c", "--collection", type=str, required=True, choices=["enterprise_attack", "mobile_attack"], help="Which collection to use (Enterprise, Mobile).") + parser.add_argument("-o", "--operation", type=str, required=True, choices=["groups", "mitigations"], help="Operation to perform on ATT&CK content.") + parser.add_argument("-s", "--save", type=str, required=False, help="Save the CSV file with a different filename.") + return parser + + +def do_groups(ds): + """Main logic to match techniques to groups""" + all_attack_patterns = get_all_techniques(ds) + writable_results = [] + + for attack_pattern in all_attack_patterns: + tid = grab_external_id(attack_pattern) + + # Grabs uses relationships for identified techniques + relationships = filter_for_term_relationships(ds, "uses", attack_pattern.id, source=False) + + for relationship in relationships: + # Groups are defined in STIX as intrusion-set objects + groups = filter_by_type_and_id(ds, "intrusion-set", relationship.source_ref) + + if groups: + group = groups[0] + gid = grab_external_id(group) + writable_results.append( + { + "TID": tid, + "Technique Name": attack_pattern.name, + "GID": gid, + "Group Name": group.name, + "Group Description": group.description, + "Usage": relationship.description + } + ) + return sorted(writable_results, key=lambda x: (x["TID"], x["GID"])) + + +def do_mitigations(ds): + """Main logic to match techniques to mitigations""" + all_attack_patterns = get_all_techniques(ds) + writable_results = [] + + for attack_pattern in all_attack_patterns: + tid = grab_external_id(attack_pattern) + + # Grabs mitigation relationships for identified techniques + relationships = filter_for_term_relationships(ds, "mitigates", attack_pattern.id, source=False) + + for relationship in relationships: + # Mitigations are defined in STIX as course-of-action objects + mitigation = filter_by_type_and_id(ds, "course-of-action", relationship.source_ref) + + if mitigation: + mitigation = mitigation[0] + mid = grab_external_id(mitigation) + writable_results.append( + { + "TID": tid, + "Technique Name": attack_pattern.name, + "MID": mid, + "Mitigation Name": mitigation.name, + "Mitigation Description": escape_chars(mitigation.description), + "Application": escape_chars(relationship.description), + } + ) + return sorted(writable_results, key=lambda x: (x["TID"], x["MID"])) + + +def main(args): + data_source = build_taxii_source(args.collection) + op = args.operation + + if op == "groups": + filename = args.save or "groups.csv" + fieldnames = ["TID", "Technique Name", "GID", "Group Name", "Group Description", "Usage"] + rowdicts = do_groups(data_source) + elif op == "mitigations": + filename = args.save or "mitigations.csv" + fieldnames = ["TID", "Technique Name", "MID", "Mitigation Name", "Mitigation Description", "Application"] + rowdicts = do_mitigations(data_source) + else: + raise RuntimeError("Unknown option: %s" % op) + + with io.open(filename, "w", newline="", encoding="utf-8") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rowdicts) + + +if __name__ == "__main__": + parser = arg_parse() + args = parser.parse_args() + main(args) From 89c3cb485065a4e69b5cdb50322da5b48c671c42 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Sun, 26 Apr 2020 19:47:31 -0400 Subject: [PATCH 2/8] add entry in README.md --- scripts/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/README.md b/scripts/README.md index 362bc308..8b3855ac 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,3 +7,4 @@ This folder contains one-off scripts for working with ATT&CK content. These scri | [techniques_from_data_source.py](techniques_from_data_source.py) | Fetches the current ATT&CK STIX 2.0 objects from the ATT&CK TAXII server, prints all of the data sources listed in Enterprise ATT&CK, and then lists all the Enterprise techniques containing a given data source. Run `python3 techniques_from_data_source.py -h` for usage instructions. | | [techniques_data_sources_vis.py](techniques_data_sources_vis.py) | Generate the csv data used to create the "Techniques Mapped to Data Sources" visualization in the ATT&CK roadmap. Run `python3 techniques_data_sources_vis.py -h` for usage instructions. | | [diff_stix.py](diff_stix.py) | Create markdown and/or ATT&CK Navigator layers reporting on the changes between two versions of the STIX2 bundles representing the ATT&CK content. For default operation, put [enterprise-attack.json](https://github.com/mitre/cti/blob/master/enterprise-attack/enterprise-attack.json), [mobile-attack.json](https://github.com/mitre/cti/blob/master/mobile-attack/mobile-attack.json), and [pre-attack.json](https://github.com/mitre/cti/blob/master/pre-attack/pre-attack.json) bundles in 'old' and 'new' folders for the script to compare. Run `python3 diff_stix.py -h` for full usage instructions. | +| [filter_techniques_by_category.py](filter_techniques_by_category.py) | Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet matching Techniques with Mitigations or Groups. Run `python3 filter_techniques_by_category.py -h` for usage instructions. | From f967bb37839aa66188f3f5183f4c5fdc4bcc12e9 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Sun, 26 Apr 2020 19:50:24 -0400 Subject: [PATCH 3/8] fix docstring for method `filter_by_type_and_id` --- scripts/filter_techniques_by_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/filter_techniques_by_category.py b/scripts/filter_techniques_by_category.py index dc1b2bba..8379a9c9 100644 --- a/scripts/filter_techniques_by_category.py +++ b/scripts/filter_techniques_by_category.py @@ -47,7 +47,7 @@ def filter_for_term_relationships(src, relationship_type, object_id, source=True def filter_by_type_and_id(src, object_type, object_id): - """Filters data source by list of ids""" + """Filters data source by id and type""" filters = [ stix2.Filter("type", "=", object_type), stix2.Filter("id", "=", object_id), From 82b14d865962500fc760b5850b5bc17feff3973c Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 29 Apr 2020 13:32:41 -0400 Subject: [PATCH 4/8] consolidate `do_groups`, `do_mitigations` into a singular method `do_mapping` add support to for `software` operation, fix issue not allowing mobile-attack operations --- scripts/filter_techniques_by_category.py | 120 ++++++++++------------- 1 file changed, 54 insertions(+), 66 deletions(-) diff --git a/scripts/filter_techniques_by_category.py b/scripts/filter_techniques_by_category.py index 8379a9c9..88016834 100644 --- a/scripts/filter_techniques_by_category.py +++ b/scripts/filter_techniques_by_category.py @@ -21,45 +21,46 @@ def build_taxii_source(collection_name): return stix2.MemorySource(stix_data=taxii_ds.query()) -def get_all_techniques(src): +def get_all_techniques(src, source_name): """Filters data source by attack-pattern which extracts all ATT&CK Techniques""" filters = [ - stix2.Filter('type', '=', 'attack-pattern'), - stix2.Filter('external_references.source_name', '=', 'mitre-attack'), + stix2.Filter("type", "=", "attack-pattern"), + stix2.Filter("external_references.source_name", "=", source_name), ] results = src.query(filters) return remove_deprecated(results) -def filter_for_term_relationships(src, relationship_type, object_id, source=True): +def filter_for_term_relationships(src, relationship_type, object_id, target=True): """Filters data source by relationship that matches type and source or target""" filters = [ stix2.Filter("type", "=", "relationship"), stix2.Filter("relationship_type", "=", relationship_type), ] - if source: - filters.append(stix2.Filter("source_ref", "=", object_id)) - else: + if target: filters.append(stix2.Filter("target_ref", "=", object_id)) + else: + filters.append(stix2.Filter("source_ref", "=", object_id)) results = src.query(filters) return remove_deprecated(results) -def filter_by_type_and_id(src, object_type, object_id): +def filter_by_type_and_id(src, object_type, object_id, source_name): """Filters data source by id and type""" filters = [ stix2.Filter("type", "=", object_type), stix2.Filter("id", "=", object_id), + stix2.Filter("external_references.source_name", "=", source_name), ] results = src.query(filters) return remove_deprecated(results) -def grab_external_id(stix_object): +def grab_external_id(stix_object, source_name): """Grab external id from STIX2 object""" for external_reference in stix_object.get("external_references", []): - if external_reference.get("source_name") == "mitre-attack": + if external_reference.get("source_name") == source_name: return external_reference["external_id"] @@ -86,85 +87,72 @@ def arg_parse(): """Function to handle script arguments.""" parser = argparse.ArgumentParser(description="Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet matching Techniques with Mitigations or Groups.") parser.add_argument("-c", "--collection", type=str, required=True, choices=["enterprise_attack", "mobile_attack"], help="Which collection to use (Enterprise, Mobile).") - parser.add_argument("-o", "--operation", type=str, required=True, choices=["groups", "mitigations"], help="Operation to perform on ATT&CK content.") + parser.add_argument("-o", "--operation", type=str, required=True, choices=["groups", "mitigations", "software"], help="Operation to perform on ATT&CK content.") parser.add_argument("-s", "--save", type=str, required=False, help="Save the CSV file with a different filename.") return parser -def do_groups(ds): - """Main logic to match techniques to groups""" - all_attack_patterns = get_all_techniques(ds) +def do_mapping(ds, fieldnames, relationship_type, type_filter, source_name, sorting_keys): + """Main logic to map techniques to mitigations, groups or software""" + all_attack_patterns = get_all_techniques(ds, source_name) writable_results = [] for attack_pattern in all_attack_patterns: - tid = grab_external_id(attack_pattern) - - # Grabs uses relationships for identified techniques - relationships = filter_for_term_relationships(ds, "uses", attack_pattern.id, source=False) + # Grabs relationships for identified techniques + relationships = filter_for_term_relationships(ds, relationship_type, attack_pattern.id) for relationship in relationships: # Groups are defined in STIX as intrusion-set objects - groups = filter_by_type_and_id(ds, "intrusion-set", relationship.source_ref) - - if groups: - group = groups[0] - gid = grab_external_id(group) - writable_results.append( - { - "TID": tid, - "Technique Name": attack_pattern.name, - "GID": gid, - "Group Name": group.name, - "Group Description": group.description, - "Usage": relationship.description - } + # Mitigations are defined in STIX as course-of-action objects + # Software are defined in STIX as malware objects + stix_results = filter_by_type_and_id(ds, type_filter, relationship.source_ref, source_name) + + if stix_results: + row_data = ( + grab_external_id(attack_pattern, source_name), + attack_pattern.name, + grab_external_id(stix_results[0], source_name), + stix_results[0].name, + escape_chars(stix_results[0].description), + escape_chars(relationship.description), ) - return sorted(writable_results, key=lambda x: (x["TID"], x["GID"])) - - -def do_mitigations(ds): - """Main logic to match techniques to mitigations""" - all_attack_patterns = get_all_techniques(ds) - writable_results = [] - - for attack_pattern in all_attack_patterns: - tid = grab_external_id(attack_pattern) - # Grabs mitigation relationships for identified techniques - relationships = filter_for_term_relationships(ds, "mitigates", attack_pattern.id, source=False) + writable_results.append(dict(zip(fieldnames, row_data))) - for relationship in relationships: - # Mitigations are defined in STIX as course-of-action objects - mitigation = filter_by_type_and_id(ds, "course-of-action", relationship.source_ref) - - if mitigation: - mitigation = mitigation[0] - mid = grab_external_id(mitigation) - writable_results.append( - { - "TID": tid, - "Technique Name": attack_pattern.name, - "MID": mid, - "Mitigation Name": mitigation.name, - "Mitigation Description": escape_chars(mitigation.description), - "Application": escape_chars(relationship.description), - } - ) - return sorted(writable_results, key=lambda x: (x["TID"], x["MID"])) + return sorted(writable_results, key=lambda x: (x[sorting_keys[0]], x[sorting_keys[1]])) def main(args): data_source = build_taxii_source(args.collection) op = args.operation + source_map = { + "enterprise_attack": "mitre-attack", + "mobile_attack": "mitre-mobile-attack", + } + source_name = source_map[args.collection] + if op == "groups": filename = args.save or "groups.csv" - fieldnames = ["TID", "Technique Name", "GID", "Group Name", "Group Description", "Usage"] - rowdicts = do_groups(data_source) + fieldnames = ("TID", "Technique Name", "GID", "Group Name", "Group Description", "Usage") + relationship_type = "uses" + type_filter = "intrusion-set" + sorting_keys = ("TID", "GID") + rowdicts = do_mapping(data_source, fieldnames, relationship_type, type_filter, source_name, sorting_keys) elif op == "mitigations": filename = args.save or "mitigations.csv" - fieldnames = ["TID", "Technique Name", "MID", "Mitigation Name", "Mitigation Description", "Application"] - rowdicts = do_mitigations(data_source) + fieldnames = ("TID", "Technique Name", "MID", "Mitigation Name", "Mitigation Description", "Application") + relationship_type = "mitigates" + type_filter = "course-of-action" + sorting_keys = ("TID", "MID") + rowdicts = do_mapping(data_source, fieldnames, relationship_type, type_filter, source_name, sorting_keys) + elif op == "software": + filename = args.save or "software.csv" + fieldnames = ("TID", "Technique Name", "SID", "Software Name", "Software Description", "Use") + relationship_type = "uses" + type_filter = "malware" + sorting_keys = ("TID", "SID") + rowdicts = do_mapping(data_source, fieldnames, relationship_type, type_filter, source_name, sorting_keys) else: raise RuntimeError("Unknown option: %s" % op) From 8260223bd439d07e6fb2e91adcc74a7c56489f1b Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 29 Apr 2020 13:44:54 -0400 Subject: [PATCH 5/8] docstring changes made from PR feedback, renamed file to `technique_mappings_to_csv.py` --- scripts/README.md | 2 +- ...ues_by_category.py => technique_mappings_to_csv.py} | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename scripts/{filter_techniques_by_category.py => technique_mappings_to_csv.py} (91%) diff --git a/scripts/README.md b/scripts/README.md index 8b3855ac..49f5e2ac 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,4 +7,4 @@ This folder contains one-off scripts for working with ATT&CK content. These scri | [techniques_from_data_source.py](techniques_from_data_source.py) | Fetches the current ATT&CK STIX 2.0 objects from the ATT&CK TAXII server, prints all of the data sources listed in Enterprise ATT&CK, and then lists all the Enterprise techniques containing a given data source. Run `python3 techniques_from_data_source.py -h` for usage instructions. | | [techniques_data_sources_vis.py](techniques_data_sources_vis.py) | Generate the csv data used to create the "Techniques Mapped to Data Sources" visualization in the ATT&CK roadmap. Run `python3 techniques_data_sources_vis.py -h` for usage instructions. | | [diff_stix.py](diff_stix.py) | Create markdown and/or ATT&CK Navigator layers reporting on the changes between two versions of the STIX2 bundles representing the ATT&CK content. For default operation, put [enterprise-attack.json](https://github.com/mitre/cti/blob/master/enterprise-attack/enterprise-attack.json), [mobile-attack.json](https://github.com/mitre/cti/blob/master/mobile-attack/mobile-attack.json), and [pre-attack.json](https://github.com/mitre/cti/blob/master/pre-attack/pre-attack.json) bundles in 'old' and 'new' folders for the script to compare. Run `python3 diff_stix.py -h` for full usage instructions. | -| [filter_techniques_by_category.py](filter_techniques_by_category.py) | Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet matching Techniques with Mitigations or Groups. Run `python3 filter_techniques_by_category.py -h` for usage instructions. | +| [technique_mappings_to_csv.py](technique_mappings_to_csv.py) | Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet mapping Techniques with Mitigations, Groups or Software. Run `python3 technique_mappings_to_csv.py -h` for usage instructions. | diff --git a/scripts/filter_techniques_by_category.py b/scripts/technique_mappings_to_csv.py similarity index 91% rename from scripts/filter_techniques_by_category.py rename to scripts/technique_mappings_to_csv.py index 88016834..f41c769e 100644 --- a/scripts/filter_techniques_by_category.py +++ b/scripts/technique_mappings_to_csv.py @@ -7,7 +7,7 @@ def build_taxii_source(collection_name): - """Downloads latest Enterprise ATT&CK content from GitHub.""" + """Downloads latest Enterprise or Mobile ATT&CK content from MITRE TAXII Server.""" # Establish TAXII2 Collection instance for Enterprise ATT&CK collection collection_map = { "enterprise_attack": "95ecc380-afe9-11e4-9b6c-751b66dd541e", @@ -32,7 +32,7 @@ def get_all_techniques(src, source_name): def filter_for_term_relationships(src, relationship_type, object_id, target=True): - """Filters data source by relationship that matches type and source or target""" + """Filters data source by type, relationship_type and source or target""" filters = [ stix2.Filter("type", "=", "relationship"), stix2.Filter("relationship_type", "=", relationship_type), @@ -85,9 +85,9 @@ def escape_chars(a_string): def arg_parse(): """Function to handle script arguments.""" - parser = argparse.ArgumentParser(description="Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet matching Techniques with Mitigations or Groups.") - parser.add_argument("-c", "--collection", type=str, required=True, choices=["enterprise_attack", "mobile_attack"], help="Which collection to use (Enterprise, Mobile).") - parser.add_argument("-o", "--operation", type=str, required=True, choices=["groups", "mitigations", "software"], help="Operation to perform on ATT&CK content.") + parser = argparse.ArgumentParser(description="Fetches the current ATT&CK content expressed as STIX2 and creates spreadsheet mapping Techniques with Mitigations, Groups or Software.") + parser.add_argument("-d", "--domain", type=str, required=True, choices=["enterprise_attack", "mobile_attack"], help="Which ATT&CK domain to use (Enterprise, Mobile).") + parser.add_argument("-m", "--mapping-type", type=str, required=True, choices=["groups", "mitigations", "software"], help="Which type of object to output mappings for using ATT&CK content.") parser.add_argument("-s", "--save", type=str, required=False, help="Save the CSV file with a different filename.") return parser From 425c6c969c9892e29a8697f060a2628f21a7968d Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 29 Apr 2020 13:49:19 -0400 Subject: [PATCH 6/8] update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b08a4f..f96f249f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Changes staged on develop +## Improvements +- New script [technique_mappings_to_csv.py](technique_mappings_to_csv.py) added to support mapping Techniques with Mitigations, Groups or Software. The output is a CSV file. Added in PR [#23](https://github.com/mitre-attack/attack-scripts/pull/23) + # V1.3 - 8 January 2019 ## New Scripts - Added [diff_stix.py](scripts/diff_stix.py). From 92a6be208af7a45f45df1d6a36f23833e66bb3d1 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 29 Apr 2020 16:19:50 -0400 Subject: [PATCH 7/8] update argument names... --- scripts/technique_mappings_to_csv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/technique_mappings_to_csv.py b/scripts/technique_mappings_to_csv.py index f41c769e..4ed29be1 100644 --- a/scripts/technique_mappings_to_csv.py +++ b/scripts/technique_mappings_to_csv.py @@ -123,14 +123,14 @@ def do_mapping(ds, fieldnames, relationship_type, type_filter, source_name, sort def main(args): - data_source = build_taxii_source(args.collection) - op = args.operation + data_source = build_taxii_source(args.domain) + op = args.mapping_type source_map = { "enterprise_attack": "mitre-attack", "mobile_attack": "mitre-mobile-attack", } - source_name = source_map[args.collection] + source_name = source_map[args.domain] if op == "groups": filename = args.save or "groups.csv" From c8c95343acd1558c8c249968043413b747bbc5e6 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Thu, 30 Apr 2020 16:09:45 -0400 Subject: [PATCH 8/8] Update technique_mappings_to_csv.py Add `tqdm` module to provide estimate of procedure completion --- scripts/technique_mappings_to_csv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/technique_mappings_to_csv.py b/scripts/technique_mappings_to_csv.py index 4ed29be1..fb90420b 100644 --- a/scripts/technique_mappings_to_csv.py +++ b/scripts/technique_mappings_to_csv.py @@ -4,6 +4,7 @@ import stix2 import taxii2client +import tqdm def build_taxii_source(collection_name): @@ -97,7 +98,7 @@ def do_mapping(ds, fieldnames, relationship_type, type_filter, source_name, sort all_attack_patterns = get_all_techniques(ds, source_name) writable_results = [] - for attack_pattern in all_attack_patterns: + for attack_pattern in tqdm.tqdm(all_attack_patterns, desc="parsing data for techniques"): # Grabs relationships for identified techniques relationships = filter_for_term_relationships(ds, relationship_type, attack_pattern.id)