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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
2
|
+
|
|
3
|
+
OPERATION_ID = "example_plugin"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def execute(query: str, arg1: str, arg2: str, **kwargs):
|
|
7
|
+
"""This function is called either when triggered manually or any of the accepted triggers are met.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
query (str): The query this action is running on
|
|
11
|
+
arg1 (str, optional): The provided value for the matching argument below. One of "a", "b", "c".
|
|
12
|
+
arg2 (str, optional): The provided value for the matching argument below. Freeform text.
|
|
13
|
+
"""
|
|
14
|
+
report = []
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
# Do whatever you want here - make requests, parse them, etc.
|
|
18
|
+
|
|
19
|
+
# There are three types of responses you can add to the report - success, skipped and error.
|
|
20
|
+
if arg1 in ["a", "c"]:
|
|
21
|
+
report.append(
|
|
22
|
+
{
|
|
23
|
+
"query": query,
|
|
24
|
+
"outcome": "success",
|
|
25
|
+
"title": "Executed Successfully",
|
|
26
|
+
"message": "Example action ran successfully!",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
report.append(
|
|
31
|
+
{
|
|
32
|
+
"query": query,
|
|
33
|
+
"outcome": "skipped",
|
|
34
|
+
"title": "Execution Skipped",
|
|
35
|
+
"message": "Since arg1 was b, we didn't run the action.",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
report.append(
|
|
40
|
+
{
|
|
41
|
+
"query": query,
|
|
42
|
+
"outcome": "error",
|
|
43
|
+
"title": "Failed to Execute",
|
|
44
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return report
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def specification():
|
|
52
|
+
"""A function that returns information about how to present the action in the UI.
|
|
53
|
+
|
|
54
|
+
This information includes what information must be provided, and how the UI should ask for it, as well as basic
|
|
55
|
+
data validation.
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"id": OPERATION_ID,
|
|
59
|
+
# The string to use if no localization is available
|
|
60
|
+
"title": "Example Plugin",
|
|
61
|
+
# If you have a localization key in the UI, set it here.
|
|
62
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
63
|
+
# Provide a short and long description. These must be useful - they'll be presented to users in the UI.
|
|
64
|
+
"description": {
|
|
65
|
+
"short": "Just an example plugin implementation",
|
|
66
|
+
"long": execute.__doc__,
|
|
67
|
+
},
|
|
68
|
+
# What roles should be necessary to run this action? In general, automation_basic should always be required,
|
|
69
|
+
# while automation_advanced should be set when this action could be dangerous or costly in terms of resources.
|
|
70
|
+
"roles": ["automation_basic"],
|
|
71
|
+
# What data should the user be required to provide? This is split intop steps, so arguments can depend on each
|
|
72
|
+
# other, giving basic control flow for specifying arguments.
|
|
73
|
+
"steps": [
|
|
74
|
+
{
|
|
75
|
+
# A list of argument values the user must provide in this step
|
|
76
|
+
"args": {"arg1": []},
|
|
77
|
+
# Specifying a matching key corresponding to a list of strings in options will allow users to choose
|
|
78
|
+
# from a pre-defined list of values, while not providing it will allow them to enter any freeform text.
|
|
79
|
+
"options": {"arg1": ["a", "b", "c"]},
|
|
80
|
+
# You can specify "warn" and "error" validation queries. The UI will pop up a warning or error if this
|
|
81
|
+
# query returns a match when ANDed with the supplied query. i.e. say the query they are running the
|
|
82
|
+
# action on is "howler.id:*" and they have chosen "a" for arg1, then
|
|
83
|
+
# if the query "howler.id:* AND howler.labels.generic:*a*" has any matches, this will warn the user.
|
|
84
|
+
"validation": {
|
|
85
|
+
"warn": {
|
|
86
|
+
# The query to match against. Basic replacement works, i.e. $<var_name> =>
|
|
87
|
+
# replaced with the value for var_name.
|
|
88
|
+
"query": "howler.labels.generic:*$arg1*",
|
|
89
|
+
# What message should be shown? This is optional - a generic message will be shown otherwise
|
|
90
|
+
"message": "You can't have a label that contains the character chosen as arg1!",
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
# This means that the UI for adding an input for arg2 will only be required if arg1 is "a" or "b",
|
|
96
|
+
# not "c".
|
|
97
|
+
"args": {"arg2": ["arg1:a", "arg1:b"]},
|
|
98
|
+
# No options provided => freeform text
|
|
99
|
+
"options": {},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
# What triggers should one be able to use this action with? In this case, we allow all of them.
|
|
103
|
+
"triggers": VALID_TRIGGERS,
|
|
104
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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 = "prioritization"
|
|
9
|
+
|
|
10
|
+
VALID_FIELDS = ["reliability", "severity", "volume", "confidence", "score"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute(query: str, field: str = "score", value: str = "0.0", **kwargs):
|
|
14
|
+
"""Change one of the priorization fields of a hit
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
query (str): The query to run this action on
|
|
18
|
+
field (str, optional): The field to update. Defaults to "score".
|
|
19
|
+
value (str, optional): The value to set it to. Must be a float in string format. Defaults to "0.0".
|
|
20
|
+
"""
|
|
21
|
+
if field not in VALID_FIELDS:
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
"query": query,
|
|
25
|
+
"outcome": "error",
|
|
26
|
+
"title": "Invalid field",
|
|
27
|
+
"message": (
|
|
28
|
+
f"Field 'howler.{field}' does not exist. You must pick from one of the following "
|
|
29
|
+
+ f"values: {', '.join(VALID_FIELDS)}."
|
|
30
|
+
),
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
report = []
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
value: float = float(value)
|
|
38
|
+
|
|
39
|
+
datastore().hit.update_by_query(
|
|
40
|
+
query,
|
|
41
|
+
[hit_helper.update(f"howler.{field}", value)],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
report.append(
|
|
45
|
+
{
|
|
46
|
+
"query": query,
|
|
47
|
+
"outcome": "success",
|
|
48
|
+
"title": "Executed Successfully",
|
|
49
|
+
"message": f"Field 'howler.{field}' updated to value '{value}' for all matching hits.",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
except ValueError:
|
|
53
|
+
report.append(
|
|
54
|
+
{
|
|
55
|
+
"query": query,
|
|
56
|
+
"outcome": "error",
|
|
57
|
+
"title": "Invalid Value",
|
|
58
|
+
"message": f"'{value}' is not a valid value. It must be a float (i.e. 12.34)",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
report.append(
|
|
63
|
+
{
|
|
64
|
+
"query": query,
|
|
65
|
+
"outcome": "error",
|
|
66
|
+
"title": "Failed to Execute",
|
|
67
|
+
"message": str(e),
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return report
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def specification():
|
|
75
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
76
|
+
return {
|
|
77
|
+
"id": OPERATION_ID,
|
|
78
|
+
"title": "Change Prioritization",
|
|
79
|
+
"priority": 10,
|
|
80
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
81
|
+
"description": {
|
|
82
|
+
"short": "Change one of the prioritization fields of a hit",
|
|
83
|
+
"long": execute.__doc__,
|
|
84
|
+
},
|
|
85
|
+
"roles": ["automation_basic"],
|
|
86
|
+
"steps": [
|
|
87
|
+
{
|
|
88
|
+
"args": {"field": [], "value": []},
|
|
89
|
+
"options": {"field": VALID_FIELDS},
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"triggers": VALID_TRIGGERS,
|
|
93
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
)
|
|
13
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
14
|
+
|
|
15
|
+
OPERATION_ID = "promote"
|
|
16
|
+
|
|
17
|
+
ESCALATIONS = [esc for esc in Escalation.list() if esc != Escalation.MISS]
|
|
18
|
+
|
|
19
|
+
odm_helper = OdmHelper(Hit)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def execute(
|
|
23
|
+
query: str,
|
|
24
|
+
escalation: Escalation = Escalation.ALERT,
|
|
25
|
+
assessment: Optional[str] = None,
|
|
26
|
+
rationale: Optional[str] = None,
|
|
27
|
+
**kwargs,
|
|
28
|
+
):
|
|
29
|
+
"""Promote a hit.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
query (str): The query on which to apply this automation.
|
|
33
|
+
escalation (str, optional): The escalation to promote to. Defaults to "alert".
|
|
34
|
+
assessment (str, optional): Required if escalation is evidence, assessment to apply.
|
|
35
|
+
rationale (str, optional): The optional rationale to apply if promoting to evidence.
|
|
36
|
+
"""
|
|
37
|
+
if escalation not in ESCALATIONS:
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
"query": query,
|
|
41
|
+
"outcome": "error",
|
|
42
|
+
"title": "Invalid Escalation",
|
|
43
|
+
"message": f"'{escalation}' is not a valid escalation.",
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
report = []
|
|
48
|
+
|
|
49
|
+
ds = datastore()
|
|
50
|
+
|
|
51
|
+
skipped_hits = ds.hit.search(
|
|
52
|
+
f"({query}) AND howler.escalation:{sanitize_lucene_query(escalation)}",
|
|
53
|
+
fl="howler.id",
|
|
54
|
+
)["items"]
|
|
55
|
+
|
|
56
|
+
if len(skipped_hits) > 0:
|
|
57
|
+
report.append(
|
|
58
|
+
{
|
|
59
|
+
"query": f"howler.id:({' OR '.join(h.howler.id for h in skipped_hits)})",
|
|
60
|
+
"outcome": "skipped",
|
|
61
|
+
"title": "Skipped Hit with Escalation",
|
|
62
|
+
"message": f"These hits already have the escalation {escalation}.",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if escalation in [Escalation.HIT, Escalation.ALERT]:
|
|
68
|
+
ds.hit.update_by_query(
|
|
69
|
+
query,
|
|
70
|
+
[
|
|
71
|
+
*hit_helper.promote_hit(escalation=escalation),
|
|
72
|
+
odm_helper.update("howler.assessment", None),
|
|
73
|
+
odm_helper.update("howler.rationale", None),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
if not assessment:
|
|
78
|
+
report.append(
|
|
79
|
+
{
|
|
80
|
+
"query": query,
|
|
81
|
+
"outcome": "error",
|
|
82
|
+
"title": "Missing assessment",
|
|
83
|
+
"message": "You must provide as assessment value when promoting to evidence.",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return report
|
|
87
|
+
|
|
88
|
+
ds.hit.update_by_query(query, hit_helper.assess_hit(assessment, rationale))
|
|
89
|
+
|
|
90
|
+
report.append(
|
|
91
|
+
{
|
|
92
|
+
"query": query,
|
|
93
|
+
"outcome": "success",
|
|
94
|
+
"title": "Executed Successfully",
|
|
95
|
+
"message": f"Promoted to '{escalation}' for all matching hits.",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
report.append(
|
|
100
|
+
{
|
|
101
|
+
"query": query,
|
|
102
|
+
"outcome": "error",
|
|
103
|
+
"title": "Failed to Execute",
|
|
104
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return report
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def specification():
|
|
112
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
113
|
+
return {
|
|
114
|
+
"id": OPERATION_ID,
|
|
115
|
+
"title": "Promote Hit",
|
|
116
|
+
"i18nKey": "operations.promote",
|
|
117
|
+
"description": {
|
|
118
|
+
"short": "Promote a hit",
|
|
119
|
+
"long": execute.__doc__,
|
|
120
|
+
},
|
|
121
|
+
"roles": ["automation_basic"],
|
|
122
|
+
"steps": [
|
|
123
|
+
{
|
|
124
|
+
"args": {"escalation": []},
|
|
125
|
+
"options": {"escalation": ESCALATIONS},
|
|
126
|
+
"validation": {"warn": {"query": "howler.escalation:$escalation"}},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"args": {
|
|
130
|
+
"assessment": ["escalation:evidence"],
|
|
131
|
+
"rationale": ["escalation:evidence"],
|
|
132
|
+
},
|
|
133
|
+
"options": {
|
|
134
|
+
"assessment": {
|
|
135
|
+
"escalation:evidence": [
|
|
136
|
+
assessment
|
|
137
|
+
for assessment in Assessment.list()
|
|
138
|
+
if AssessmentEscalationMap[assessment] == Escalation.EVIDENCE
|
|
139
|
+
],
|
|
140
|
+
"escalation:alert": [],
|
|
141
|
+
"escalation:hit": [],
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
"triggers": VALID_TRIGGERS,
|
|
147
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from howler.common.exceptions import HowlerException
|
|
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.services import hit_service
|
|
9
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
10
|
+
|
|
11
|
+
hit_helper = OdmHelper(Hit)
|
|
12
|
+
|
|
13
|
+
OPERATION_ID = "remove_from_bundle"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
17
|
+
"""Remove a set of hits matching the query from the specified bundle.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
query (str): The query containing the matching hits
|
|
21
|
+
bundle_id (str): The `howler.id` of the bundle to remove the hits from.
|
|
22
|
+
"""
|
|
23
|
+
report = []
|
|
24
|
+
|
|
25
|
+
if not bundle_id:
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
"query": query,
|
|
29
|
+
"outcome": "error",
|
|
30
|
+
"title": "Invalid Bundle ID",
|
|
31
|
+
"message": "Bundle ID cannot be empty.",
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
|
|
37
|
+
if not bundle_hit or not bundle_hit.howler.is_bundle:
|
|
38
|
+
report.append(
|
|
39
|
+
{
|
|
40
|
+
"query": query,
|
|
41
|
+
"outcome": "error",
|
|
42
|
+
"title": "Invalid Bundle",
|
|
43
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it is not a bundle.",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
return report
|
|
47
|
+
|
|
48
|
+
ds = datastore()
|
|
49
|
+
|
|
50
|
+
skipped_hits = ds.hit.search(
|
|
51
|
+
f"({query}) AND -howler.bundles:{sanitize_lucene_query(bundle_id)}",
|
|
52
|
+
fl="howler.id",
|
|
53
|
+
)["items"]
|
|
54
|
+
|
|
55
|
+
if len(skipped_hits) > 0:
|
|
56
|
+
report.append(
|
|
57
|
+
{
|
|
58
|
+
"query": f"howler.id:({' OR '.join(h.howler.id for h in skipped_hits)})",
|
|
59
|
+
"outcome": "skipped",
|
|
60
|
+
"title": "Skipped Hit not in Bundle",
|
|
61
|
+
"message": "These hits already are not in the bundle.",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
safe_query = f"{query} AND (howler.bundles:{bundle_id})"
|
|
66
|
+
|
|
67
|
+
matching_hits = ds.hit.search(safe_query)["items"]
|
|
68
|
+
if len(matching_hits) < 1:
|
|
69
|
+
report.append(
|
|
70
|
+
{
|
|
71
|
+
"query": safe_query,
|
|
72
|
+
"outcome": "skipped",
|
|
73
|
+
"title": "No Matching Hits",
|
|
74
|
+
"message": "There were no hits matching this query.",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return report
|
|
78
|
+
|
|
79
|
+
ds.hit.update_by_query(
|
|
80
|
+
safe_query,
|
|
81
|
+
[hit_helper.list_remove("howler.bundles", bundle_id)],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
hit_service.update_hit(
|
|
85
|
+
bundle_id,
|
|
86
|
+
[hit_helper.list_remove("howler.hits", h["howler"]["id"]) for h in ds.hit.search(safe_query)["items"]],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if len(ds.hit.get(bundle_id).howler.hits) < 1:
|
|
90
|
+
hit_service.update_hit(bundle_id, [hit_helper.update("howler.is_bundle", False)])
|
|
91
|
+
|
|
92
|
+
report.append(
|
|
93
|
+
{
|
|
94
|
+
"query": query,
|
|
95
|
+
"outcome": "success",
|
|
96
|
+
"title": "Executed Successfully",
|
|
97
|
+
"message": f"Matching hits removed from bundle with id {bundle_id}",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
except HowlerException as e:
|
|
101
|
+
report.append(
|
|
102
|
+
{
|
|
103
|
+
"query": query,
|
|
104
|
+
"outcome": "error",
|
|
105
|
+
"title": "Failed to Execute",
|
|
106
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return report
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def specification():
|
|
114
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
115
|
+
return {
|
|
116
|
+
"id": OPERATION_ID,
|
|
117
|
+
"title": "Remove from Bundle",
|
|
118
|
+
"priority": 5,
|
|
119
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
120
|
+
"description": {
|
|
121
|
+
"short": "Remove a set of hits from a bundle",
|
|
122
|
+
"long": execute.__doc__,
|
|
123
|
+
},
|
|
124
|
+
"roles": ["automation_basic"],
|
|
125
|
+
"steps": [
|
|
126
|
+
{
|
|
127
|
+
"args": {"bundle_id": []},
|
|
128
|
+
"options": {},
|
|
129
|
+
"validation": {"error": {"query": "-howler.bundles:$bundle_id"}},
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"triggers": VALID_TRIGGERS,
|
|
133
|
+
}
|
|
@@ -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 = "remove_label"
|
|
13
|
+
|
|
14
|
+
CATEGORIES = list(Label.fields().keys())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def execute(query: str, category: str = "generic", label: Optional[str] = None, **kwargs):
|
|
18
|
+
"""Remove a label from a hit.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
query (str): The query on which to apply this automation.
|
|
22
|
+
category (str, optional): The category of label from which to remove the label. Defaults to "generic".
|
|
23
|
+
label (str, optional): The label to remove. 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 without Label",
|
|
60
|
+
"message": f"These hits already do not have the label {label}.",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
ds.hit.update_by_query(
|
|
66
|
+
query,
|
|
67
|
+
[hit_helper.list_remove(f"howler.labels.{category}", label)],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
report.append(
|
|
71
|
+
{
|
|
72
|
+
"query": query,
|
|
73
|
+
"outcome": "success",
|
|
74
|
+
"title": "Executed Successfully",
|
|
75
|
+
"message": f"Label '{label}' removed from 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": "Remove Label",
|
|
96
|
+
"priority": 7,
|
|
97
|
+
"i18nKey": "operations.remove_label",
|
|
98
|
+
"description": {
|
|
99
|
+
"short": "Remove a label from 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
|
+
}
|