howler-api 3.0.0.dev374__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of howler-api might be problematic. Click here for more details.

Files changed (198) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +168 -0
  3. howler/actions/add_label.py +111 -0
  4. howler/actions/add_to_bundle.py +159 -0
  5. howler/actions/change_field.py +76 -0
  6. howler/actions/demote.py +160 -0
  7. howler/actions/example_plugin.py +104 -0
  8. howler/actions/prioritization.py +93 -0
  9. howler/actions/promote.py +147 -0
  10. howler/actions/remove_from_bundle.py +133 -0
  11. howler/actions/remove_label.py +111 -0
  12. howler/actions/transition.py +200 -0
  13. howler/api/__init__.py +249 -0
  14. howler/api/base.py +88 -0
  15. howler/api/socket.py +114 -0
  16. howler/api/v1/__init__.py +97 -0
  17. howler/api/v1/action.py +372 -0
  18. howler/api/v1/analytic.py +748 -0
  19. howler/api/v1/auth.py +382 -0
  20. howler/api/v1/clue.py +99 -0
  21. howler/api/v1/configs.py +58 -0
  22. howler/api/v1/dossier.py +222 -0
  23. howler/api/v1/help.py +28 -0
  24. howler/api/v1/hit.py +1181 -0
  25. howler/api/v1/notebook.py +82 -0
  26. howler/api/v1/overview.py +191 -0
  27. howler/api/v1/search.py +788 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +416 -0
  31. howler/api/v1/utils/__init__.py +0 -0
  32. howler/api/v1/utils/etag.py +84 -0
  33. howler/api/v1/view.py +288 -0
  34. howler/app.py +235 -0
  35. howler/common/README.md +125 -0
  36. howler/common/__init__.py +0 -0
  37. howler/common/classification.py +979 -0
  38. howler/common/classification.yml +107 -0
  39. howler/common/exceptions.py +167 -0
  40. howler/common/loader.py +154 -0
  41. howler/common/logging/__init__.py +241 -0
  42. howler/common/logging/audit.py +138 -0
  43. howler/common/logging/format.py +38 -0
  44. howler/common/net.py +79 -0
  45. howler/common/net_static.py +1494 -0
  46. howler/common/random_user.py +316 -0
  47. howler/common/swagger.py +117 -0
  48. howler/config.py +64 -0
  49. howler/cronjobs/__init__.py +29 -0
  50. howler/cronjobs/retention.py +61 -0
  51. howler/cronjobs/rules.py +274 -0
  52. howler/cronjobs/view_cleanup.py +88 -0
  53. howler/datastore/README.md +112 -0
  54. howler/datastore/__init__.py +0 -0
  55. howler/datastore/bulk.py +72 -0
  56. howler/datastore/collection.py +2342 -0
  57. howler/datastore/constants.py +119 -0
  58. howler/datastore/exceptions.py +41 -0
  59. howler/datastore/howler_store.py +105 -0
  60. howler/datastore/migrations/fix_process.py +41 -0
  61. howler/datastore/operations.py +130 -0
  62. howler/datastore/schemas.py +90 -0
  63. howler/datastore/store.py +231 -0
  64. howler/datastore/support/__init__.py +0 -0
  65. howler/datastore/support/build.py +215 -0
  66. howler/datastore/support/schemas.py +90 -0
  67. howler/datastore/types.py +22 -0
  68. howler/error.py +91 -0
  69. howler/external/__init__.py +0 -0
  70. howler/external/generate_mitre.py +96 -0
  71. howler/external/generate_sigma_rules.py +31 -0
  72. howler/external/generate_tlds.py +47 -0
  73. howler/external/reindex_data.py +66 -0
  74. howler/external/wipe_databases.py +58 -0
  75. howler/gunicorn_config.py +25 -0
  76. howler/healthz.py +47 -0
  77. howler/helper/__init__.py +0 -0
  78. howler/helper/azure.py +50 -0
  79. howler/helper/discover.py +59 -0
  80. howler/helper/hit.py +236 -0
  81. howler/helper/oauth.py +247 -0
  82. howler/helper/search.py +92 -0
  83. howler/helper/workflow.py +110 -0
  84. howler/helper/ws.py +378 -0
  85. howler/odm/README.md +102 -0
  86. howler/odm/__init__.py +1 -0
  87. howler/odm/base.py +1543 -0
  88. howler/odm/charter.txt +146 -0
  89. howler/odm/helper.py +416 -0
  90. howler/odm/howler_enum.py +25 -0
  91. howler/odm/models/__init__.py +0 -0
  92. howler/odm/models/action.py +33 -0
  93. howler/odm/models/analytic.py +90 -0
  94. howler/odm/models/assemblyline.py +48 -0
  95. howler/odm/models/aws.py +23 -0
  96. howler/odm/models/azure.py +16 -0
  97. howler/odm/models/cbs.py +44 -0
  98. howler/odm/models/config.py +558 -0
  99. howler/odm/models/dossier.py +33 -0
  100. howler/odm/models/ecs/__init__.py +0 -0
  101. howler/odm/models/ecs/agent.py +17 -0
  102. howler/odm/models/ecs/autonomous_system.py +16 -0
  103. howler/odm/models/ecs/client.py +149 -0
  104. howler/odm/models/ecs/cloud.py +141 -0
  105. howler/odm/models/ecs/code_signature.py +27 -0
  106. howler/odm/models/ecs/container.py +32 -0
  107. howler/odm/models/ecs/dns.py +62 -0
  108. howler/odm/models/ecs/egress.py +10 -0
  109. howler/odm/models/ecs/elf.py +74 -0
  110. howler/odm/models/ecs/email.py +122 -0
  111. howler/odm/models/ecs/error.py +14 -0
  112. howler/odm/models/ecs/event.py +140 -0
  113. howler/odm/models/ecs/faas.py +24 -0
  114. howler/odm/models/ecs/file.py +84 -0
  115. howler/odm/models/ecs/geo.py +30 -0
  116. howler/odm/models/ecs/group.py +18 -0
  117. howler/odm/models/ecs/hash.py +16 -0
  118. howler/odm/models/ecs/host.py +17 -0
  119. howler/odm/models/ecs/http.py +37 -0
  120. howler/odm/models/ecs/ingress.py +12 -0
  121. howler/odm/models/ecs/interface.py +21 -0
  122. howler/odm/models/ecs/network.py +30 -0
  123. howler/odm/models/ecs/observer.py +45 -0
  124. howler/odm/models/ecs/organization.py +12 -0
  125. howler/odm/models/ecs/os.py +21 -0
  126. howler/odm/models/ecs/pe.py +17 -0
  127. howler/odm/models/ecs/process.py +216 -0
  128. howler/odm/models/ecs/registry.py +26 -0
  129. howler/odm/models/ecs/related.py +45 -0
  130. howler/odm/models/ecs/rule.py +51 -0
  131. howler/odm/models/ecs/server.py +24 -0
  132. howler/odm/models/ecs/threat.py +247 -0
  133. howler/odm/models/ecs/tls.py +58 -0
  134. howler/odm/models/ecs/url.py +51 -0
  135. howler/odm/models/ecs/user.py +57 -0
  136. howler/odm/models/ecs/user_agent.py +20 -0
  137. howler/odm/models/ecs/vulnerability.py +41 -0
  138. howler/odm/models/gcp.py +16 -0
  139. howler/odm/models/hit.py +356 -0
  140. howler/odm/models/howler_data.py +328 -0
  141. howler/odm/models/lead.py +24 -0
  142. howler/odm/models/localized_label.py +13 -0
  143. howler/odm/models/overview.py +16 -0
  144. howler/odm/models/pivot.py +40 -0
  145. howler/odm/models/template.py +24 -0
  146. howler/odm/models/user.py +83 -0
  147. howler/odm/models/view.py +34 -0
  148. howler/odm/random_data.py +888 -0
  149. howler/odm/randomizer.py +609 -0
  150. howler/patched.py +5 -0
  151. howler/plugins/__init__.py +25 -0
  152. howler/plugins/config.py +123 -0
  153. howler/remote/__init__.py +0 -0
  154. howler/remote/datatypes/README.md +355 -0
  155. howler/remote/datatypes/__init__.py +98 -0
  156. howler/remote/datatypes/counters.py +63 -0
  157. howler/remote/datatypes/events.py +66 -0
  158. howler/remote/datatypes/hash.py +206 -0
  159. howler/remote/datatypes/lock.py +42 -0
  160. howler/remote/datatypes/queues/__init__.py +0 -0
  161. howler/remote/datatypes/queues/comms.py +59 -0
  162. howler/remote/datatypes/queues/multi.py +32 -0
  163. howler/remote/datatypes/queues/named.py +93 -0
  164. howler/remote/datatypes/queues/priority.py +215 -0
  165. howler/remote/datatypes/set.py +118 -0
  166. howler/remote/datatypes/user_quota_tracker.py +54 -0
  167. howler/security/__init__.py +253 -0
  168. howler/security/socket.py +108 -0
  169. howler/security/utils.py +185 -0
  170. howler/services/__init__.py +0 -0
  171. howler/services/action_service.py +111 -0
  172. howler/services/analytic_service.py +128 -0
  173. howler/services/auth_service.py +323 -0
  174. howler/services/config_service.py +128 -0
  175. howler/services/dossier_service.py +252 -0
  176. howler/services/event_service.py +93 -0
  177. howler/services/hit_service.py +893 -0
  178. howler/services/jwt_service.py +158 -0
  179. howler/services/lucene_service.py +286 -0
  180. howler/services/notebook_service.py +119 -0
  181. howler/services/overview_service.py +44 -0
  182. howler/services/template_service.py +45 -0
  183. howler/services/user_service.py +331 -0
  184. howler/utils/__init__.py +0 -0
  185. howler/utils/annotations.py +28 -0
  186. howler/utils/chunk.py +38 -0
  187. howler/utils/dict_utils.py +200 -0
  188. howler/utils/isotime.py +17 -0
  189. howler/utils/list_utils.py +11 -0
  190. howler/utils/lucene.py +77 -0
  191. howler/utils/path.py +27 -0
  192. howler/utils/socket_utils.py +61 -0
  193. howler/utils/str_utils.py +256 -0
  194. howler/utils/uid.py +47 -0
  195. howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
  196. howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
  197. howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
  198. howler_api-3.0.0.dev374.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,206 @@
