howler-api 2.13.0.dev329__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.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +167 -0
- howler/actions/add_label.py +111 -0
- howler/actions/add_to_bundle.py +159 -0
- howler/actions/change_field.py +76 -0
- howler/actions/demote.py +160 -0
- howler/actions/example_plugin.py +104 -0
- howler/actions/prioritization.py +93 -0
- howler/actions/promote.py +147 -0
- howler/actions/remove_from_bundle.py +133 -0
- howler/actions/remove_label.py +111 -0
- howler/actions/transition.py +200 -0
- howler/api/__init__.py +249 -0
- howler/api/base.py +88 -0
- howler/api/socket.py +114 -0
- howler/api/v1/__init__.py +97 -0
- howler/api/v1/action.py +372 -0
- howler/api/v1/analytic.py +748 -0
- howler/api/v1/auth.py +382 -0
- howler/api/v1/borealis.py +101 -0
- howler/api/v1/configs.py +55 -0
- howler/api/v1/dossier.py +222 -0
- howler/api/v1/help.py +28 -0
- howler/api/v1/hit.py +1181 -0
- howler/api/v1/notebook.py +82 -0
- howler/api/v1/overview.py +191 -0
- howler/api/v1/search.py +715 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +414 -0
- howler/api/v1/utils/__init__.py +0 -0
- howler/api/v1/utils/etag.py +84 -0
- howler/api/v1/view.py +288 -0
- howler/app.py +235 -0
- howler/common/README.md +144 -0
- howler/common/__init__.py +0 -0
- howler/common/classification.py +979 -0
- howler/common/classification.yml +107 -0
- howler/common/exceptions.py +167 -0
- howler/common/hexdump.py +48 -0
- howler/common/iprange.py +171 -0
- howler/common/loader.py +154 -0
- howler/common/logging/__init__.py +241 -0
- howler/common/logging/audit.py +138 -0
- howler/common/logging/format.py +38 -0
- howler/common/net.py +79 -0
- howler/common/net_static.py +1494 -0
- howler/common/random_user.py +316 -0
- howler/common/swagger.py +117 -0
- howler/config.py +64 -0
- howler/cronjobs/__init__.py +29 -0
- howler/cronjobs/retention.py +61 -0
- howler/cronjobs/rules.py +274 -0
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +112 -0
- howler/datastore/__init__.py +0 -0
- howler/datastore/bulk.py +72 -0
- howler/datastore/collection.py +2327 -0
- howler/datastore/constants.py +117 -0
- howler/datastore/exceptions.py +41 -0
- howler/datastore/howler_store.py +105 -0
- howler/datastore/migrations/fix_process.py +41 -0
- howler/datastore/operations.py +130 -0
- howler/datastore/schemas.py +90 -0
- howler/datastore/store.py +231 -0
- howler/datastore/support/__init__.py +0 -0
- howler/datastore/support/build.py +214 -0
- howler/datastore/support/schemas.py +90 -0
- howler/datastore/types.py +22 -0
- howler/error.py +91 -0
- howler/external/__init__.py +0 -0
- howler/external/generate_mitre.py +96 -0
- howler/external/generate_sigma_rules.py +31 -0
- howler/external/generate_tlds.py +47 -0
- howler/external/reindex_data.py +46 -0
- howler/external/wipe_databases.py +58 -0
- howler/gunicorn_config.py +25 -0
- howler/healthz.py +47 -0
- howler/helper/__init__.py +0 -0
- howler/helper/azure.py +50 -0
- howler/helper/discover.py +59 -0
- howler/helper/hit.py +236 -0
- howler/helper/oauth.py +247 -0
- howler/helper/search.py +92 -0
- howler/helper/workflow.py +110 -0
- howler/helper/ws.py +378 -0
- howler/odm/README.md +102 -0
- howler/odm/__init__.py +1 -0
- howler/odm/base.py +1504 -0
- howler/odm/charter.txt +146 -0
- howler/odm/helper.py +416 -0
- howler/odm/howler_enum.py +25 -0
- howler/odm/models/__init__.py +0 -0
- howler/odm/models/action.py +33 -0
- howler/odm/models/analytic.py +90 -0
- howler/odm/models/assemblyline.py +48 -0
- howler/odm/models/aws.py +23 -0
- howler/odm/models/azure.py +16 -0
- howler/odm/models/cbs.py +44 -0
- howler/odm/models/config.py +558 -0
- howler/odm/models/dossier.py +33 -0
- howler/odm/models/ecs/__init__.py +0 -0
- howler/odm/models/ecs/agent.py +17 -0
- howler/odm/models/ecs/autonomous_system.py +16 -0
- howler/odm/models/ecs/client.py +149 -0
- howler/odm/models/ecs/cloud.py +141 -0
- howler/odm/models/ecs/code_signature.py +27 -0
- howler/odm/models/ecs/container.py +32 -0
- howler/odm/models/ecs/dns.py +62 -0
- howler/odm/models/ecs/egress.py +10 -0
- howler/odm/models/ecs/elf.py +74 -0
- howler/odm/models/ecs/email.py +122 -0
- howler/odm/models/ecs/error.py +14 -0
- howler/odm/models/ecs/event.py +140 -0
- howler/odm/models/ecs/faas.py +24 -0
- howler/odm/models/ecs/file.py +84 -0
- howler/odm/models/ecs/geo.py +30 -0
- howler/odm/models/ecs/group.py +18 -0
- howler/odm/models/ecs/hash.py +16 -0
- howler/odm/models/ecs/host.py +17 -0
- howler/odm/models/ecs/http.py +37 -0
- howler/odm/models/ecs/ingress.py +12 -0
- howler/odm/models/ecs/interface.py +21 -0
- howler/odm/models/ecs/network.py +30 -0
- howler/odm/models/ecs/observer.py +45 -0
- howler/odm/models/ecs/organization.py +12 -0
- howler/odm/models/ecs/os.py +21 -0
- howler/odm/models/ecs/pe.py +17 -0
- howler/odm/models/ecs/process.py +216 -0
- howler/odm/models/ecs/registry.py +26 -0
- howler/odm/models/ecs/related.py +45 -0
- howler/odm/models/ecs/rule.py +51 -0
- howler/odm/models/ecs/server.py +24 -0
- howler/odm/models/ecs/threat.py +247 -0
- howler/odm/models/ecs/tls.py +58 -0
- howler/odm/models/ecs/url.py +51 -0
- howler/odm/models/ecs/user.py +57 -0
- howler/odm/models/ecs/user_agent.py +20 -0
- howler/odm/models/ecs/vulnerability.py +41 -0
- howler/odm/models/gcp.py +16 -0
- howler/odm/models/hit.py +356 -0
- howler/odm/models/howler_data.py +328 -0
- howler/odm/models/lead.py +33 -0
- howler/odm/models/localized_label.py +13 -0
- howler/odm/models/overview.py +16 -0
- howler/odm/models/pivot.py +40 -0
- howler/odm/models/template.py +24 -0
- howler/odm/models/user.py +83 -0
- howler/odm/models/view.py +34 -0
- howler/odm/random_data.py +888 -0
- howler/odm/randomizer.py +606 -0
- howler/patched.py +5 -0
- howler/plugins/__init__.py +25 -0
- howler/plugins/config.py +123 -0
- howler/remote/__init__.py +0 -0
- howler/remote/datatypes/README.md +355 -0
- howler/remote/datatypes/__init__.py +98 -0
- howler/remote/datatypes/counters.py +63 -0
- howler/remote/datatypes/events.py +66 -0
- howler/remote/datatypes/hash.py +206 -0
- howler/remote/datatypes/lock.py +42 -0
- howler/remote/datatypes/queues/__init__.py +0 -0
- howler/remote/datatypes/queues/comms.py +59 -0
- howler/remote/datatypes/queues/multi.py +32 -0
- howler/remote/datatypes/queues/named.py +93 -0
- howler/remote/datatypes/queues/priority.py +215 -0
- howler/remote/datatypes/set.py +118 -0
- howler/remote/datatypes/user_quota_tracker.py +54 -0
- howler/security/__init__.py +253 -0
- howler/security/socket.py +108 -0
- howler/security/utils.py +185 -0
- howler/services/__init__.py +0 -0
- howler/services/action_service.py +111 -0
- howler/services/analytic_service.py +128 -0
- howler/services/auth_service.py +323 -0
- howler/services/config_service.py +128 -0
- howler/services/dossier_service.py +252 -0
- howler/services/event_service.py +93 -0
- howler/services/hit_service.py +893 -0
- howler/services/jwt_service.py +158 -0
- howler/services/lucene_service.py +286 -0
- howler/services/notebook_service.py +119 -0
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/services/user_service.py +330 -0
- howler/utils/__init__.py +0 -0
- howler/utils/annotations.py +28 -0
- howler/utils/chunk.py +38 -0
- howler/utils/dict_utils.py +200 -0
- howler/utils/isotime.py +17 -0
- howler/utils/list_utils.py +11 -0
- howler/utils/lucene.py +77 -0
- howler/utils/path.py +27 -0
- howler/utils/socket_utils.py +61 -0
- howler/utils/str_utils.py +256 -0
- howler/utils/uid.py +47 -0
- howler_api-2.13.0.dev329.dist-info/METADATA +71 -0
- howler_api-2.13.0.dev329.dist-info/RECORD +200 -0
- howler_api-2.13.0.dev329.dist-info/WHEEL +4 -0
- howler_api-2.13.0.dev329.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)
|