diff --git a/detection_rules/rule.py b/detection_rules/rule.py index d26624e9b04..99df1d95480 100644 --- a/detection_rules/rule.py +++ b/detection_rules/rule.py @@ -183,7 +183,7 @@ class BaseRuleData(MarshmallowDataclassMixin): def save_schema(cls): """Save the schema as a jsonschema.""" fields: List[dataclasses.Field] = dataclasses.fields(cls) - type_field = next(field for field in fields if field.name == "type") + type_field = next(f for f in fields if f.name == "type") rule_type = typing.get_args(type_field.type)[0] if cls != BaseRuleData else "base" schema = cls.jsonschema() version_dir = SCHEMA_DIR / "master" @@ -256,9 +256,9 @@ class ThresholdCardinality: field: str value: definitions.ThresholdValue - field: List[definitions.NonEmptyStr] + field: definitions.CardinalityFields value: definitions.ThresholdValue - cardinality: Optional[ThresholdCardinality] + cardinality: Optional[List[ThresholdCardinality]] type: Literal["threshold"] threshold: ThresholdMapping diff --git a/detection_rules/rule_formatter.py b/detection_rules/rule_formatter.py index ff43dab9791..bb4cb485d21 100644 --- a/detection_rules/rule_formatter.py +++ b/detection_rules/rule_formatter.py @@ -206,5 +206,5 @@ def _do_write(_data, _contents): _do_write(data, _contents) finally: - if needs_close: + if needs_close and hasattr(outfile, "close"): outfile.close() diff --git a/detection_rules/schemas/__init__.py b/detection_rules/schemas/__init__.py index f20eb1b30ca..2787dcf199f 100644 --- a/detection_rules/schemas/__init__.py +++ b/detection_rules/schemas/__init__.py @@ -123,7 +123,7 @@ def downgrade_threshold_to_7_11(version: Version, api_contents: dict) -> dict: if len(threshold_field) > 1: raise ValueError('Cannot downgrade a threshold rule that has multiple threshold fields defined') - if threshold.get('cardinality', {}).get('field') or threshold.get('cardinality', {}).get('value'): + if threshold.get('cardinality'): raise ValueError('Cannot downgrade a threshold rule that has a defined cardinality') api_contents = api_contents.copy() diff --git a/detection_rules/schemas/definitions.py b/detection_rules/schemas/definitions.py index 9d4bcb3c2d9..1fb005e2915 100644 --- a/detection_rules/schemas/definitions.py +++ b/detection_rules/schemas/definitions.py @@ -5,7 +5,7 @@ """Custom shared definitions for schemas.""" -from typing import Literal, Final +from typing import List, Literal, Final from marshmallow import validate from marshmallow_dataclass import NewType @@ -44,19 +44,21 @@ } +NonEmptyStr = NewType('NonEmptyStr', str, validate=validate.Length(min=1)) + BranchVer = NewType('BranchVer', str, validate=validate.Regexp(BRANCH_PATTERN)) +CardinalityFields = NewType('CardinalityFields', List[NonEmptyStr], validate=validate.Length(min=0, max=3)) CodeString = NewType("CodeString", str) ConditionSemVer = NewType('ConditionSemVer', str, validate=validate.Regexp(CONDITION_VERSION_PATTERN)) Date = NewType('Date', str, validate=validate.Regexp(DATE_PATTERN)) FilterLanguages = Literal["kuery", "lucene"] Interval = NewType('Interval', str, validate=validate.Regexp(INTERVAL_PATTERN)) -PositiveInteger = NewType('PositiveInteger', int, validate=validate.Range(min=1)) Markdown = NewType("MarkdownField", CodeString) Maturity = Literal['development', 'experimental', 'beta', 'production', 'deprecated'] MaxSignals = NewType("MaxSignals", int, validate=validate.Range(min=1)) -NonEmptyStr = NewType('NonEmptyStr', str, validate=validate.Length(min=1)) Operator = Literal['equals'] OSType = Literal['windows', 'linux', 'macos'] +PositiveInteger = NewType('PositiveInteger', int, validate=validate.Range(min=1)) RiskScore = NewType("MaxSignals", int, validate=validate.Range(min=1, max=100)) RuleType = Literal['query', 'saved_query', 'machine_learning', 'eql', 'threshold', 'threat_match'] SemVer = NewType('SemVer', str, validate=validate.Regexp(VERSION_PATTERN)) diff --git a/etc/api_schemas/7.12/7.12.threshold.json b/etc/api_schemas/7.12/7.12.threshold.json index d426ff86832..f5c606b1643 100644 --- a/etc/api_schemas/7.12/7.12.threshold.json +++ b/etc/api_schemas/7.12/7.12.threshold.json @@ -919,35 +919,46 @@ "additionalProperties": false, "properties": { "cardinality": { - "additionalProperties": false, - "properties": { - "field": { - "type": "string" + "items": { + "additionalProperties": false, + "properties": { + "field": { + "type": "string" + }, + "value": { + "description": "ThresholdValue", + "format": "integer", + "minimum": 1, + "type": "number" + } }, - "value": { - "minimum": 1, - "type": "integer" - } + "required": [ + "field", + "value" + ], + "type": "object" }, - "required": [ - "field", - "value" - ], - "type": "object" + "type": "array" }, "field": { + "description": "CardinalityFields", "items": { - "default": "", + "description": "NonEmptyStr", + "minLength": 1, "type": "string" }, + "maxItems": 3, "type": "array" }, "value": { + "description": "ThresholdValue", + "format": "integer", "minimum": 1, - "type": "integer" + "type": "number" } }, "required": [ + "field", "value" ], "type": "object" diff --git a/etc/api_schemas/7.13/7.13.threshold.json b/etc/api_schemas/7.13/7.13.threshold.json index 724d46ccee9..aeb982b1912 100644 --- a/etc/api_schemas/7.13/7.13.threshold.json +++ b/etc/api_schemas/7.13/7.13.threshold.json @@ -112,6 +112,9 @@ "type": "string" }, "operator": { + "enum": [ + "equals" + ], "type": "string" }, "value": { @@ -151,6 +154,9 @@ "type": "string" }, "operator": { + "enum": [ + "equals" + ], "type": "string" }, "severity": { @@ -178,6 +184,9 @@ "additionalProperties": false, "properties": { "framework": { + "enum": [ + "MITRE ATT&CK" + ], "type": "string" }, "tactic": { @@ -265,30 +274,35 @@ "additionalProperties": false, "properties": { "cardinality": { - "additionalProperties": false, - "properties": { - "field": { - "type": "string" + "items": { + "additionalProperties": false, + "properties": { + "field": { + "type": "string" + }, + "value": { + "description": "ThresholdValue", + "format": "integer", + "minimum": 1, + "type": "number" + } }, - "value": { - "description": "ThresholdValue", - "format": "integer", - "minimum": 1, - "type": "number" - } + "required": [ + "field", + "value" + ], + "type": "object" }, - "required": [ - "field", - "value" - ], - "type": "object" + "type": "array" }, "field": { + "description": "CardinalityFields", "items": { "description": "NonEmptyStr", "minLength": 1, "type": "string" }, + "maxItems": 3, "type": "array" }, "value": { @@ -336,6 +350,9 @@ "type": "string" }, "type": { + "enum": [ + "threshold" + ], "type": "string" } }, diff --git a/etc/api_schemas/7.14/7.14.threshold.json b/etc/api_schemas/7.14/7.14.threshold.json index e0715120611..aeb982b1912 100644 --- a/etc/api_schemas/7.14/7.14.threshold.json +++ b/etc/api_schemas/7.14/7.14.threshold.json @@ -274,30 +274,35 @@ "additionalProperties": false, "properties": { "cardinality": { - "additionalProperties": false, - "properties": { - "field": { - "type": "string" + "items": { + "additionalProperties": false, + "properties": { + "field": { + "type": "string" + }, + "value": { + "description": "ThresholdValue", + "format": "integer", + "minimum": 1, + "type": "number" + } }, - "value": { - "description": "ThresholdValue", - "format": "integer", - "minimum": 1, - "type": "number" - } + "required": [ + "field", + "value" + ], + "type": "object" }, - "required": [ - "field", - "value" - ], - "type": "object" + "type": "array" }, "field": { + "description": "CardinalityFields", "items": { "description": "NonEmptyStr", "minLength": 1, "type": "string" }, + "maxItems": 3, "type": "array" }, "value": { diff --git a/etc/api_schemas/master/master.threshold.json b/etc/api_schemas/master/master.threshold.json index e0715120611..aeb982b1912 100644 --- a/etc/api_schemas/master/master.threshold.json +++ b/etc/api_schemas/master/master.threshold.json @@ -274,30 +274,35 @@ "additionalProperties": false, "properties": { "cardinality": { - "additionalProperties": false, - "properties": { - "field": { - "type": "string" + "items": { + "additionalProperties": false, + "properties": { + "field": { + "type": "string" + }, + "value": { + "description": "ThresholdValue", + "format": "integer", + "minimum": 1, + "type": "number" + } }, - "value": { - "description": "ThresholdValue", - "format": "integer", - "minimum": 1, - "type": "number" - } + "required": [ + "field", + "value" + ], + "type": "object" }, - "required": [ - "field", - "value" - ], - "type": "object" + "type": "array" }, "field": { + "description": "CardinalityFields", "items": { "description": "NonEmptyStr", "minLength": 1, "type": "string" }, + "maxItems": 3, "type": "array" }, "value": { diff --git a/etc/stack-schema-map.yaml b/etc/stack-schema-map.yaml index e992786fa37..871ca3764a8 100644 --- a/etc/stack-schema-map.yaml +++ b/etc/stack-schema-map.yaml @@ -9,4 +9,4 @@ "7.14.0": beats: "master" # TODO: 7.14.x - ecs: "1.10.0" + ecs: "master" # TODO: master came out after 7.13.0 release diff --git a/requirements.txt b/requirements.txt index 9292b86002c..3c3e4e79952 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ PyYAML~=5.3 eql==0.9.9 elasticsearch~=7.9 XlsxWriter~=1.3.6 -marshmallow~=3.10.0 +marshmallow~=3.12.2 marshmallow-dataclass[union]~=8.4 # test deps diff --git a/rules/cross-platform/defense_evasion_agent_spoofing_mismatched_id.toml b/rules/cross-platform/defense_evasion_agent_spoofing_mismatched_id.toml new file mode 100644 index 00000000000..484819cbf38 --- /dev/null +++ b/rules/cross-platform/defense_evasion_agent_spoofing_mismatched_id.toml @@ -0,0 +1,48 @@ +[metadata] +creation_date = "2021/07/14" +maturity = "production" +updated_date = "2021/07/14" +min_stack_version = "7.14.0" + +[rule] +author = ["Elastic"] +description = """Detects events which have a mismatch on the expected event agent ID. The status "agent_id_mismatch" +occurs when the expected agent ID associated with the API key does not match the actual agent ID in an event. This could +indicate attempts to spoof events in order to masquerade actual activity to evade detection. +""" +false_positives = [ + """ + This is meant to run only on datasources using agents v7.14+ since versions prior to that will be missing the + necessary field, resulting in false positives. + """, +] +from = "now-9m" +index = ["logs-*", "metrics-*", "traces-*"] +language = "kuery" +license = "Elastic License v2" +name = "Agent Spoofing - Mismatched Agent ID" +risk_score = 73 +rule_id = "3115bd2c-0baa-4df0-80ea-45e474b5ef93" +severity = "high" +tags = ["Elastic", "Threat Detection", "Defense Evasion"] +timestamp_override = "event.ingested" +type = "query" + +query = ''' +event.agent_id_status:agent_id_mismatch +''' + + +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1036" +name = "Masquerading" +reference = "https://attack.mitre.org/techniques/T1036/" + + +[rule.threat.tactic] +id = "TA0005" +name = "Defense Evasion" +reference = "https://attack.mitre.org/tactics/TA0005/" + diff --git a/rules/cross-platform/defense_evasion_agent_spoofing_multiple_hosts.toml b/rules/cross-platform/defense_evasion_agent_spoofing_multiple_hosts.toml new file mode 100644 index 00000000000..5caecf3a49b --- /dev/null +++ b/rules/cross-platform/defense_evasion_agent_spoofing_multiple_hosts.toml @@ -0,0 +1,56 @@ +[metadata] +creation_date = "2021/07/14" +maturity = "production" +updated_date = "2021/07/14" +min_stack_version = "7.14.0" + +[rule] +author = ["Elastic"] +description = """Detects when multiple hosts are using the same agent ID. This could occur in the event of an agent +being taken over and used to inject illegitimate documents into an instance as an attempt to spoof events in order to +masquerade actual activity to evade detection. +""" +false_positives = [ + """ + This is meant to run only on datasources using agents v7.14+ since versions prior to that will be missing the + necessary field, resulting in false positives. + """, +] +from = "now-9m" +index = ["logs-*", "metrics-*", "traces-*"] +language = "kuery" +license = "Elastic License v2" +name = "Agent Spoofing - Multiple Hosts Using Same Agent" +risk_score = 73 +rule_id = "493834ca-f861-414c-8602-150d5505b777" +severity = "high" +tags = ["Elastic", "Threat Detection", "Defense Evasion"] +timestamp_override = "event.ingested" +type = "threshold" + +query = ''' +event.agent_id_status:* +''' + + +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1036" +name = "Masquerading" +reference = "https://attack.mitre.org/techniques/T1036/" + + +[rule.threat.tactic] +id = "TA0005" +name = "Defense Evasion" +reference = "https://attack.mitre.org/tactics/TA0005/" + + +[rule.threshold] +field = ["agent.id"] +value = 2 + +[[rule.threshold.cardinality]] +field = "host.id" +value = 2 diff --git a/tests/test_schemas.py b/tests/test_schemas.py index e78c8326535..68ed2a3d1d0 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -86,10 +86,10 @@ def setUpClass(cls): cls.v712_threshold_rule = dict(copy.deepcopy(cls.v79_threshold_contents), threshold={ 'field': ['destination.bytes', 'process.args'], 'value': 75, - 'cardinality': { + 'cardinality': [{ 'field': 'user.name', 'value': 2 - } + }] }) def test_query_downgrade(self):