Adds commands to manage ATT&CK mappings (#2343)

* add att&ck commands; fix 2 rule mappings

* update message to stdout

* updated date for rule changes

* unrelated click bug fix

* add type hinting
This commit is contained in:
Justin Ibarra
2022-11-01 11:14:40 -08:00
committed by GitHub
parent 85e8c0abad
commit c1dd3c57ad
9 changed files with 156 additions and 58 deletions
+19 -18
View File
@@ -7,24 +7,27 @@
import re
import time
from pathlib import Path
from typing import Optional
import json
import requests
from collections import OrderedDict
from .semver import Version
from .utils import get_etc_path, get_etc_glob_path, read_gzip, gzip_compress
from .utils import cached, clear_caches, get_etc_path, get_etc_glob_path, read_gzip, gzip_compress
PLATFORMS = ['Windows', 'macOS', 'Linux']
CROSSWALK_FILE = get_etc_path('attack-crosswalk.json')
TECHNIQUES_REDIRECT_FILE = get_etc_path('attack-technique-redirects.json')
with open(TECHNIQUES_REDIRECT_FILE, 'r') as f:
techniques_redirect_map = json.load(f)['mapping']
CROSSWALK_FILE = Path(get_etc_path('attack-crosswalk.json'))
TECHNIQUES_REDIRECT_FILE = Path(get_etc_path('attack-technique-redirects.json'))
tactics_map = {}
@cached
def load_techniques_redirect() -> dict:
return json.loads(TECHNIQUES_REDIRECT_FILE.read_text())['mapping']
def get_attack_file_path() -> str:
pattern = 'attack-v*.json.gz'
attack_file = get_etc_glob_path(pattern)
@@ -91,7 +94,7 @@ technique_id_list = [t for t in technique_lookup if '.' not in t]
sub_technique_id_list = [t for t in technique_lookup if '.' in t]
def refresh_attack_data(save=True):
def refresh_attack_data(save=True) -> (Optional[dict], Optional[bytes]):
"""Refresh ATT&CK data from Mitre."""
attack_path = Path(get_attack_file_path())
filename, _, _ = attack_path.name.rsplit('.', 2)
@@ -111,7 +114,7 @@ def refresh_attack_data(save=True):
if Version(current_version) >= Version(latest_version):
print(f'No versions newer than the current detected: {current_version}')
return
return None, None
download = f'https://raw.githubusercontent.com/mitre/cti/{release_name}/enterprise-attack/enterprise-attack.json'
r = requests.get(download)
@@ -130,6 +133,7 @@ def refresh_attack_data(save=True):
def build_threat_map_entry(tactic: str, *technique_ids: str) -> dict:
"""Build rule threat map from technique IDs."""
techniques_redirect_map = load_techniques_redirect()
url_base = 'https://attack.mitre.org/{type}/{id}/'
tactic_id = tactics_map[tactic]
tech_entries = {}
@@ -218,22 +222,19 @@ def build_redirected_techniques_map(threads=50):
return technique_map
def refresh_redirected_techniques_map(threads=50):
def refresh_redirected_techniques_map(threads: int = 50):
"""Refresh the locally saved copy of the mapping."""
global techniques_redirect_map
replacement_map = build_redirected_techniques_map(threads)
mapping = {'saved_date': time.asctime(), 'mapping': replacement_map}
with open(TECHNIQUES_REDIRECT_FILE, 'w') as f:
json.dump(mapping, f, sort_keys=True, indent=2)
techniques_redirect_map = mapping
TECHNIQUES_REDIRECT_FILE.write_text(json.dumps(mapping, sort_keys=True, indent=2))
# reset the cached redirect contents
clear_caches()
print(f'refreshed mapping file: {TECHNIQUES_REDIRECT_FILE}')
def load_crosswalk_map():
@cached
def load_crosswalk_map() -> dict:
"""Retrieve the replacement mapping."""
with open(CROSSWALK_FILE, 'r') as f:
return json.load(f)['mapping']
return json.loads(CROSSWALK_FILE.read_text())['mapping']
+73 -2
View File
@@ -26,8 +26,8 @@ from elasticsearch import Elasticsearch
from eql.table import Table
from kibana.connector import Kibana
from . import rule_loader, utils
from .cli_utils import single_collection
from . import attack, rule_loader, utils
from .cli_utils import single_collection, multi_collection
from .docs import IntegrationSecurityDocs
from .endgame import EndgameSchemaManager
from .eswrap import CollectEvents, add_range_to_dsl
@@ -1157,3 +1157,74 @@ def generate_endgame_schema(token: str, endgame_version: str, overwrite: bool):
client = github.authenticated_client
schema_manager = EndgameSchemaManager(client, endgame_version)
schema_manager.save_schemas(overwrite=overwrite)
@dev_group.group('attack')
def attack_group():
"""Commands for managing Mitre ATT&CK data and mappings."""
@attack_group.command('refresh-data')
def refresh_attack_data() -> dict:
"""Refresh the ATT&CK data file."""
data, _ = attack.refresh_attack_data()
return data
@attack_group.command('refresh-redirect-mappings')
def refresh_threat_mappings():
"""Refresh the ATT&CK redirect file and update all rule threat mappings."""
# refresh the attack_technique_redirects
click.echo('refreshing data in attack_technique_redirects.json')
attack.refresh_redirected_techniques_map()
@attack_group.command('update-rules')
@multi_collection
def update_attack_in_rules(rules: RuleCollection) -> List[Optional[TOMLRule]]:
"""Update threat mappings attack data in all rules."""
new_rules = []
redirected_techniques = attack.load_techniques_redirect()
today = time.strftime('%Y/%m/%d')
for rule in rules.rules:
needs_update = False
valid_threat: List[ThreatMapping] = []
threat_pending_update = {}
threat = rule.contents.data.threat or []
for entry in threat:
tactic = entry.tactic.name
techniques = []
for technique in entry.technique or []:
techniques.append(technique.id)
techniques.extend([st.id for st in technique.subtechnique or []])
if any([t for t in techniques if t in redirected_techniques]):
needs_update = True
threat_pending_update[tactic] = techniques
else:
valid_threat.append(entry)
if needs_update:
for tactic, techniques in threat_pending_update.items():
try:
updated_threat = attack.build_threat_map_entry(tactic, *techniques)
except ValueError as err:
raise ValueError(f'{rule.id} - {rule.name}: {err}')
tm = ThreatMapping.from_dict(updated_threat)
valid_threat.append(tm)
new_meta = dataclasses.replace(rule.contents.metadata, updated_date=today)
new_data = dataclasses.replace(rule.contents.data, threat=valid_threat)
new_contents = dataclasses.replace(rule.contents, data=new_data, metadata=new_meta)
new_rule = TOMLRule(contents=new_contents)
new_rule.save_toml()
new_rules.append(new_rule)
if new_rules:
click.echo(f'{len(new_rules)} rules updated')
else:
click.echo('No rule changes needed')
return new_rules
+2 -1
View File
@@ -6,6 +6,7 @@
"""Elasticsearch cli commands."""
import json
import os
import sys
import time
from collections import defaultdict
from typing import List, Union
@@ -347,7 +348,7 @@ def es_group(ctx: click.Context, **kwargs):
ctx.ensure_object(dict)
# only initialize an es client if the subcommand is invoked without help (hacky)
if click.get_os_args()[-1] in ctx.help_option_names:
if sys.argv[-1] in ctx.help_option_names:
click.echo('Elasticsearch client:')
click.echo(format_command_options(ctx))
@@ -19,6 +19,7 @@
"T1044": "T1574.010",
"T1045": "T1027.002",
"T1050": "T1543.003",
"T1053.001": "T1053.002",
"T1054": "T1562.006",
"T1058": "T1574.011",
"T1060": "T1547.001",
@@ -128,7 +129,8 @@
"T1519": "T1546.014",
"T1522": "T1552.005",
"T1527": "T1550.001",
"T1536": "T1578.004"
"T1536": "T1578.004",
"T1547.011": "T1647"
},
"saved_date": "Tue Dec 8 23:08:53 2020"
"saved_date": "Tue Oct 4 21:58:48 2022"
}
+2 -1
View File
@@ -4,6 +4,7 @@
# 2.0.
"""Kibana cli commands."""
import sys
import uuid
import click
@@ -25,7 +26,7 @@ def kibana_group(ctx: click.Context, **kibana_kwargs):
ctx.ensure_object(dict)
# only initialize an kibana client if the subcommand is invoked without help (hacky)
if click.get_os_args()[-1] in ctx.help_option_names:
if sys.argv[-1] in ctx.help_option_names:
click.echo('Kibana client:')
click.echo(format_command_options(ctx))
+1 -1
View File
@@ -126,7 +126,7 @@ def save_etc_dump(contents, *path, **kwargs):
return eql.utils.save_dump(contents, path)
def gzip_compress(contents):
def gzip_compress(contents) -> bytes:
gz_file = io.BytesIO()
with gzip.GzipFile(mode="w", fileobj=gz_file) as f:
@@ -3,7 +3,7 @@ creation_date = "2020/01/05"
maturity = "production"
min_stack_comments = "New fields added: required_fields, related_integrations, setup"
min_stack_version = "8.3.0"
updated_date = "2022/08/24"
updated_date = "2022/10/07"
[rule]
author = ["Elastic"]
@@ -35,36 +35,49 @@ process where event.type in ("start", "process_started") and process.name : "osa
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1547"
name = "Boot or Logon Autostart Execution"
reference = "https://attack.mitre.org/techniques/T1547/"
[[rule.threat.technique.subtechnique]]
id = "T1547.011"
name = "Plist Modification"
reference = "https://attack.mitre.org/techniques/T1547/011/"
[rule.threat.tactic]
id = "TA0003"
name = "Persistence"
reference = "https://attack.mitre.org/tactics/TA0003/"
[[rule.threat.technique]]
id = "T1547"
name = "Boot or Logon Autostart Execution"
reference = "https://attack.mitre.org/techniques/T1547/"
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1059"
name = "Command and Scripting Interpreter"
reference = "https://attack.mitre.org/techniques/T1059/"
[[rule.threat.technique.subtechnique]]
id = "T1059.002"
name = "AppleScript"
reference = "https://attack.mitre.org/techniques/T1059/002/"
[rule.threat.tactic]
id = "TA0002"
name = "Execution"
reference = "https://attack.mitre.org/tactics/TA0002/"
[[rule.threat.technique]]
id = "T1059"
name = "Command and Scripting Interpreter"
reference = "https://attack.mitre.org/techniques/T1059/"
[[rule.threat.technique.subtechnique]]
id = "T1059.002"
name = "AppleScript"
reference = "https://attack.mitre.org/techniques/T1059/002/"
[[rule.threat]]
framework = "MITRE ATT&CK"
[rule.threat.tactic]
id = "TA0005"
name = "Defense Evasion"
reference = "https://attack.mitre.org/tactics/TA0005/"
[[rule.threat.technique]]
id = "T1647"
name = "Plist File Modification"
reference = "https://attack.mitre.org/techniques/T1647/"
@@ -3,7 +3,7 @@ creation_date = "2021/01/21"
maturity = "production"
min_stack_comments = "New fields added: required_fields, related_integrations, setup"
min_stack_version = "8.3.0"
updated_date = "2022/08/24"
updated_date = "2022/10/07"
[rule]
author = ["Elastic"]
@@ -37,19 +37,28 @@ event.category:"file" and not event.type:"deletion" and
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1547"
name = "Boot or Logon Autostart Execution"
reference = "https://attack.mitre.org/techniques/T1547/"
[[rule.threat.technique.subtechnique]]
id = "T1547.011"
name = "Plist Modification"
reference = "https://attack.mitre.org/techniques/T1547/011/"
[rule.threat.tactic]
id = "TA0003"
name = "Persistence"
reference = "https://attack.mitre.org/tactics/TA0003/"
[[rule.threat.technique]]
id = "T1547"
name = "Boot or Logon Autostart Execution"
reference = "https://attack.mitre.org/techniques/T1547/"
[[rule.threat]]
framework = "MITRE ATT&CK"
[rule.threat.tactic]
id = "TA0005"
name = "Defense Evasion"
reference = "https://attack.mitre.org/tactics/TA0005/"
[[rule.threat.technique]]
id = "T1647"
name = "Plist File Modification"
reference = "https://attack.mitre.org/techniques/T1647/"
+1 -1
View File
@@ -93,7 +93,7 @@ class TestThreatMappings(BaseRuleTest):
def test_technique_deprecations(self):
"""Check for use of any ATT&CK techniques that have been deprecated."""
replacement_map = attack.techniques_redirect_map
replacement_map = attack.load_techniques_redirect()
revoked = list(attack.revoked)
deprecated = list(attack.deprecated)