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,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(writepath):
|
|
11
|
+
"""Run the script to generate mitre lookups"""
|
|
12
|
+
dirname = Path(writepath)
|
|
13
|
+
print(f"Generating mitre lookups to {dirname}")
|
|
14
|
+
file_name = os.path.join(os.getcwd(), "enterprise-attack.json")
|
|
15
|
+
tactics_name = dirname / "tactics.yml"
|
|
16
|
+
techniques_name = dirname / "techniques.yml"
|
|
17
|
+
|
|
18
|
+
if not os.path.exists(file_name):
|
|
19
|
+
print("Pulling mitre attack data")
|
|
20
|
+
response = requests.get(
|
|
21
|
+
"https://raw.githubusercontent.com/mitre-attack/attack-stix-data/"
|
|
22
|
+
+ "master/enterprise-attack/enterprise-attack.json",
|
|
23
|
+
timeout=10,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
response.raise_for_status()
|
|
27
|
+
|
|
28
|
+
with open(file_name, "w") as f:
|
|
29
|
+
f.write(response.text)
|
|
30
|
+
|
|
31
|
+
print("Done!")
|
|
32
|
+
|
|
33
|
+
print("Processing mitre attack data")
|
|
34
|
+
with open(file_name, "r") as f:
|
|
35
|
+
data = json.load(f)["objects"]
|
|
36
|
+
|
|
37
|
+
tactics_list = sorted(
|
|
38
|
+
[e for e in data if e["type"] == "x-mitre-tactic"],
|
|
39
|
+
key=lambda x: x["external_references"][0]["external_id"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
tactics_list = [
|
|
43
|
+
{
|
|
44
|
+
"name": e["name"],
|
|
45
|
+
"key": e["external_references"][0]["external_id"],
|
|
46
|
+
"url": e["external_references"][0]["url"],
|
|
47
|
+
# "description": e["description"].strip(),
|
|
48
|
+
}
|
|
49
|
+
for e in tactics_list
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
tactics = {}
|
|
53
|
+
for tactic in tactics_list:
|
|
54
|
+
tactics[tactic["key"]] = tactic
|
|
55
|
+
|
|
56
|
+
print(f"Writing tactics to {tactics_name}")
|
|
57
|
+
with open(tactics_name, "w") as f:
|
|
58
|
+
yaml.dump(tactics, f)
|
|
59
|
+
|
|
60
|
+
techniques_list = sorted(
|
|
61
|
+
[e for e in data if e["type"] == "attack-pattern"],
|
|
62
|
+
key=lambda x: x["external_references"][0]["external_id"],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
techniques_list = [
|
|
66
|
+
{
|
|
67
|
+
"name": e["name"],
|
|
68
|
+
"key": e["external_references"][0]["external_id"],
|
|
69
|
+
"url": e["external_references"][0]["url"],
|
|
70
|
+
# "description": e["description"].strip(),
|
|
71
|
+
}
|
|
72
|
+
for e in techniques_list
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
techniques = {technique["key"]: technique for technique in techniques_list}
|
|
76
|
+
|
|
77
|
+
print(f"Writing techniques to {techniques_name}")
|
|
78
|
+
with open(techniques_name, "w") as f:
|
|
79
|
+
yaml.dump(techniques, f)
|
|
80
|
+
|
|
81
|
+
print("Done!")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
"Main run function"
|
|
86
|
+
writepath = None
|
|
87
|
+
try:
|
|
88
|
+
writepath = sys.argv[1]
|
|
89
|
+
except Exception:
|
|
90
|
+
writepath = "lookups"
|
|
91
|
+
|
|
92
|
+
run(writepath)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tempfile import mkdtemp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
"""Run the script to generate sigma rule lookups"""
|
|
10
|
+
print("Generating sigma yaml")
|
|
11
|
+
|
|
12
|
+
git_dir = Path(mkdtemp())
|
|
13
|
+
subprocess.call(shlex.split((f"git clone git@github.com:SigmaHQ/sigma.git {git_dir} --depth 1")))
|
|
14
|
+
|
|
15
|
+
output_dir = Path(__file__).parent.parent / "odm" / "sigma"
|
|
16
|
+
if not output_dir.exists():
|
|
17
|
+
output_dir.mkdir()
|
|
18
|
+
|
|
19
|
+
print("Copying files")
|
|
20
|
+
for network_yaml in (git_dir / "rules" / "network").glob("**/*.yml"):
|
|
21
|
+
print(f" {network_yaml.relative_to(((git_dir / 'rules' / 'network')))}")
|
|
22
|
+
new_file = output_dir / network_yaml.name
|
|
23
|
+
shutil.copyfile(network_yaml, new_file)
|
|
24
|
+
|
|
25
|
+
shutil.rmtree(git_dir)
|
|
26
|
+
|
|
27
|
+
print("Done!")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_tlds(url):
|
|
7
|
+
"Get a list of top-level domains"
|
|
8
|
+
comments = []
|
|
9
|
+
tlds = []
|
|
10
|
+
|
|
11
|
+
response = requests.get(url, timeout=10)
|
|
12
|
+
for line in response.text.splitlines():
|
|
13
|
+
if not line:
|
|
14
|
+
continue
|
|
15
|
+
if line.startswith("#"):
|
|
16
|
+
comments.append(line)
|
|
17
|
+
else:
|
|
18
|
+
tlds.append(line)
|
|
19
|
+
|
|
20
|
+
return comments, tlds
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
tlds_url = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
|
|
25
|
+
tlds_location = "../howler/common/net_static.py"
|
|
26
|
+
if not os.path.exists(tlds_location):
|
|
27
|
+
print(
|
|
28
|
+
"Could not find net_static.py file. Make sure you run this script "
|
|
29
|
+
"in its home directory otherwise this won't work."
|
|
30
|
+
)
|
|
31
|
+
exit(1)
|
|
32
|
+
|
|
33
|
+
comments, tlds = get_tlds(tlds_url)
|
|
34
|
+
comments_lines = "\n".join(comments)
|
|
35
|
+
tlds_lines = '",\n "'.join(tlds)
|
|
36
|
+
|
|
37
|
+
with open(tlds_location, "w") as tlds_fh:
|
|
38
|
+
tlds_fh.write(
|
|
39
|
+
"# This file is generated using generate_tlds.py script\n"
|
|
40
|
+
"# DO NOT EDIT! Re-run the script instead...\n\n"
|
|
41
|
+
f"# Top level domains from: {tlds_url}\n"
|
|
42
|
+
f"{comments_lines}\n"
|
|
43
|
+
f'TLDS_ALPHA_BY_DOMAIN = {{\n "{tlds_lines}"\n}}\n'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
print(f"TLDS list file written into: {tlds_location}")
|
|
47
|
+
print("You can now commit the new net_static.py file to your git.")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
DELAY = 5
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
print("This script will allow you to reindex all indexes in elasticsearch.")
|
|
11
|
+
print("For obvious reasons, be EXTREMELY CAREFUL running this code.")
|
|
12
|
+
print()
|
|
13
|
+
answer = input("Are you sure you want to reindex data for an index in this cluster? [yes/NO]\n")
|
|
14
|
+
|
|
15
|
+
if not answer.startswith("y"):
|
|
16
|
+
print("Confirmation not provided, stopping.")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
from howler.datastore.collection import ESCollection
|
|
20
|
+
|
|
21
|
+
ESCollection.IGNORE_ENSURE_COLLECTION = True
|
|
22
|
+
|
|
23
|
+
from howler.common import loader
|
|
24
|
+
|
|
25
|
+
ds = loader.datastore(archive_access=False)
|
|
26
|
+
|
|
27
|
+
indexes: dict[str, tuple[ESCollection, Callable]] = {
|
|
28
|
+
"analytic": (ds.analytic, ds.analytic.reindex),
|
|
29
|
+
"hit": (ds.hit, ds.hit.reindex),
|
|
30
|
+
"view": (ds.view, ds.view.reindex),
|
|
31
|
+
"template": (ds.template, ds.template.reindex),
|
|
32
|
+
"overview": (ds.overview, ds.overview.reindex),
|
|
33
|
+
"action": (ds.action, ds.action.reindex),
|
|
34
|
+
"user": (ds.user, ds.user.reindex),
|
|
35
|
+
"dossier": (ds.dossier, ds.dossier.reindex),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
print("Which index will you reindex?")
|
|
39
|
+
index_answer = input(", ".join(indexes.keys()) + "\n> ")
|
|
40
|
+
|
|
41
|
+
if index_answer not in indexes:
|
|
42
|
+
print("Invalid index provided, stopping.")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
print("Index schema:")
|
|
46
|
+
print(json.dumps(indexes[index_answer][0]._get_index_mappings(), indent=2))
|
|
47
|
+
|
|
48
|
+
print("\nYou will be reindexing the following indexes:")
|
|
49
|
+
print("\n".join(indexes[index_answer][0].index_list_full))
|
|
50
|
+
|
|
51
|
+
answer = input(("\nAre you sure you want to reindex these indexes? [yes/NO]\n"))
|
|
52
|
+
print()
|
|
53
|
+
|
|
54
|
+
if not answer.startswith("y"):
|
|
55
|
+
print("Confirmation not provided, stopping.")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
for i in range(2 * DELAY):
|
|
59
|
+
print(f"Reindexing in {2 * DELAY - i}...", end="\r")
|
|
60
|
+
time.sleep(1)
|
|
61
|
+
|
|
62
|
+
print()
|
|
63
|
+
|
|
64
|
+
result = indexes[index_answer][1]()
|
|
65
|
+
|
|
66
|
+
print(f"Reindex complete. Success: {result}.")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
DELAY = 5
|
|
5
|
+
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
print("This script will allow you to completely remove all howler data and indexes from elasticsearch.")
|
|
8
|
+
print("For obvious reasons, be EXTREMELY CAREFUL running this code.")
|
|
9
|
+
|
|
10
|
+
for i in range(DELAY):
|
|
11
|
+
print(f"Continuing in {str(DELAY - i)}...", end="\r")
|
|
12
|
+
time.sleep(1)
|
|
13
|
+
print()
|
|
14
|
+
index = input("\nWhat index do you want to wipe? (Supported options are [hit, user]):\n")
|
|
15
|
+
|
|
16
|
+
if index not in ["hit", "user"]:
|
|
17
|
+
print("Invalid index.")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
answer = input(f"\nSelected index is {index}. Are you sure you want to wipe all data in this index? [yes/NO]\n")
|
|
21
|
+
|
|
22
|
+
if not answer.startswith("y"):
|
|
23
|
+
print("Confirmation not provided, stopping.")
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
answer = input(
|
|
27
|
+
(
|
|
28
|
+
"\nSeriously, this will completely wipe the index. It'll go poof.\n"
|
|
29
|
+
"Are you sure you want to wipe all data in this index? [yes/NO]\n"
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
print()
|
|
33
|
+
|
|
34
|
+
if not answer.startswith("y"):
|
|
35
|
+
print("Confirmation not provided, stopping.")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
for i in range(2 * DELAY):
|
|
39
|
+
print(f"Deleting {index} in {2 * DELAY - i}...", end="\r")
|
|
40
|
+
time.sleep(1)
|
|
41
|
+
|
|
42
|
+
print()
|
|
43
|
+
|
|
44
|
+
from howler.datastore.collection import ESCollection
|
|
45
|
+
|
|
46
|
+
ESCollection.IGNORE_ENSURE_COLLECTION = True
|
|
47
|
+
|
|
48
|
+
from howler.common import loader
|
|
49
|
+
from howler.odm.random_data import wipe_hits, wipe_users
|
|
50
|
+
|
|
51
|
+
ds = loader.datastore(archive_access=False)
|
|
52
|
+
|
|
53
|
+
if index == "user":
|
|
54
|
+
wipe_users(ds)
|
|
55
|
+
else:
|
|
56
|
+
wipe_hits(ds)
|
|
57
|
+
|
|
58
|
+
print(f"Wiped {index}.")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import multiprocessing
|
|
2
|
+
from os import environ as env
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
# Port to bind to
|
|
9
|
+
bind = f":{int(env.get('PORT', 5000))}"
|
|
10
|
+
|
|
11
|
+
# Number of processes to launch
|
|
12
|
+
workers = int(env.get("WORKERS", multiprocessing.cpu_count()))
|
|
13
|
+
|
|
14
|
+
# Number of concurrent handled connections
|
|
15
|
+
threads = int(env.get("THREADS", 4))
|
|
16
|
+
worker_connections = int(env.get("WORKER_CONNECTIONS", "1000"))
|
|
17
|
+
|
|
18
|
+
# Recycle the process after X request randomized by the jitter
|
|
19
|
+
max_requests = int(env.get("MAX_REQUESTS", "1000"))
|
|
20
|
+
max_requests_jitter = int(env.get("MAX_REQUESTS_JITTER", "100"))
|
|
21
|
+
|
|
22
|
+
# Connection timeouts
|
|
23
|
+
graceful_timeout = int(env.get("GRACEFUL_TIMEOUT", "30"))
|
|
24
|
+
# Official microsoft documentation suggest 600
|
|
25
|
+
timeout = int(env.get("TIMEOUT", "360"))
|
howler/healthz.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from flask import Blueprint, abort, make_response
|
|
2
|
+
|
|
3
|
+
from howler.common.loader import datastore
|
|
4
|
+
|
|
5
|
+
API_PREFIX = "/api/healthz"
|
|
6
|
+
healthz = Blueprint("healthz", __name__, url_prefix=API_PREFIX)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@healthz.route("/live")
|
|
10
|
+
def liveness(**_):
|
|
11
|
+
"""Check if the API is live
|
|
12
|
+
|
|
13
|
+
Variables:
|
|
14
|
+
None
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
None
|
|
18
|
+
|
|
19
|
+
Result Example:
|
|
20
|
+
OK or FAIL
|
|
21
|
+
"""
|
|
22
|
+
return make_response("OK")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@healthz.route("/ready")
|
|
26
|
+
def readyness(**_):
|
|
27
|
+
"""Check if the API is Ready
|
|
28
|
+
|
|
29
|
+
Variables:
|
|
30
|
+
None
|
|
31
|
+
|
|
32
|
+
Arguments:
|
|
33
|
+
None
|
|
34
|
+
|
|
35
|
+
Result Example:
|
|
36
|
+
OK or FAIL
|
|
37
|
+
"""
|
|
38
|
+
if datastore().ds.ping():
|
|
39
|
+
return make_response("OK")
|
|
40
|
+
else:
|
|
41
|
+
abort(503)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@healthz.errorhandler(503)
|
|
45
|
+
def error(_):
|
|
46
|
+
"Handle errors exposed in healthz routes"
|
|
47
|
+
return "FAIL", 503
|
|
File without changes
|
howler/helper/azure.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from howler.common.exceptions import HowlerException
|
|
4
|
+
from howler.common.logging import get_logger
|
|
5
|
+
from howler.config import config
|
|
6
|
+
from howler.utils.str_utils import default_string_value
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__file__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def azure_obo(token: str) -> str:
|
|
12
|
+
"""OBO an azure access token to MS Graph
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
token (str): The azure access token
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
HowlerException: OBO failed
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: The new access token with updated privileges
|
|
22
|
+
"""
|
|
23
|
+
azure_provider_config = config.auth.oauth.providers["azure"]
|
|
24
|
+
|
|
25
|
+
logger.debug("OBOing to MS Graph")
|
|
26
|
+
# Azure is a special case here, as we need to OBO to MS Graph
|
|
27
|
+
data = {
|
|
28
|
+
"client_id": default_string_value(
|
|
29
|
+
azure_provider_config.client_id,
|
|
30
|
+
env_name="AZURE_CLIENT_ID",
|
|
31
|
+
),
|
|
32
|
+
"scope": "https://graph.microsoft.com/user.read",
|
|
33
|
+
"assertion": token,
|
|
34
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
35
|
+
"client_secret": default_string_value(
|
|
36
|
+
azure_provider_config.client_secret,
|
|
37
|
+
env_name="AZURE_CLIENT_SECRET",
|
|
38
|
+
),
|
|
39
|
+
"requested_token_use": "on_behalf_of",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if not azure_provider_config.access_token_url:
|
|
43
|
+
raise HowlerException("Azure OBO failed - access_token_url must be set!")
|
|
44
|
+
|
|
45
|
+
resp = requests.post(azure_provider_config.access_token_url, data=data, timeout=10)
|
|
46
|
+
|
|
47
|
+
if not resp.ok:
|
|
48
|
+
raise HowlerException(f"Azure OBO failed. Reason: {resp.content!r}")
|
|
49
|
+
|
|
50
|
+
return resp.json()["access_token"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typing
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from howler.common.logging import get_logger
|
|
8
|
+
from howler.config import config
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__file__)
|
|
11
|
+
DISCO_CACHE = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_apps_list(discovery_url: Optional[str]) -> list[dict[str, str]]:
|
|
15
|
+
"""Get a list of apps from the discovery service
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
list[dict[str, str]]: A list of other apps
|
|
19
|
+
"""
|
|
20
|
+
if discovery_url not in DISCO_CACHE:
|
|
21
|
+
apps = []
|
|
22
|
+
|
|
23
|
+
if "pytest" in sys.modules:
|
|
24
|
+
logger.info("Skipping discovery, running in a test environment")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
resp = requests.get(
|
|
28
|
+
typing.cast(str, discovery_url or config.ui.discover_url),
|
|
29
|
+
headers={"accept": "application/json"},
|
|
30
|
+
timeout=5,
|
|
31
|
+
)
|
|
32
|
+
if resp.ok:
|
|
33
|
+
data = resp.json()
|
|
34
|
+
for app in data["applications"]["application"]:
|
|
35
|
+
try:
|
|
36
|
+
url = app["instance"][0]["hostName"]
|
|
37
|
+
if "howler" not in url:
|
|
38
|
+
apps.append(
|
|
39
|
+
{
|
|
40
|
+
"alt": app["instance"][0]["metadata"]["alternateText"],
|
|
41
|
+
"name": app["name"],
|
|
42
|
+
"img_d": app["instance"][0]["metadata"]["imageDark"],
|
|
43
|
+
"img_l": app["instance"][0]["metadata"]["imageLight"],
|
|
44
|
+
"route": url,
|
|
45
|
+
"classification": app["instance"][0]["metadata"]["classification"],
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
except Exception:
|
|
50
|
+
logger.exception(f"Failed to parse get app: {str(app)}")
|
|
51
|
+
else:
|
|
52
|
+
logger.warning(f"Invalid response from server for apps discovery: {discovery_url}")
|
|
53
|
+
except Exception:
|
|
54
|
+
logger.exception(f"Failed to get apps from discover URL: {discovery_url}")
|
|
55
|
+
|
|
56
|
+
DISCO_CACHE[discovery_url] = sorted(apps, key=lambda k: ["name"])
|
|
57
|
+
return sorted(apps, key=lambda k: k["name"])
|
|
58
|
+
else:
|
|
59
|
+
return DISCO_CACHE[discovery_url]
|