2
2
# or more contributor license agreements. Licensed under the Elastic License;
3
3
# you may not use this file except in compliance with the Elastic License.
4
4
5
- from .connector import Kibana
6
- import abc
7
5
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
23
7
8
+ from .connector import Kibana
24
9
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
34
11
35
12
36
- class RestEndpoint :
13
+ class BaseResource ( dict ) :
37
14
BASE_URI = ""
15
+ ID_FIELD = "id"
38
16
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 )
49
20
50
21
@classmethod
51
22
def bulk_create (cls , resources : list ):
52
23
for r in resources :
53
24
assert isinstance (r , cls )
54
25
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 ]
58
28
59
29
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 )
62
32
return self
63
33
64
34
@classmethod
@@ -72,10 +42,10 @@ def find(cls, per_page=None, **params) -> iter:
72
42
return ResourceIterator (cls , cls .BASE_URI + "/_find" , per_page = per_page , ** params )
73
43
74
44
@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 })
77
47
78
- def update (self ):
48
+ def put (self ):
79
49
response = Kibana .current ().put (self .BASE_URI , data = self .to_dict ())
80
50
self ._update_from (response )
81
51
return self
@@ -118,45 +88,16 @@ def __next__(self) -> BaseResource:
118
88
self ._batch ()
119
89
120
90
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 ])
122
92
self .batch_pos += 1
123
93
return result
124
94
125
95
raise StopIteration ()
126
96
127
97
128
- @resource
129
98
class RuleResource (BaseResource ):
130
99
BASE_URI = "/api/detection_engine/rules"
131
100
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
-
160
101
@staticmethod
161
102
def _add_internal_filter (is_internal : bool , params : dict ) -> dict :
162
103
custom_filter = f'alert.attributes.tags:"__internal_immutable:{ str (is_internal ).lower ()} "'
@@ -185,20 +126,23 @@ def find_elastic(cls, **params):
185
126
params = cls ._add_internal_filter (True , params )
186
127
return cls .find (** params )
187
128
188
- def update (self ):
129
+ def put (self ):
189
130
# 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 )
192
133
193
134
try :
194
135
# apparently Kibana doesn't like `rule_id` for existing documents
195
136
return super (RuleResource , self ).update ()
196
137
except Exception :
197
138
# if it fails, restore the id back
198
- self .rule_id = rule_id
139
+ if rule_id :
140
+ self ["rule_id" ] = rule_id
141
+
199
142
raise
200
143
201
- class Signal (RestEndpoint ):
144
+
145
+ class Signal (BaseResource ):
202
146
BASE_URI = "/api/detection_engine/signals"
203
147
204
148
def __init__ (self ):
0 commit comments