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.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +168 -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/clue.py +99 -0
- howler/api/v1/configs.py +58 -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 +788 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +416 -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 +125 -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/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 +2342 -0
- howler/datastore/constants.py +119 -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 +215 -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 +66 -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 +1543 -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 +24 -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 +609 -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 +331 -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-3.0.0.dev374.dist-info/METADATA +71 -0
- howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
- howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
- howler_api-3.0.0.dev374.dist-info/entry_points.txt +8 -0
howler/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from howler.common.logging import get_logger
|
|
8
|
+
from howler.odm.models.user import User
|
|
9
|
+
from howler.plugins import get_plugins
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__file__)
|
|
12
|
+
|
|
13
|
+
PLUGIN_PATH = Path(os.environ.get("HWL_PLUGIN_DIRECTORY", "/etc/howler/plugins"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __sanitize_specification(spec: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
"""Adapt the specification for use in the UI
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
spec (dict[str, Any]): The raw specification
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict[str, Any]: The sanitized specification for use in the UI
|
|
24
|
+
"""
|
|
25
|
+
return {
|
|
26
|
+
**spec,
|
|
27
|
+
"description": {
|
|
28
|
+
**spec["description"],
|
|
29
|
+
"long": re.sub(r"\n +(request_id|query).+", "", spec["description"]["long"])
|
|
30
|
+
.replace("\n ", "\n")
|
|
31
|
+
.replace("Args:", "Args:\n"),
|
|
32
|
+
},
|
|
33
|
+
"steps": [{**step, "args": {k: list(v) for k, v in step["args"].items()}} for step in spec.get("steps", [])],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __sanitize_report(report: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
38
|
+
"""Deduplicate identical entries with different queries
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
report (list[dict[str, Any]]): The unsanitized, verbose report
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
list[dict[str, Any]]: The sanitized, concise report
|
|
45
|
+
"""
|
|
46
|
+
by_message: dict[str, Any] = {}
|
|
47
|
+
|
|
48
|
+
for entry in report:
|
|
49
|
+
# if these three keys match, we should merge the queries that use them both. For example, when multiple hits
|
|
50
|
+
# fail to transition for the same reason.
|
|
51
|
+
key = f"{entry['title']}==={entry['message']}==={entry['outcome']}"
|
|
52
|
+
|
|
53
|
+
if key in by_message:
|
|
54
|
+
by_message[key].append(f'({entry["query"]})')
|
|
55
|
+
else:
|
|
56
|
+
by_message[key] = [f'({entry["query"]})']
|
|
57
|
+
|
|
58
|
+
sanitized: list[dict[str, Any]] = []
|
|
59
|
+
for key, queries in by_message.items():
|
|
60
|
+
(title, message, outcome) = key.split("===")
|
|
61
|
+
|
|
62
|
+
sanitized.append(
|
|
63
|
+
{
|
|
64
|
+
"query": " OR ".join(queries),
|
|
65
|
+
"outcome": outcome,
|
|
66
|
+
"title": title,
|
|
67
|
+
"message": message,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return sanitized
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def execute(
|
|
75
|
+
operation_id: str,
|
|
76
|
+
query: str,
|
|
77
|
+
user: User | None,
|
|
78
|
+
request_id: Optional[str] = None,
|
|
79
|
+
**kwargs,
|
|
80
|
+
) -> list[dict[str, Any]]:
|
|
81
|
+
"""Execute a specification
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
operation_id (str): The id of the operation to run
|
|
85
|
+
query (str): The query to run this action on
|
|
86
|
+
user (dict[str, Any]): The user running this action
|
|
87
|
+
request_id (str, None): A user-provided ID, can be used to track the progress of their excecution via websockets
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
list[dict[str, Any]]: A report on the execution
|
|
91
|
+
"""
|
|
92
|
+
operation = None
|
|
93
|
+
try:
|
|
94
|
+
operation = importlib.import_module(f"howler.actions.{operation_id}")
|
|
95
|
+
except ImportError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
if not operation:
|
|
99
|
+
for plugin in get_plugins():
|
|
100
|
+
if not plugin.modules.operations:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
operation = next(
|
|
104
|
+
(operation for operation in plugin.modules.operations if operation.OPERATION_ID == operation_id), None
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if operation:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if not operation:
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
"query": query,
|
|
114
|
+
"outcome": "error",
|
|
115
|
+
"title": "Unknown Action",
|
|
116
|
+
"message": f"The operation ID provided ({operation_id}) does not match any enabled operations.",
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
user_roles: set[str] = set(user["type"] if user else [])
|
|
121
|
+
missing_roles = set(operation.specification()["roles"]) - user_roles
|
|
122
|
+
if missing_roles:
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
"query": query,
|
|
126
|
+
"outcome": "error",
|
|
127
|
+
"title": "Insufficient permissions",
|
|
128
|
+
"message": (
|
|
129
|
+
f"The operation ID provided ({operation_id}) requires permissions you do not have "
|
|
130
|
+
f"({', '.join(missing_roles)}). Contact HOWLER Support for more information."
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
report = operation.execute(query=query, request_id=request_id, user=user, **kwargs)
|
|
136
|
+
|
|
137
|
+
return __sanitize_report(report)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def specifications() -> list[dict[str, Any]]:
|
|
141
|
+
"""A list of specifications for the available operations
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
list[dict[str, Any]]: A list of specifications
|
|
145
|
+
"""
|
|
146
|
+
specifications = []
|
|
147
|
+
|
|
148
|
+
for module in (
|
|
149
|
+
_file
|
|
150
|
+
for _file in Path(__file__).parent.iterdir()
|
|
151
|
+
if _file.suffix == ".py" and _file.name not in ["__init__.py", "example_plugin.py"]
|
|
152
|
+
):
|
|
153
|
+
try:
|
|
154
|
+
operation = importlib.import_module(f"howler.actions.{module.stem}")
|
|
155
|
+
|
|
156
|
+
specifications.append(__sanitize_specification(operation.specification()))
|
|
157
|
+
|
|
158
|
+
except Exception: # pragma: no cover
|
|
159
|
+
logger.exception("Error when initializing %s", module)
|
|
160
|
+
|
|
161
|
+
for plugin in get_plugins():
|
|
162
|
+
if not plugin.modules.operations:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
for operation in plugin.modules.operations:
|
|
166
|
+
specifications.append(__sanitize_specification(operation.specification()))
|
|
167
|
+
|
|
168
|
+
return specifications
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from howler.common.loader import datastore
|
|
4
|
+
from howler.datastore.operations import OdmHelper
|
|
5
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
6
|
+
from howler.odm.models.hit import Hit
|
|
7
|
+
from howler.odm.models.howler_data import Label
|
|
8
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
9
|
+
|
|
10
|
+
hit_helper = OdmHelper(Hit)
|
|
11
|
+
|
|
12
|
+
OPERATION_ID = "add_label"
|
|
13
|
+
|
|
14
|
+
CATEGORIES = list(Label.fields().keys())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def execute(query: str, category: str = "generic", label: Optional[str] = None, **kwargs):
|
|
18
|
+
"""Add a label to a hit.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
query (str): The query on which to apply this automation.
|
|
22
|
+
category (str, optional): The category of label to add. Defaults to "generic".
|
|
23
|
+
label (str): The label content. Defaults to None.
|
|
24
|
+
"""
|
|
25
|
+
if category not in CATEGORIES:
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
"query": query,
|
|
29
|
+
"outcome": "error",
|
|
30
|
+
"title": "Invalid Category",
|
|
31
|
+
"message": f"'{category}' is not a valid category.",
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
if not label:
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
"query": query,
|
|
39
|
+
"outcome": "error",
|
|
40
|
+
"title": "Invalid Label",
|
|
41
|
+
"message": "Label cannot be empty.",
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
report = []
|
|
46
|
+
|
|
47
|
+
ds = datastore()
|
|
48
|
+
|
|
49
|
+
skipped_hits = ds.hit.search(
|
|
50
|
+
f"({query}) AND howler.labels.{category}:{sanitize_lucene_query((label))}",
|
|
51
|
+
fl="howler.id",
|
|
52
|
+
)["items"]
|
|
53
|
+
|
|
54
|
+
if len(skipped_hits) > 0:
|
|
55
|
+
report.append(
|
|
56
|
+
{
|
|
57
|
+
"query": f"howler.id:({' OR '.join(h.howler.id for h in skipped_hits)})",
|
|
58
|
+
"outcome": "skipped",
|
|
59
|
+
"title": "Skipped Hit with Label",
|
|
60
|
+
"message": f"These hits already have the label {label}.",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
ds.hit.update_by_query(
|
|
66
|
+
query,
|
|
67
|
+
[hit_helper.list_add(f"howler.labels.{category}", label, if_missing=True)],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
report.append(
|
|
71
|
+
{
|
|
72
|
+
"query": query,
|
|
73
|
+
"outcome": "success",
|
|
74
|
+
"title": "Executed Successfully",
|
|
75
|
+
"message": f"Label '{label}' added to category '{category}' for all matching hits.",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
report.append(
|
|
80
|
+
{
|
|
81
|
+
"query": query,
|
|
82
|
+
"outcome": "error",
|
|
83
|
+
"title": "Failed to Execute",
|
|
84
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return report
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def specification():
|
|
92
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
93
|
+
return {
|
|
94
|
+
"id": OPERATION_ID,
|
|
95
|
+
"title": "Add Label",
|
|
96
|
+
"priority": 8,
|
|
97
|
+
"i18nKey": "operations.add_label",
|
|
98
|
+
"description": {
|
|
99
|
+
"short": "Add a label to a hit",
|
|
100
|
+
"long": execute.__doc__,
|
|
101
|
+
},
|
|
102
|
+
"roles": ["automation_basic"],
|
|
103
|
+
"steps": [
|
|
104
|
+
{
|
|
105
|
+
"args": {"category": [], "label": []},
|
|
106
|
+
"options": {"category": CATEGORIES},
|
|
107
|
+
"validation": {"warn": {"query": "howler.labels.$category:$label"}},
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"triggers": VALID_TRIGGERS,
|
|
111
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from howler.common.loader import datastore
|
|
4
|
+
from howler.datastore.operations import OdmHelper
|
|
5
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
6
|
+
from howler.odm.models.hit import Hit
|
|
7
|
+
from howler.services import hit_service
|
|
8
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
9
|
+
|
|
10
|
+
hit_helper = OdmHelper(Hit)
|
|
11
|
+
|
|
12
|
+
OPERATION_ID = "add_to_bundle"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
16
|
+
"""Add a set of hits matching the query to the specified bundle.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
query (str): The query containing the matching hits
|
|
20
|
+
bundle_id (str): The `howler.id` of the bundle to add the hits to.
|
|
21
|
+
"""
|
|
22
|
+
report = []
|
|
23
|
+
|
|
24
|
+
if not bundle_id:
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
"query": query,
|
|
28
|
+
"outcome": "error",
|
|
29
|
+
"title": "Invalid Bundle ID",
|
|
30
|
+
"message": "Bundle ID cannot be empty.",
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
|
|
36
|
+
if not bundle_hit or not bundle_hit.howler.is_bundle:
|
|
37
|
+
report.append(
|
|
38
|
+
{
|
|
39
|
+
"query": query,
|
|
40
|
+
"outcome": "error",
|
|
41
|
+
"title": "Invalid Bundle",
|
|
42
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it is not a bundle.",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
return report
|
|
46
|
+
|
|
47
|
+
ds = datastore()
|
|
48
|
+
|
|
49
|
+
skipped_hits_bundles = ds.hit.search(
|
|
50
|
+
f"({query}) AND howler.is_bundle:true",
|
|
51
|
+
fl="howler.id",
|
|
52
|
+
)["items"]
|
|
53
|
+
|
|
54
|
+
if len(skipped_hits_bundles) > 0:
|
|
55
|
+
report.append(
|
|
56
|
+
{
|
|
57
|
+
"query": f"({query}) AND howler.is_bundle:true",
|
|
58
|
+
"outcome": "skipped",
|
|
59
|
+
"title": "Skipped Bundles",
|
|
60
|
+
"message": "Bundles cannot be added to a bundle.",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
skipped_hits_already_added = ds.hit.search(
|
|
65
|
+
f"({query}) AND (howler.bundles:{sanitize_lucene_query(bundle_id)})",
|
|
66
|
+
fl="howler.id",
|
|
67
|
+
)["items"]
|
|
68
|
+
|
|
69
|
+
if len(skipped_hits_already_added) > 0:
|
|
70
|
+
report.append(
|
|
71
|
+
{
|
|
72
|
+
"query": f"({query}) AND (howler.bundles:{sanitize_lucene_query(bundle_id)})",
|
|
73
|
+
"outcome": "skipped",
|
|
74
|
+
"title": "Skipped Hits",
|
|
75
|
+
"message": "These hits have already been added to the specified bundle.",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
safe_query = f"({query}) AND (-howler.bundles:({sanitize_lucene_query(bundle_id)}) AND howler.is_bundle:false)"
|
|
80
|
+
matching_hits = ds.hit.search(safe_query)["items"]
|
|
81
|
+
if len(matching_hits) < 1:
|
|
82
|
+
report.append(
|
|
83
|
+
{
|
|
84
|
+
"query": safe_query,
|
|
85
|
+
"outcome": "skipped",
|
|
86
|
+
"title": "No Matching Hits",
|
|
87
|
+
"message": "There were no hits matching this query.",
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
return report
|
|
91
|
+
|
|
92
|
+
ds.hit.update_by_query(
|
|
93
|
+
safe_query,
|
|
94
|
+
[hit_helper.list_add("howler.bundles", sanitize_lucene_query(bundle_id), if_missing=True)],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
operations = [
|
|
98
|
+
hit_helper.list_add(
|
|
99
|
+
"howler.hits",
|
|
100
|
+
hit["howler"]["id"],
|
|
101
|
+
if_missing=True,
|
|
102
|
+
)
|
|
103
|
+
for hit in matching_hits
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
operations.append(hit_helper.update("howler.bundle_size", len(operations)))
|
|
107
|
+
hit_service.update_hit(
|
|
108
|
+
bundle_id,
|
|
109
|
+
operations,
|
|
110
|
+
)
|
|
111
|
+
bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
|
|
112
|
+
report.append(
|
|
113
|
+
{
|
|
114
|
+
"query": safe_query.replace("-howler.bundles", "howler.bundles"),
|
|
115
|
+
"outcome": "success",
|
|
116
|
+
"title": "Executed Successfully",
|
|
117
|
+
"message": "The specified bundle has had all matching hits added.",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
report.append(
|
|
122
|
+
{
|
|
123
|
+
"query": query,
|
|
124
|
+
"outcome": "error",
|
|
125
|
+
"title": "Failed to Execute",
|
|
126
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return report
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def specification():
|
|
134
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
135
|
+
return {
|
|
136
|
+
"id": OPERATION_ID,
|
|
137
|
+
"title": "Add to Bundle",
|
|
138
|
+
"priority": 6,
|
|
139
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
140
|
+
"description": {
|
|
141
|
+
"short": "Add a set of hits to a bundle",
|
|
142
|
+
"long": execute.__doc__,
|
|
143
|
+
},
|
|
144
|
+
"roles": ["automation_basic"],
|
|
145
|
+
"steps": [
|
|
146
|
+
{
|
|
147
|
+
"args": {"bundle_id": []},
|
|
148
|
+
"options": {},
|
|
149
|
+
"validation": {
|
|
150
|
+
"warn": {"query": "howler.bundles:($bundle_id) OR howler.is_bundle:true"},
|
|
151
|
+
"error": {
|
|
152
|
+
"query": "howler.id:$bundle_id AND howler.is_bundle:false",
|
|
153
|
+
"message": "The bundle id given must be a bundle.",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"triggers": VALID_TRIGGERS,
|
|
159
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from howler.common.loader import datastore
|
|
2
|
+
from howler.datastore.operations import OdmHelper
|
|
3
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
4
|
+
from howler.odm.models.hit import Hit
|
|
5
|
+
|
|
6
|
+
hit_helper = OdmHelper(Hit)
|
|
7
|
+
|
|
8
|
+
OPERATION_ID = "change_field"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def execute(query: str, field: str, value: str, **kwargs):
|
|
12
|
+
"""Change one of the fields of a hit
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
query (str): The query to run this action on
|
|
16
|
+
field (str): The field to update.
|
|
17
|
+
value (str): The value to set it to. Must be a string.
|
|
18
|
+
"""
|
|
19
|
+
if field not in Hit.flat_fields():
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
"query": query,
|
|
23
|
+
"outcome": "error",
|
|
24
|
+
"title": "Invalid field",
|
|
25
|
+
"message": (f"Field '{field}' does not exist. You must pick a valid entry from the howler index."),
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
report = []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
datastore().hit.update_by_query(
|
|
33
|
+
query,
|
|
34
|
+
[hit_helper.update(field, value)],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
report.append(
|
|
38
|
+
{
|
|
39
|
+
"query": query,
|
|
40
|
+
"outcome": "success",
|
|
41
|
+
"title": "Executed Successfully",
|
|
42
|
+
"message": f"Field '{field}' updated to value '{value}' for all matching hits.",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
report.append(
|
|
47
|
+
{
|
|
48
|
+
"query": query,
|
|
49
|
+
"outcome": "error",
|
|
50
|
+
"title": "Failed to Execute",
|
|
51
|
+
"message": str(e),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return report
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def specification():
|
|
59
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
60
|
+
return {
|
|
61
|
+
"id": OPERATION_ID,
|
|
62
|
+
"title": "Change Field",
|
|
63
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
64
|
+
"description": {
|
|
65
|
+
"short": "Change one of the fields of a hit",
|
|
66
|
+
"long": execute.__doc__,
|
|
67
|
+
},
|
|
68
|
+
"roles": ["automation_advanced", "admin"],
|
|
69
|
+
"steps": [
|
|
70
|
+
{
|
|
71
|
+
"args": {"field": [], "value": []},
|
|
72
|
+
"options": {"field": list(Hit.flat_fields().keys())},
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"triggers": VALID_TRIGGERS,
|
|
76
|
+
}
|
howler/actions/demote.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import howler.helper.hit as hit_helper
|
|
4
|
+
from howler.common.loader import datastore
|
|
5
|
+
from howler.datastore.operations import OdmHelper
|
|
6
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
7
|
+
from howler.odm.models.hit import Hit
|
|
8
|
+
from howler.odm.models.howler_data import (
|
|
9
|
+
Assessment,
|
|
10
|
+
AssessmentEscalationMap,
|
|
11
|
+
Escalation,
|
|
12
|
+
HitStatus,
|
|
13
|
+
)
|
|
14
|
+
from howler.odm.models.user import User
|
|
15
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
16
|
+
|
|
17
|
+
OPERATION_ID = "demote"
|
|
18
|
+
|
|
19
|
+
ESCALATIONS = [esc for esc in Escalation.list() if esc != Escalation.EVIDENCE]
|
|
20
|
+
|
|
21
|
+
odm_helper = OdmHelper(Hit)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def execute(
|
|
25
|
+
query: str,
|
|
26
|
+
escalation: Escalation = Escalation.HIT,
|
|
27
|
+
assessment: Optional[str] = None,
|
|
28
|
+
rationale: Optional[str] = None,
|
|
29
|
+
user: Optional[User] = None,
|
|
30
|
+
**kwargs,
|
|
31
|
+
):
|
|
32
|
+
"""Demote a hit.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query (str): The query on which to apply this automation.
|
|
36
|
+
escalation (str, optional): The escalation to demote to. Defaults to "hit".
|
|
37
|
+
assessment (str, optional): The assessment to apply if demoting to miss. Required if escalation is "miss".
|
|
38
|
+
rationale (str, optional): The optional rationale to apply if demoting to miss.
|
|
39
|
+
"""
|
|
40
|
+
if escalation not in ESCALATIONS:
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
"query": query,
|
|
44
|
+
"outcome": "error",
|
|
45
|
+
"title": "Invalid Escalation",
|
|
46
|
+
"message": f"'{escalation}' is not a valid escalation.",
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
report = []
|
|
51
|
+
|
|
52
|
+
ds = datastore()
|
|
53
|
+
|
|
54
|
+
skipped_hits = ds.hit.search(
|
|
55
|
+
f"({query}) AND howler.escalation:{sanitize_lucene_query(escalation)}",
|
|
56
|
+
fl="howler.id",
|
|
57
|
+
)["items"]
|
|
58
|
+
|
|
59
|
+
if len(skipped_hits) > 0:
|
|
60
|
+
report.append(
|
|
61
|
+
{
|
|
62
|
+
"query": f"howler.id:({' OR '.join(h.howler.id for h in skipped_hits)})",
|
|
63
|
+
"outcome": "skipped",
|
|
64
|
+
"title": "Skipped Hit with Escalation",
|
|
65
|
+
"message": f"These hits already have the escalation {escalation}.",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
if escalation in [Escalation.HIT, Escalation.ALERT]:
|
|
71
|
+
ds.hit.update_by_query(
|
|
72
|
+
query,
|
|
73
|
+
[
|
|
74
|
+
*hit_helper.demote_hit(escalation=escalation),
|
|
75
|
+
odm_helper.update("howler.assessment", None),
|
|
76
|
+
odm_helper.update("howler.rationale", None),
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
if not assessment:
|
|
81
|
+
report.append(
|
|
82
|
+
{
|
|
83
|
+
"query": query,
|
|
84
|
+
"outcome": "error",
|
|
85
|
+
"title": "Missing assessment",
|
|
86
|
+
"message": "You must provide as assessment value when demoting to miss.",
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
return report
|
|
90
|
+
|
|
91
|
+
ds.hit.update_by_query(
|
|
92
|
+
query,
|
|
93
|
+
[
|
|
94
|
+
*hit_helper.assess_hit(assessment, rationale),
|
|
95
|
+
odm_helper.update(
|
|
96
|
+
"howler.assignment",
|
|
97
|
+
user.get("uname", "automation") if user else "automation",
|
|
98
|
+
),
|
|
99
|
+
odm_helper.update("howler.status", HitStatus.RESOLVED),
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
report.append(
|
|
104
|
+
{
|
|
105
|
+
"query": query,
|
|
106
|
+
"outcome": "success",
|
|
107
|
+
"title": "Executed Successfully",
|
|
108
|
+
"message": f"Demoted to '{escalation}' for all matching hits.",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
report.append(
|
|
113
|
+
{
|
|
114
|
+
"query": query,
|
|
115
|
+
"outcome": "error",
|
|
116
|
+
"title": "Failed to Execute",
|
|
117
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return report
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def specification():
|
|
125
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
126
|
+
return {
|
|
127
|
+
"id": OPERATION_ID,
|
|
128
|
+
"title": "Demote Hit",
|
|
129
|
+
"i18nKey": "operations.demote",
|
|
130
|
+
"description": {
|
|
131
|
+
"short": "Demote a hit",
|
|
132
|
+
"long": execute.__doc__,
|
|
133
|
+
},
|
|
134
|
+
"roles": ["automation_basic"],
|
|
135
|
+
"steps": [
|
|
136
|
+
{
|
|
137
|
+
"args": {"escalation": []},
|
|
138
|
+
"options": {"escalation": ESCALATIONS},
|
|
139
|
+
"validation": {"warn": {"query": "howler.escalation:$escalation"}},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"args": {
|
|
143
|
+
"assessment": ["escalation:miss"],
|
|
144
|
+
"rationale": ["escalation:miss"],
|
|
145
|
+
},
|
|
146
|
+
"options": {
|
|
147
|
+
"assessment": {
|
|
148
|
+
"escalation:miss": [
|
|
149
|
+
assessment
|
|
150
|
+
for assessment in Assessment.list()
|
|
151
|
+
if AssessmentEscalationMap[assessment] == Escalation.MISS
|
|
152
|
+
],
|
|
153
|
+
"escalation:alert": [],
|
|
154
|
+
"escalation:hit": [],
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"triggers": VALID_TRIGGERS,
|
|
160
|
+
}
|