Skip to content

Move Rule into a dataclass #1029

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
32bd816
WIP: Convert Rule to a dataclass
rw-access Mar 9, 2021
04392f3
WIP: Create TOMLRule class
rw-access Mar 10, 2021
53a93a7
Fix make release
rw-access Mar 10, 2021
22a5a64
Lint fixes
rw-access Mar 10, 2021
8c864ae
Remove dead code
rw-access Mar 10, 2021
11ad1a5
Fix lint and tests
rw-access Mar 10, 2021
1049864
Use Python 3.8 in GitHub actions
rw-access Mar 10, 2021
2f296b9
Update README to 3.8+
rw-access Mar 10, 2021
4351b18
Add Python 3.8 assertion
rw-access Mar 22, 2021
6c7d118
Fix is_dirty property
rw-access Mar 22, 2021
ad6d72d
Remove incorrect pop from contents
rw-access Mar 22, 2021
400cb06
Add mixin with from_dict() and to_dict() methods
rw-access Mar 22, 2021
c66b8ea
Bypass validation for deprecated rules
rw-access Mar 22, 2021
43218e4
Fix rule_prompt
rw-access Mar 22, 2021
54b3c78
Fix dict_hash usage
rw-access Mar 22, 2021
af56af5
Fix rule_event_search
rw-access Mar 22, 2021
1795775
Merge branch 'main' into rule-refactor
rw-access Mar 22, 2021
a00af31
Switch to definitions.Date
rw-access Mar 22, 2021
9ebb71c
Fix toml-lint command, ignoring 'unneeded defaults'
rw-access Mar 22, 2021
804f512
Moved severity Literal to definitions.Severity
rw-access Mar 22, 2021
5fd0bfe
Remove BaseMarshmallowDataclass
rw-access Mar 22, 2021
b58170e
Fix lint and tests
rw-access Mar 22, 2021
233b60f
Add maturity to metadata for rule prompt loop
rw-access Mar 23, 2021
3881bc1
Fix typo in devtools
rw-access Mar 23, 2021
3b74752
Fix typo
rw-access Mar 23, 2021
52376b9
Use rule loader to load single rule in toml-lint
rw-access Mar 23, 2021
40d9e58
Add Schema hint to __schema method
rw-access Mar 23, 2021
a426b3f
Add MITREAttackURL definition
rw-access Mar 23, 2021
4c8ebda
Fix is_dirty to compare sha<-->sha
rw-access Mar 23, 2021
a627b46
Normalize the autoformatted rule output for API and toml-lint
rw-access Mar 23, 2021
1caa0e9
Make the package hash match
rw-access Mar 24, 2021
5497331
Make the rule object mutable but not rule contents
rw-access Mar 24, 2021
d84dbb2
Restore the rules
rw-access Mar 24, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Set up Python 3.7
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ all: release

