Files
sigma-rules/tests/test_schemas.py
T
Terrance DeJesus fb2b4529c5 [FR] Adapt PyPi semver Library and Remove Custom (#2503)
* removed custom semver and replaced with pypi

* updated beats.py version references

* updated bump-versions CLI command to use semver and change logic

* updated schemas __init__, test_version_lock and unstage incompatible rules CLI

* updated test_stack_schema_map in TestVersions unittest

* updated test_all_rules unit testing Version() references

* updated stack_compat.py for get_restricted_field references)

* updated version_lock.py Version() references

* updated docs.py Version() reference for parse_registry

* updated devtools.py Version() reference for trim-version-lock

* updated mixins.py Version() reference in validate_field_compatibility

* adjusted schemas.__init__ Version() reference in get_stack_schemas

* adjusted ecs.py Version() references

* adjusted integrations.py Version() references

* adjusted rule.py Version() references

* sorted imports

* replaced custom semver with pypi semver in unit test files

* addressed unit test and flake errors

* changed semver strings casted to version_lock.py

* fixed sorting in integrations.py

* updated bump-pkgs-versions CLI command

* adjusted semantic version in unstage-incompatible-rules command

* adjusted semver import to VersionInfo

* added semver 3 and adjusted import names

* added option_minor_and_patch parameter where version is major.minor

* updated bump-pkg-versions to always save to packages.yml

* removed leftover split call & updated find latest compatible version command

* updated integrations.py, version_lock.py and schemas.__init__.py

* changed fstring reference in downgrade function

* reverted formatting changes for detection_rules __init__.py

* added newline to detection_rules __init__.py

* adjusted finding latest_release for attack package logic

* adjusted unstage-incompatible-rules command logic comparing versions

* removing changes from misc.py related to auto-formatting

* adding newline to misc.py

* fixed bug in downgrade function calling decorators

* added semantic version validation on migrate decorator function

* added expected type returned from find_latest_integration_version in integrations.py

* add comment about stripped versions for version lock file

Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com>

---------

Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com>
2023-02-07 14:26:29 -05:00

292 lines
12 KiB
Python

# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0; you may not use this file except in compliance with the Elastic License
# 2.0.
"""Test stack versioned schemas."""
import copy
import unittest
import uuid
from semver import Version
import eql
from detection_rules import utils
from detection_rules.misc import load_current_package_version
from detection_rules.rule import TOMLRuleContents
from detection_rules.schemas import downgrade
from detection_rules.version_lock import VersionLockFile
from marshmallow import ValidationError
class TestSchemas(unittest.TestCase):
"""Test schemas and downgrade functions."""
@classmethod
def setUpClass(cls):
cls.current_version = load_current_package_version()
# expected contents for a downgraded rule
cls.v78_kql = {
"description": "test description",
"index": ["filebeat-*"],
"language": "kuery",
"name": "test rule",
"query": "process.name:test.query",
"risk_score": 21,
"rule_id": str(uuid.uuid4()),
"severity": "low",
"type": "query",
"threat": [
{
"framework": "MITRE ATT&CK",
"tactic": {
"id": "TA0001",
"name": "Execution",
"reference": "https://attack.mitre.org/tactics/TA0001/"
},
"technique": [
{
"id": "T1059",
"name": "Command and Scripting Interpreter",
"reference": "https://attack.mitre.org/techniques/T1059/",
}
],
}
]
}
cls.v79_kql = dict(cls.v78_kql, author=["Elastic"], license="Elastic License v2")
cls.v711_kql = copy.deepcopy(cls.v79_kql)
# noinspection PyTypeChecker
cls.v711_kql["threat"][0]["technique"][0]["subtechnique"] = [{
"id": "T1059.001",
"name": "PowerShell",
"reference": "https://attack.mitre.org/techniques/T1059/001/"
}]
# noinspection PyTypeChecker
cls.v711_kql["threat"].append({
"framework": "MITRE ATT&CK",
"tactic": {
"id": "TA0008",
"name": "Lateral Movement",
"reference": "https://attack.mitre.org/tactics/TA0008/"
},
})
cls.v79_threshold_contents = {
"author": ["Elastic"],
"description": "test description",
"language": "kuery",
"license": "Elastic License v2",
"name": "test rule",
"query": "process.name:test.query",
"risk_score": 21,
"rule_id": str(uuid.uuid4()),
"severity": "low",
"threshold": {
"field": "destination.bytes",
"value": 75,
},
"type": "threshold",
}
cls.v712_threshold_rule = dict(copy.deepcopy(cls.v79_threshold_contents), threshold={
'field': ['destination.bytes', 'process.args'],
'value': 75,
'cardinality': [{
'field': 'user.name',
'value': 2
}]
})
def test_query_downgrade_7_x(self):
"""Downgrade a standard KQL rule."""
if Version.parse(self.current_version, optional_minor_and_patch=True).major > 7:
return
self.assertDictEqual(downgrade(self.v711_kql, "7.11"), self.v711_kql)
self.assertDictEqual(downgrade(self.v711_kql, "7.9"), self.v79_kql)
self.assertDictEqual(downgrade(self.v711_kql, "7.9.2"), self.v79_kql)
self.assertDictEqual(downgrade(self.v711_kql, "7.8.1"), self.v78_kql)
self.assertDictEqual(downgrade(self.v79_kql, "7.8"), self.v78_kql)
self.assertDictEqual(downgrade(self.v79_kql, "7.8"), self.v78_kql)
with self.assertRaises(ValueError):
downgrade(self.v711_kql, "7.7")
with self.assertRaises(ValueError):
downgrade(self.v79_kql, "7.7")
with self.assertRaises(ValueError):
downgrade(self.v78_kql, "7.7", current_version="7.8")
def test_versioned_downgrade_7_x(self):
"""Downgrade a KQL rule with version information"""
if Version.parse(self.current_version, optional_minor_and_patch=True).major > 7:
return
api_contents = self.v79_kql
self.assertDictEqual(downgrade(api_contents, "7.9"), api_contents)
self.assertDictEqual(downgrade(api_contents, "7.9.2"), api_contents)
api_contents78 = api_contents.copy()
api_contents78.pop("author")
api_contents78.pop("license")
self.assertDictEqual(downgrade(api_contents, "7.8"), api_contents78)
with self.assertRaises(ValueError):
downgrade(api_contents, "7.7")
def test_threshold_downgrade_7_x(self):
"""Downgrade a threshold rule that was first introduced in 7.9."""
if Version.parse(self.current_version, optional_minor_and_patch=True).major > 7:
return
api_contents = self.v712_threshold_rule
self.assertDictEqual(downgrade(api_contents, '7.13'), api_contents)
self.assertDictEqual(downgrade(api_contents, '7.13.1'), api_contents)
exc_msg = 'Cannot downgrade a threshold rule that has multiple threshold fields defined'
with self.assertRaisesRegex(ValueError, exc_msg):
downgrade(api_contents, '7.9')
v712_threshold_contents_single_field = copy.deepcopy(api_contents)
v712_threshold_contents_single_field['threshold']['field'].pop()
with self.assertRaisesRegex(ValueError, "Cannot downgrade a threshold rule that has a defined cardinality"):
downgrade(v712_threshold_contents_single_field, "7.9")
v712_no_cardinality = copy.deepcopy(v712_threshold_contents_single_field)
v712_no_cardinality['threshold'].pop('cardinality')
self.assertEqual(downgrade(v712_no_cardinality, "7.9"), self.v79_threshold_contents)
with self.assertRaises(ValueError):
downgrade(v712_no_cardinality, "7.7")
with self.assertRaisesRegex(ValueError, "Unsupported rule type"):
downgrade(v712_no_cardinality, "7.8")
def test_query_downgrade_8_x(self):
"""Downgrade a standard KQL rule."""
if Version.parse(self.current_version, optional_minor_and_patch=True).major > 8:
return
def test_versioned_downgrade_8_x(self):
"""Downgrade a KQL rule with version information"""
if Version.parse(self.current_version, optional_minor_and_patch=True).major > 8:
return
def test_threshold_downgrade_8_x(self):
"""Downgrade a threshold rule that was first introduced in 7.9."""
if Version.parse(self.current_version, optional_minor_and_patch=True).major > 7:
return
def test_eql_validation(self):
base_fields = {
"author": ["Elastic"],
"description": "test description",
"index": ["filebeat-*"],
"language": "eql",
"license": "Elastic License v2",
"name": "test rule",
"risk_score": 21,
"rule_id": str(uuid.uuid4()),
"severity": "low",
"type": "eql"
}
def build_rule(query):
metadata = {
"creation_date": "1970/01/01",
"updated_date": "1970/01/01",
"min_stack_version": load_current_package_version()
}
data = base_fields.copy()
data["query"] = query
obj = {"metadata": metadata, "rule": data}
return TOMLRuleContents.from_dict(obj)
build_rule("""
process where process.name == "cmd.exe"
""")
example_text_fields = ['client.as.organization.name.text', 'client.user.full_name.text',
'client.user.name.text', 'destination.as.organization.name.text',
'destination.user.full_name.text', 'destination.user.name.text',
'error.message', 'error.stack_trace.text', 'file.path.text',
'file.target_path.text', 'host.os.full.text', 'host.os.name.text',
'host.user.full_name.text', 'host.user.name.text']
for text_field in example_text_fields:
with self.assertRaises(eql.parser.EqlSchemaError):
build_rule(f"""
any where {text_field} == "some string field"
""")
with self.assertRaises(eql.EqlSyntaxError):
build_rule("""
process where process.name == this!is$not#v@lid
""")
with self.assertRaises(eql.EqlSemanticError):
build_rule("""
process where process.invalid_field == "hello world"
""")
with self.assertRaises(eql.EqlTypeMismatchError):
build_rule("""
process where process.pid == "some string field"
""")
class TestVersionLockSchema(unittest.TestCase):
"""Test that the version lock has proper entries."""
@classmethod
def setUpClass(cls):
cls.version_lock_contents = {
"33f306e8-417c-411b-965c-c2812d6d3f4d": {
"rule_name": "Remote File Download via PowerShell",
"sha256": "8679cd72bf85b67dde3dcfdaba749ed1fa6560bca5efd03ed41c76a500ce31d6",
"type": "eql",
"version": 4
},
"34fde489-94b0-4500-a76f-b8a157cf9269": {
"min_stack_version": "8.2",
"previous": {
"7.13": {
"rule_name": "Telnet Port Activity",
"sha256": "3dd4a438c915920e6ddb0a5212603af5d94fb8a6b51a32f223d930d7e3becb89",
"type": "query",
"version": 9
}
},
"rule_name": "Telnet Port Activity",
"sha256": "b0bdfa73639226fb83eadc0303ad1801e0707743f96a36209aa58228d3bf6a89",
"type": "query",
"version": 10
}
}
def test_version_lock_no_previous(self):
"""Pass field validation on version lock without nested previous fields"""
version_lock_contents = copy.deepcopy(self.version_lock_contents)
VersionLockFile.from_dict(dict(data=version_lock_contents))
def test_version_lock_has_nested_previous(self):
"""Fail field validation on version lock with nested previous fields"""
version_lock_contents = copy.deepcopy(self.version_lock_contents)
with self.assertRaises(ValidationError):
previous = version_lock_contents["34fde489-94b0-4500-a76f-b8a157cf9269"]["previous"]
version_lock_contents["34fde489-94b0-4500-a76f-b8a157cf9269"]["previous"]["previous"] = previous
VersionLockFile.from_dict(dict(data=version_lock_contents))
class TestVersions(unittest.TestCase):
"""Test that schema versioning aligns."""
def test_stack_schema_map(self):
"""Test to ensure that an entry exists in the stack-schema-map for the current package version."""
package_version = Version.parse(load_current_package_version(), optional_minor_and_patch=True)
stack_map = utils.load_etc_dump('stack-schema-map.yaml')
err_msg = f'There is no entry defined for the current package ({package_version}) in the stack-schema-map'
self.assertIn(package_version, [Version.parse(v) for v in stack_map], err_msg)