1
+ from flask import request
2
+
3
+ from howler.api import (
4
+ bad_request,
5
+ conflict,
6
+ created,
7
+ forbidden,
8
+ make_subapi_blueprint,
9
+ no_content,
10
+ not_found,
11
+ ok,
12
+ )
13
+ from howler.common.exceptions import HowlerException
14
+ from howler.common.loader import datastore
15
+ from howler.common.logging import get_logger
16
+ from howler.common.swagger import generate_swagger_docs
17
+ from howler.datastore.operations import OdmHelper
18
+ from howler.odm.models.template import Template
19
+ from howler.odm.models.user import User
20
+ from howler.security import api_login
21
+ from howler.utils.str_utils import sanitize_lucene_query
22
+
23
+ SUB_API = "template"
24
+ template_api = make_subapi_blueprint(SUB_API, api_version=1)
25
+ template_api._doc = "Manage the different templates created for viewing hits"
26
+
27
+ logger = get_logger(__file__)
28
+
29
+ template_helper = OdmHelper(Template)
30
+
31
+
32
+ @generate_swagger_docs()
33
+ @template_api.route("/", methods=["GET"])
34
+ @api_login(required_priv=["R"])
35
+ def get_templates(**kwargs):
36
+ """Get a list of templates the user can use to render hits
37
+
38
+ Variables:
39
+ None
40
+
41
+ Optional Arguments:
42
+ None
43
+
44
+ Result Example:
45
+ [
46
+ ...templates # A list of templates the user can use
47
+ ]
48
+ """
49
+ try:
50
+ return ok(
51
+ datastore().template.search(
52
+ f"type:global OR owner:{kwargs['user']['uname']}",
53
+ as_obj=False,
54
+ rows=10000,
55
+ )["items"]
56
+ )
57
+ except ValueError as e:
58
+ return bad_request(err=str(e))
59
+
60
+
61
+ @generate_swagger_docs()
62
+ @template_api.route("/", methods=["POST"])
63
+ @api_login(required_priv=["R", "W"])
64
+ def create_template(**kwargs):
65
+ """Create a new template
66
+
67
+ Variables:
68
+ None
69
+
70
+ Optional Arguments:
71
+ None
72
+
73
+ Data Block:
74
+ {
75
+ "analytic": "analytic name" # The analytic this template applies to
76
+ "detection": "detection name" # The detection this template applies to
77
+ "type": "global" # The type of template to create
78
+ "keys": ["howler.id", "howler.hash"] # The keys to show when this template matches a hit
79
+ }
80
+
81
+ Result Example:
82
+ {
83
+ ...template # The new template data
84
+ }
85
+ """
86
+ template_data = request.json
87
+ if not isinstance(template_data, dict):
88
+ return bad_request(err="Invalid data format")
89
+
90
+ if "keys" not in template_data:
91
+ return bad_request(err="You must specify a list of keys when creating a template!")
92
+
93
+ storage = datastore()
94
+
95
+ try:
96
+ template = Template(template_data)
97
+
98
+ if template.type == "personal":
99
+ template.owner = kwargs["user"]["uname"]
100
+ else:
101
+ template.owner = None
102
+
103
+ query_str = f"analytic:{sanitize_lucene_query(template.analytic)} AND type:{template.type}"
104
+
105
+ if template.type == "personal":
106
+ query_str += f" AND owner:{template.owner}"
107
+
108
+ if template.detection:
109
+ query_str += f" AND detection:{template.detection}"
110
+ else:
111
+ query_str += " AND -_exists_:detection"
112
+
113
+ if storage.template.search(query_str)["total"] > 0:
114
+ return conflict(err="A template covering this case already exists.")
115
+
116
+ storage.template.save(template.template_id, template)
117
+ return created(template)
118
+ except HowlerException as e:
119
+ return bad_request(err=str(e))
120
+
121
+
122
+ @generate_swagger_docs()
123
+ @template_api.route("/<id>", methods=["DELETE"])
124
+ @api_login(required_priv=["W"])
125
+ def delete_template(id: str, user: User, **kwargs):
126
+ """Delete a template
127
+
128
+ Variables:
129
+ id => The id of the template to delete
130
+
131
+ Optional Arguments:
132
+ None
133
+
134
+ Data Block:
135
+ None
136
+
137
+ Result Example:
138
+ {
139
+ "success": true # Did the deletion succeed?
140
+ }
141
+ """
142
+ storage = datastore()
143
+
144
+ if not storage.template.exists(id):
145
+ return not_found(err="This template does not exist")
146
+
147
+ existing_template: Template = storage.template.get_if_exists(id)
148
+
149
+ if existing_template.type == "personal" and existing_template.owner != user.uname:
150
+ return forbidden(err="You cannot delete a personal template that is not owned by you.")
151
+
152
+ if existing_template.type == "global" and "admin" not in user.type:
153
+ return forbidden(err="You cannot delete a global template unless you are an administrator.")
154
+
155
+ result = storage.template.delete(id)
156
+ if result:
157
+ return no_content()
158
+ else:
159
+ return not_found()
160
+
161
+
162
+ @generate_swagger_docs()
163
+ @template_api.route("/<id>", methods=["PUT"])
164
+ @api_login(required_priv=["R", "W"])
165
+ def update_template_fields(id: str, user: User, **kwargs):
166
+ """Update a template's keys
167
+
168
+ Variables:
169
+ id => The id of the template to modify
170
+
171
+ Optional Arguments:
172
+ None
173
+
174
+ Data Block:
175
+ [
176
+ "howler.id",
177
+ "howler.hash"
178
+ ]
179
+
180
+ Result Example:
181
+ {
182
+ ...template # The updated template data
183
+ }
184
+ """
185
+ storage = datastore()
186
+
187
+ if not storage.template.exists(id):
188
+ return not_found(err="This template does not exist")
189
+
190
+ new_fields = request.json
191
+ if not isinstance(new_fields, list) or not all(isinstance(f, str) for f in new_fields):
192
+ return bad_request(err="List of new fields must be a list of strings.")
193
+
194
+ existing_template: Template = storage.template.get_if_exists(id)
195
+
196
+ if existing_template.type == "personal" and existing_template.owner != user.uname:
197
+ return forbidden(err="You cannot update a personal template that is not owned by you.")
198
+
199
+ existing_template.keys = new_fields
200
+
201
+ storage.template.save(existing_template.template_id, existing_template)
202
+
203
+ try:
204
+ return ok(storage.template.get_if_exists(existing_template.template_id, as_obj=False))
205
+ except HowlerException as e:
206
+ return bad_request(err=str(e))
howler/api/v1/tool.py ADDED
@@ -0,0 +1,183 @@
1
+ from typing import Any, Optional
2
+
3
+ from flask import request
4
+
5
+ from howler.api import bad_request, created, make_subapi_blueprint
6
+ from howler.common.exceptions import HowlerException
7
+ from howler.common.loader import datastore
8
+ from howler.common.logging import get_logger
9
+ from howler.common.swagger import generate_swagger_docs
10
+ from howler.datastore.operations import OdmHelper
11
+ from howler.odm.base import _Field
12
+ from howler.odm.models.hit import Hit
13
+ from howler.odm.models.user import User
14
+ from howler.security import api_login
15
+ from howler.services import action_service, analytic_service, hit_service
16
+ from howler.utils.dict_utils import flatten
17
+ from howler.utils.isotime import now_as_iso
18
+ from howler.utils.str_utils import get_parent_key
19
+ from howler.utils.uid import get_random_id
20
+
21
+ SUB_API = "tools"
22
+ tool_api = make_subapi_blueprint(SUB_API, api_version=1)
23
+ tool_api._doc = "Manage the tools"
24
+
25
+ logger = get_logger(__file__)
26
+
27
+ hit_helper = OdmHelper(Hit)
28
+
29
+
30
+ @generate_swagger_docs()
31
+ @tool_api.route("/<tool_name>/hits", methods=["POST", "PUT"])
32
+ @api_login(required_priv=["W"])
33
+ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
34
+ """Create one or many hits for a tool using field mapping.
35
+
36
+ Variables:
37
+ tool_name => Name of the tool the hit is for
38
+
39
+ Arguments:
40
+ None
41
+
42
+ Data Block:
43
+ {
44
+ "map": { # For each field in the hit, list of field data will be copied to
45
+ "source.field.in.raw.data": ["target.field.in.howler.hit.index"],
46
+ ...
47
+ },
48
+ "hits": [ # List of raw hits to create the hit from
49
+ {...},
50
+ {...}
51
+ ]
52
+ }
53
+
54
+ Result Example:
55
+ {
56
+ [ # List of hits IDs/Errors created of the different hits (preserved order)
57
+ {'id': "id1", 'error': None},
58
+ {'id': "id2", 'error': None},
59
+ {'id': None, 'error': "Error message"},
60
+ ]
61
+ }
62
+ """
63
+ data = request.json
64
+ if not isinstance(data, dict):
65
+ return bad_request(err="Invalid data format")
66
+
67
+ field_map = data.pop("map", None)
68
+ hits = data.pop("hits", None)
69
+ ignore_extra_values: bool = bool(request.args.get("ignore_extra_values", False, type=lambda v: v.lower() == "true"))
70
+ logger.debug(f"ignore_extra_values = {ignore_extra_values}")
71
+ # Check data type
72
+ if not isinstance(field_map, dict):
73
+ return bad_request(err="Invalid: 'map' field is missing or invalid.")
74
+
75
+ if not isinstance(hits, list):
76
+ return bad_request(err="Invalid: 'hits' field is missing or invalid.")
77
+ warnings = []
78
+ # Validate field_map targets
79
+ hit_fields = Hit.flat_fields()
80
+ for targets in field_map.values():
81
+ for target in targets:
82
+ # This is checking to see if the target matches one of two cases:
83
+ # Simple fields - hit.obj.key of type str (should match)
84
+ # Compound fields - hit.obj of type dict (should also match)
85
+ # This allows significantly easier creation of hits, since you don't need to deconstruct every dict into
86
+ # individual fields
87
+ if target not in hit_fields and not any(f for f in hit_fields.keys() if get_parent_key(f) == target):
88
+ warning = f"Invalid target field in the map: {target}"
89
+ if ignore_extra_values:
90
+ warnings.append(warning)
91
+ # field_map.pop(target)
92
+ else:
93
+ return bad_request(err=warning)
94
+
95
+ out: list[dict[str, Any]] = []
96
+ odms = []
97
+ bundle_hit: Optional[Hit] = None
98
+ for hit in hits:
99
+ cur_id = get_random_id()
100
+ cur_time = now_as_iso()
101
+ obj: dict[str, Any] = {
102
+ "agent.type": tool_name,
103
+ "event.created": cur_time,
104
+ "event.id": cur_id,
105
+ "howler.id": cur_id,
106
+ "howler.analytic": tool_name,
107
+ "howler.score": 0,
108
+ }
109
+ hit = flatten(hit)
110
+ for source, targets in field_map.items():
111
+ val = hit.get(source, None)
112
+ if val is not None:
113
+ for target in targets:
114
+ _val = val
115
+ try:
116
+ field_data: Optional[_Field] = hit_fields[target]
117
+ except KeyError:
118
+ logger.debug(f"`{target}` not in hit fields")
119
+ field_data = next(
120
+ (v for k, v in hit_fields.items() if get_parent_key(k) == target),
121
+ None,
122
+ )
123
+
124
+ if field_data is not None and field_data.multivalued:
125
+ if not isinstance(_val, list):
126
+ _val = [val]
127
+ obj.setdefault(target, [])
128
+ obj[target].extend(_val)
129
+ else:
130
+ if isinstance(val, list):
131
+ if not len(val):
132
+ continue
133
+
134
+ _val = val[0]
135
+
136
+ obj[target] = _val
137
+
138
+ try:
139
+ odm, warns = hit_service.convert_hit(obj, unique=True, ignore_extra_values=ignore_extra_values)
140
+
141
+ if odm.howler.is_bundle and bundle_hit is None:
142
+ bundle_hit = odm
143
+ elif odm.howler.is_bundle:
144
+ return bad_request(err="You can only specify one bundle hit!")
145
+ else:
146
+ odms.append(odm)
147
+
148
+ out.append(
149
+ {
150
+ "id": odm.howler.id,
151
+ "error": None,
152
+ "warn": warns,
153
+ }
154
+ )
155
+ except HowlerException as e:
156
+ logger.warning(f"{type(e).__name__} when saving {cur_id}!")
157
+ logger.warning(e)
158
+
159
+ out.append({"id": None, "error": str(e)})
160
+ # If there are any errors...
161
+ if any([obj["error"] for obj in out]):
162
+ return bad_request(out, warnings=warnings, err="No valid hits were provided")
163
+ else:
164
+ for odm in odms:
165
+ if bundle_hit is not None:
166
+ bundle_hit.howler.hits.append(odm.howler.id)
167
+ bundle_hit.howler.bundle_size += 1
168
+ odm.howler.bundles.append(bundle_hit.howler.id)
169
+
170
+ hit_service.create_hit(odm.howler.id, odm, user=user["uname"])
171
+
172
+ analytic_service.save_from_hit(odm, user)
173
+
174
+ if bundle_hit:
175
+ hit_service.create_hit(bundle_hit.howler.id, bundle_hit, user=user["uname"])
176
+
177
+ analytic_service.save_from_hit(bundle_hit, user)
178
+
179
+ datastore().hit.commit()
180
+
181
+ action_service.bulk_execute_on_query(f"howler.id:({' OR '.join(entry['id'] for entry in out)})", user=user)
182
+
183
+ return created(out, warnings=warnings)