Skip to content

Commit 464d5e6

Browse files
authored
Fix kibana-upload and remove cumbersome dataclasses (#216)
* Fix kibana-upload and remove cumbersom dataclasses * Linting fixes
1 parent aec3ec3 commit 464d5e6

File tree

4 files changed

+28
-82
lines changed

4 files changed

+28
-82
lines changed

detection_rules/eswrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def kibana_upload(toml_files, kibana_url, cloud_id, user, password):
258258
meta["original"] = dict(id=rule.id, **rule.metadata)
259259
payload["rule_id"] = str(uuid4())
260260
payload = downgrade(payload, kibana.version)
261-
rule = RuleResource.from_dict(payload)
261+
rule = RuleResource(payload)
262262
api_payloads.append(rule)
263263

264264
rules = RuleResource.bulk_create(api_payloads)

kibana/resources.py

Lines changed: 24 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,33 @@
22
# or more contributor license agreements. Licensed under the Elastic License;
33
# you may not use this file except in compliance with the Elastic License.
44

5-
from .connector import Kibana
6-
import abc
75
import datetime
8-
from dataclasses import dataclass, field, fields
9-
from dataclasses_json import dataclass_json, config, DataClassJsonMixin
10-
from typing import List, Optional, Type, TypeVar
11-
12-
DEFAULT_PAGE_SIZE = 10
13-
14-
15-
class DataClassJsonPatch(abc.ABC):
16-
"""Temporary class to hold DataClassJsonMixin that we want to overwrite."""
17-
18-
def to_dict(self, *args, **kwargs) -> dict:
19-
return {k: v for k, v in DataClassJsonMixin.to_dict(self, *args, **kwargs).items() if v is not None}
20-
21-
22-
ResourceDataClass = TypeVar('T')
6+
from typing import List, Type
237

8+
from .connector import Kibana
249

25-
def resource(cls: ResourceDataClass) -> ResourceDataClass:
26-
cls = dataclass(cls)
27-
cls = dataclass_json(cls)
28-
# apparently dataclass_json/DataClassJsonMixin completely overwrites this method upon class construction
29-
# which is a little weird, because it means you can't define your own to override it.
30-
# but we want a custom implementation that skips nulls. so we need to overwrite it DataClassJsonPatch.to_dict
31-
# overwrite this method, to drop keys set to None
32-
cls.to_dict = DataClassJsonPatch.to_dict
33-
return cls
10+
DEFAULT_PAGE_SIZE = 10
3411

3512

36-
class RestEndpoint:
13+
class BaseResource(dict):
3714
BASE_URI = ""
15+
ID_FIELD = "id"
3816

39-
40-
@resource
41-
class BaseResource(RestEndpoint):
42-
43-
def _update_from(self, other):
44-
# copy over the attributes from the new one
45-
if not isinstance(other, BaseResource) and isinstance(other, dict):
46-
other = self.from_dict(other)
47-
48-
vars(self).update(vars(other))
17+
@property
18+
def id(self):
19+
return self.get(self.ID_FIELD)
4920

5021
@classmethod
5122
def bulk_create(cls, resources: list):
5223
for r in resources:
5324
assert isinstance(r, cls)
5425

55-
payloads = [r.to_dict() for r in resources]
56-
responses = Kibana.current().post(cls.BASE_URI + "/_bulk_create", data=payloads)
57-
return [cls.from_dict(r) for r in responses]
26+
responses = Kibana.current().post(cls.BASE_URI + "/_bulk_create", data=resources)
27+
return [cls(r) for r in responses]
5828

5929
def create(self):
60-
response = Kibana.current().post(self.BASE_URI, data=self.to_dict())
61-
self._update_from(response)
30+
response = Kibana.current().post(self.BASE_URI, data=self)
31+
self.update(response)
6232
return self
6333

6434
@classmethod
@@ -72,10 +42,10 @@ def find(cls, per_page=None, **params) -> iter:
7242
return ResourceIterator(cls, cls.BASE_URI + "/_find", per_page=per_page, **params)
7343

7444
@classmethod
75-
def from_id(cls, resource_id, id_field="id") -> 'BaseResource':
76-
return Kibana.current().get(cls.BASE_URI, params={id_field: resource_id})
45+
def from_id(cls, resource_id) -> 'BaseResource':
46+
return Kibana.current().get(cls.BASE_URI, params={self.ID_FIELD: resource_id})
7747

78-
def update(self):
48+
def put(self):
7949
response = Kibana.current().put(self.BASE_URI, data=self.to_dict())
8050
self._update_from(response)
8151
return self
@@ -118,45 +88,16 @@ def __next__(self) -> BaseResource:
11888
self._batch()
11989

12090
if self.batch_pos < len(self.batch):
121-
result = self.cls.from_dict(self.batch[self.batch_pos])
91+
result = self.cls(self.batch[self.batch_pos])
12292
self.batch_pos += 1
12393
return result
12494

12595
raise StopIteration()
12696

12797

128-
@resource
12998
class RuleResource(BaseResource):
13099
BASE_URI = "/api/detection_engine/rules"
131100

132-
description: str
133-
name: str
134-
risk_score: int
135-
severity: str
136-
type_: str = field(metadata=config(field_name="type"))
137-
138-
actions: Optional[List] = None
139-
author: Optional[List[str]] = None
140-
building_block_type: Optional[str] = None
141-
enabled: Optional[bool] = None
142-
exceptions_list: Optional[List] = None
143-
false_positives: Optional[List[str]] = None
144-
filters: Optional[List[dict]] = None
145-
from_: Optional[str] = field(metadata=config(field_name="from"), default=None)
146-
id: Optional[str] = None
147-
interval: Optional[str] = None
148-
license: Optional[str] = None
149-
language: Optional[str] = None
150-
meta: Optional[dict] = None
151-
note: Optional[str] = None
152-
references: Optional[List[str]] = None
153-
rule_id: Optional[str] = None
154-
tags: Optional[List[str]] = None
155-
throttle: Optional[str] = None
156-
threat: Optional[List[dict]] = None
157-
to_: Optional[str] = field(metadata=config(field_name="to"), default=None)
158-
query: Optional[str] = None
159-
160101
@staticmethod
161102
def _add_internal_filter(is_internal: bool, params: dict) -> dict:
162103
custom_filter = f'alert.attributes.tags:"__internal_immutable:{str(is_internal).lower()}"'
@@ -185,20 +126,23 @@ def find_elastic(cls, **params):
185126
params = cls._add_internal_filter(True, params)
186127
return cls.find(**params)
187128

188-
def update(self):
129+
def put(self):
189130
# id and rule_id are mutually exclusive
190-
rule_id = self.rule_id
191-
self.rule_id = None
131+
rule_id = self.get("rule_id")
132+
self.pop("rule_id", None)
192133

193134
try:
194135
# apparently Kibana doesn't like `rule_id` for existing documents
195136
return super(RuleResource, self).update()
196137
except Exception:
197138
# if it fails, restore the id back
198-
self.rule_id = rule_id
139+
if rule_id:
140+
self["rule_id"] = rule_id
141+
199142
raise
200143

201-
class Signal(RestEndpoint):
144+
145+
class Signal(BaseResource):
202146
BASE_URI = "/api/detection_engine/signals"
203147

204148
def __init__(self):

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ toml==0.10.0
55
requests==2.22.0
66
Click==7.0
77
PyYAML~=5.3
8-
dataclasses-json~=0.4.2
98
eql~=0.9
109
elasticsearch~=7.5.1
1110

tests/test_schemas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def setUpClass(cls):
1818
cls.compatible_rule = Rule("test.toml", {
1919
"author": ["Elastic"],
2020
"description": "test description",
21+
"index": ["filebeat-*"],
2122
"language": "kuery",
2223
"license": "Elastic License",
2324
"name": "test rule",
@@ -55,6 +56,7 @@ def test_query_downgrade(self):
5556
self.assertDictEqual(downgrade(api_contents, "7.8"), {
5657
# "author": ["Elastic"],
5758
"description": "test description",
59+
"index": ["filebeat-*"],
5860
"language": "kuery",
5961
# "license": "Elastic License",
6062
"name": "test rule",
@@ -77,6 +79,7 @@ def test_versioned_downgrade(self):
7779
self.assertDictEqual(downgrade(api_contents, "7.8"), {
7880
# "author": ["Elastic"],
7981
"description": "test description",
82+
"index": ["filebeat-*"],
8083
"language": "kuery",
8184
# "license": "Elastic License",
8285
"name": "test rule",

0 commit comments

Comments
 (0)