$(VENV):
pip install virtualenv
virtualenv $(VENV) --python=python3.7
virtualenv $(VENV) --python=python3.8
$(PIP) install -r requirements.txt
$(PIP) install setuptools -U

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Supported Python versions](https://img.shields.io/badge/python-3.7+-yellow.svg)](https://www.python.org/downloads/)
[![Supported Python versions](https://img.shields.io/badge/python-3.8+-yellow.svg)](https://www.python.org/downloads/)
[![Unit Tests](https://github.com/elastic/detection-rules/workflows/Unit%20Tests/badge.svg)](https://github.com/elastic/detection-rules/actions)
[![Chat](https://img.shields.io/badge/chat-%23security--detection--rules-blueviolet)](https://ela.st/slack)

Expand Down Expand Up @@ -35,7 +35,7 @@ Detection Rules contains more than just static rule files. This repository also

## Getting started

Although rules can be added by manually creating `.toml` files, we don't recommend it. This repository also consists of a python module that aids rule creation and unit testing. Assuming you have Python 3.7+, run the below command to install the dependencies:
Although rules can be added by manually creating `.toml` files, we don't recommend it. This repository also consists of a python module that aids rule creation and unit testing. Assuming you have Python 3.8+, run the below command to install the dependencies:
```console
$ pip install -r requirements.txt
Collecting jsl==0.2.4
Expand Down
8 changes: 4 additions & 4 deletions detection_rules/cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
import kql
from . import ecs
from .attack import matrix, tactics, build_threat_map_entry
from .rule import Rule
from .rule import TOMLRule
from .schemas import CurrentSchema
from .utils import clear_caches, get_path

RULES_DIR = get_path("rules")


def rule_prompt(path=None, rule_type=None, required_only=True, save=True, verbose=False, **kwargs) -> Rule:
def rule_prompt(path=None, rule_type=None, required_only=True, save=True, verbose=False, **kwargs) -> TOMLRule:
"""Prompt loop to build a rule."""
from .misc import schema_prompt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably move schema_prompt into this file too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah. yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can follow up with that. the diff is already getting big


Expand Down Expand Up @@ -100,7 +100,7 @@ def rule_prompt(path=None, rule_type=None, required_only=True, save=True, verbos
rule = None

try:
rule = Rule(path, {'rule': contents})
rule = TOMLRule(path, {'rule': contents})
except kql.KqlParseError as e:
if e.error_msg == 'Unknown field':
warning = ('If using a non-ECS field, you must update "ecs{}.non-ecs-schema.json" under `beats` or '
Expand All @@ -113,7 +113,7 @@ def rule_prompt(path=None, rule_type=None, required_only=True, save=True, verbos
while True:
try:
contents['query'] = click.edit(contents['query'], extension='.eql')
rule = Rule(path, {'rule': contents})
rule = TOMLRule(path, {'rule': contents})
except kql.KqlParseError as e:
click.secho(e.args[0], fg='red', err=True)
click.pause()
Expand Down
6 changes: 3 additions & 3 deletions detection_rules/devtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .main import root
from .misc import PYTHON_LICENSE, add_client, GithubClient, Manifest, client_error, getdefault
from .packaging import PACKAGE_FILE, Package, manage_versions, RELEASE_DIR
from .rule import Rule
from .rule import TOMLRule
from .rule_loader import get_rule
from .utils import get_path

Expand Down Expand Up @@ -96,7 +96,7 @@ def kibana_diff(rule_id, repo, branch, threads):
repo_hashes = {r.id: r.get_hash() for r in rules.values()}

kibana_rules = {r['rule_id']: r for r in get_kibana_rules(repo=repo, branch=branch, threads=threads).values()}
kibana_hashes = {r['rule_id']: Rule.dict_hash(r) for r in kibana_rules.values()}
kibana_hashes = {r['rule_id']: TOMLRule.dict_hash(r) for r in kibana_rules.values()}

missing_from_repo = list(set(kibana_hashes).difference(set(repo_hashes)))
missing_from_kibana = list(set(repo_hashes).difference(set(kibana_hashes)))
Expand Down Expand Up @@ -354,7 +354,7 @@ def rule_event_search(ctx, rule_file, rule_id, date_range, count, max_results, v
if rule_id:
rule = get_rule(rule_id, verbose=False)
elif rule_file:
rule = Rule(rule_file, load_dump(rule_file))
rule = TOMLRule(rule_file, load_dump(rule_file))
else:
client_error('Must specify a rule file or rule ID')

Expand Down
22 changes: 12 additions & 10 deletions detection_rules/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

import xlsxwriter

from . import utils
from .attack import technique_lookup, matrix, attack_tm, tactics
from .packaging import Package
from .rule import ThreatMapping


class PackageDocument(xlsxwriter.Workbook):
Expand Down Expand Up @@ -47,16 +49,16 @@ def _get_attack_coverage(self):
coverage = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))

for rule in self.package.rules:
threat = rule.contents.get('threat')
threat = rule.contents.data.threat
sub_dir = Path(rule.path).parent.name

if threat:
for entry in threat:
tactic = entry['tactic']
techniques = entry.get('technique', [])
tactic = entry.tactic
techniques = entry.technique or []
for technique in techniques:
if technique['id'] in matrix[tactic['name']]:
coverage[tactic['name']][technique['id']][sub_dir] += 1
if technique.id in matrix[tactic.name]:
coverage[tactic.name][technique.id][sub_dir] += 1

return coverage

Expand Down Expand Up @@ -85,10 +87,10 @@ def add_summary(self):

tactic_counts = defaultdict(int)
for rule in self.package.rules:
threat = rule.contents.get('threat')
threat = rule.contents.data.threat
if threat:
for entry in threat:
tactic_counts[entry['tactic']['name']] += 1
tactic_counts[entry.tactic.name] += 1

worksheet.write(row, 0, "Total Production Rules")
worksheet.write(row, 1, len(self.production_rules))
Expand Down Expand Up @@ -134,9 +136,9 @@ def add_rule_details(self, rules=None, name='Rule Details'):
)

for row, rule in enumerate(rules, 1):
flat_mitre = rule.get_flat_mitre()
rule_contents = {'tactics': flat_mitre['tactic_names'], 'techniques': flat_mitre['technique_ids']}
rule_contents.update(rule.contents.copy())
flat_mitre = ThreatMapping.flatten(rule.contents.data.threat)
rule_contents = {'tactics': flat_mitre.tactic_names, 'techniques': flat_mitre.technique_ids}
rule_contents.update(utils.dataclass_to_dict(rule.contents.data))

for column, field in enumerate(metadata_fields):
value = rule_contents.get(field)
Expand Down
4 changes: 2 additions & 2 deletions detection_rules/eswrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .main import root
from .misc import add_params, client_error, elasticsearch_options
from .utils import format_command_options, normalize_timing_and_sort, unix_time_to_formatted, get_path
from .rule import Rule
from .rule import TOMLRule
from .rule_loader import get_rule, rta_mappings


Expand Down Expand Up @@ -195,7 +195,7 @@ def search(self, query, language, index: Union[str, list] = '*', start_time=None

return results

def search_from_rule(self, *rules: Rule, start_time=None, end_time='now', size=None):
def search_from_rule(self, *rules: TOMLRule, start_time=None, end_time='now', size=None):
"""Search an elasticsearch instance using a rule."""
from .misc import nested_get

Expand Down
8 changes: 4 additions & 4 deletions detection_rules/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from . import rule_loader
from .cli_utils import rule_prompt
from .misc import client_error, nested_set, parse_config
from .rule import Rule
from .rule import TOMLRule
from .rule_formatter import toml_write
from .schemas import CurrentSchema, available_versions
from .utils import get_path, clear_caches, load_rule_contents
Expand Down Expand Up @@ -119,7 +119,7 @@ def toml_lint(rule_file):
"""Cleanup files with some simple toml formatting."""
if rule_file:
contents = pytoml.load(rule_file)
rule = Rule(path=rule_file.name, contents=contents)
rule = TOMLRule(path=rule_file.name, contents=contents)

# removed unneeded defaults
for field in rule_loader.find_unneeded_defaults_from_rule(rule):
Expand Down Expand Up @@ -179,7 +179,7 @@ def view_rule(ctx, rule_id, rule_file, api_format, verbose=True):
contents = {k: v for k, v in load_rule_contents(rule_file, single_only=True)[0].items() if v}

try:
rule = Rule(rule_file, contents)
rule = TOMLRule(rule_file, contents)
except jsonschema.ValidationError as e:
client_error(f'Rule: {rule_id or os.path.basename(rule_file)} failed validation', e, ctx=ctx)
else:
Expand Down Expand Up @@ -318,7 +318,7 @@ def search_rules(query, columns, language, count, verbose=True, rules: Dict[str,
subtechnique_ids.extend([st['id'] for t in techniques for st in t.get('subtechnique', [])])

flat.update(techniques=technique_ids, tactics=tactic_names, subtechniques=subtechnique_ids,
unique_fields=Rule.get_unique_query_fields(rule_doc['rule']))
unique_fields=TOMLRule.get_unique_query_fields(rule_doc['rule']))
flattened_rules.append(flat)

flattened_rules.sort(key=lambda dct: dct["name"])
Expand Down
Loading