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.

Files changed (200) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +167 -0
  3. howler/actions/add_label.py +111 -0
  4. howler/actions/add_to_bundle.py +159 -0
  5. howler/actions/change_field.py +76 -0
  6. howler/actions/demote.py +160 -0
  7. howler/actions/example_plugin.py +104 -0
  8. howler/actions/prioritization.py +93 -0
  9. howler/actions/promote.py +147 -0
  10. howler/actions/remove_from_bundle.py +133 -0
  11. howler/actions/remove_label.py +111 -0
  12. howler/actions/transition.py +200 -0
  13. howler/api/__init__.py +249 -0
  14. howler/api/base.py +88 -0
  15. howler/api/socket.py +114 -0
  16. howler/api/v1/__init__.py +97 -0
  17. howler/api/v1/action.py +372 -0
  18. howler/api/v1/analytic.py +748 -0
  19. howler/api/v1/auth.py +382 -0
  20. howler/api/v1/borealis.py +101 -0
  21. howler/api/v1/configs.py +55 -0
  22. howler/api/v1/dossier.py +222 -0
  23. howler/api/v1/help.py +28 -0
  24. howler/api/v1/hit.py +1181 -0
  25. howler/api/v1/notebook.py +82 -0
  26. howler/api/v1/overview.py +191 -0
  27. howler/api/v1/search.py +715 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +414 -0
  31. howler/api/v1/utils/__init__.py +0 -0
  32. howler/api/v1/utils/etag.py +84 -0
  33. howler/api/v1/view.py +288 -0
  34. howler/app.py +235 -0
  35. howler/common/README.md +144 -0
  36. howler/common/__init__.py +0 -0
  37. howler/common/classification.py +979 -0
  38. howler/common/classification.yml +107 -0
  39. howler/common/exceptions.py +167 -0
  40. howler/common/hexdump.py +48 -0
  41. howler/common/iprange.py +171 -0
  42. howler/common/loader.py +154 -0
  43. howler/common/logging/__init__.py +241 -0
  44. howler/common/logging/audit.py +138 -0
  45. howler/common/logging/format.py +38 -0
  46. howler/common/net.py +79 -0
  47. howler/common/net_static.py +1494 -0
  48. howler/common/random_user.py +316 -0
  49. howler/common/swagger.py +117 -0
  50. howler/config.py +64 -0
  51. howler/cronjobs/__init__.py +29 -0
  52. howler/cronjobs/retention.py +61 -0
  53. howler/cronjobs/rules.py +274 -0
  54. howler/cronjobs/view_cleanup.py +88 -0
  55. howler/datastore/README.md +112 -0
  56. howler/datastore/__init__.py +0 -0
  57. howler/datastore/bulk.py +72 -0
  58. howler/datastore/collection.py +2327 -0
  59. howler/datastore/constants.py +117 -0
  60. howler/datastore/exceptions.py +41 -0
  61. howler/datastore/howler_store.py +105 -0
  62. howler/datastore/migrations/fix_process.py +41 -0
  63. howler/datastore/operations.py +130 -0
  64. howler/datastore/schemas.py +90 -0
  65. howler/datastore/store.py +231 -0
  66. howler/datastore/support/__init__.py +0 -0
  67. howler/datastore/support/build.py +214 -0
  68. howler/datastore/support/schemas.py +90 -0
  69. howler/datastore/types.py +22 -0
  70. howler/error.py +91 -0
  71. howler/external/__init__.py +0 -0
  72. howler/external/generate_mitre.py +96 -0
  73. howler/external/generate_sigma_rules.py +31 -0
  74. howler/external/generate_tlds.py +47 -0
  75. howler/external/reindex_data.py +46 -0
  76. howler/external/wipe_databases.py +58 -0
  77. howler/gunicorn_config.py +25 -0
  78. howler/healthz.py +47 -0
  79. howler/helper/__init__.py +0 -0
  80. howler/helper/azure.py +50 -0
  81. howler/helper/discover.py +59 -0
  82. howler/helper/hit.py +236 -0
  83. howler/helper/oauth.py +247 -0
  84. howler/helper/search.py +92 -0
  85. howler/helper/workflow.py +110 -0
  86. howler/helper/ws.py +378 -0
  87. howler/odm/README.md +102 -0
  88. howler/odm/__init__.py +1 -0
  89. howler/odm/base.py +1504 -0
  90. howler/odm/charter.txt +146 -0
  91. howler/odm/helper.py +416 -0
  92. howler/odm/howler_enum.py +25 -0
  93. howler/odm/models/__init__.py +0 -0
  94. howler/odm/models/action.py +33 -0
  95. howler/odm/models/analytic.py +90 -0
  96. howler/odm/models/assemblyline.py +48 -0
  97. howler/odm/models/aws.py +23 -0
  98. howler/odm/models/azure.py +16 -0
  99. howler/odm/models/cbs.py +44 -0
  100. howler/odm/models/config.py +558 -0
  101. howler/odm/models/dossier.py +33 -0
  102. howler/odm/models/ecs/__init__.py +0 -0
  103. howler/odm/models/ecs/agent.py +17 -0
  104. howler/odm/models/ecs/autonomous_system.py +16 -0
  105. howler/odm/models/ecs/client.py +149 -0
  106. howler/odm/models/ecs/cloud.py +141 -0
  107. howler/odm/models/ecs/code_signature.py +27 -0
  108. howler/odm/models/ecs/container.py +32 -0
  109. howler/odm/models/ecs/dns.py +62 -0
  110. howler/odm/models/ecs/egress.py +10 -0
  111. howler/odm/models/ecs/elf.py +74 -0
  112. howler/odm/models/ecs/email.py +122 -0
  113. howler/odm/models/ecs/error.py +14 -0
  114. howler/odm/models/ecs/event.py +140 -0
  115. howler/odm/models/ecs/faas.py +24 -0
  116. howler/odm/models/ecs/file.py +84 -0
  117. howler/odm/models/ecs/geo.py +30 -0
  118. howler/odm/models/ecs/group.py +18 -0
  119. howler/odm/models/ecs/hash.py +16 -0
  120. howler/odm/models/ecs/host.py +17 -0
  121. howler/odm/models/ecs/http.py +37 -0
  122. howler/odm/models/ecs/ingress.py +12 -0
  123. howler/odm/models/ecs/interface.py +21 -0
  124. howler/odm/models/ecs/network.py +30 -0
  125. howler/odm/models/ecs/observer.py +45 -0
  126. howler/odm/models/ecs/organization.py +12 -0
  127. howler/odm/models/ecs/os.py +21 -0
  128. howler/odm/models/ecs/pe.py +17 -0
  129. howler/odm/models/ecs/process.py +216 -0
  130. howler/odm/models/ecs/registry.py +26 -0
  131. howler/odm/models/ecs/related.py +45 -0
  132. howler/odm/models/ecs/rule.py +51 -0
  133. howler/odm/models/ecs/server.py +24 -0
  134. howler/odm/models/ecs/threat.py +247 -0
  135. howler/odm/models/ecs/tls.py +58 -0
  136. howler/odm/models/ecs/url.py +51 -0
  137. howler/odm/models/ecs/user.py +57 -0
  138. howler/odm/models/ecs/user_agent.py +20 -0
  139. howler/odm/models/ecs/vulnerability.py +41 -0
  140. howler/odm/models/gcp.py +16 -0
  141. howler/odm/models/hit.py +356 -0
  142. howler/odm/models/howler_data.py +328 -0
  143. howler/odm/models/lead.py +33 -0
  144. howler/odm/models/localized_label.py +13 -0
  145. howler/odm/models/overview.py +16 -0
  146. howler/odm/models/pivot.py +40 -0
  147. howler/odm/models/template.py +24 -0
  148. howler/odm/models/user.py +83 -0
  149. howler/odm/models/view.py +34 -0
  150. howler/odm/random_data.py +888 -0
  151. howler/odm/randomizer.py +606 -0
  152. howler/patched.py +5 -0
  153. howler/plugins/__init__.py +25 -0
  154. howler/plugins/config.py +123 -0
  155. howler/remote/__init__.py +0 -0
  156. howler/remote/datatypes/README.md +355 -0
  157. howler/remote/datatypes/__init__.py +98 -0
  158. howler/remote/datatypes/counters.py +63 -0
  159. howler/remote/datatypes/events.py +66 -0
  160. howler/remote/datatypes/hash.py +206 -0
  161. howler/remote/datatypes/lock.py +42 -0
  162. howler/remote/datatypes/queues/__init__.py +0 -0
  163. howler/remote/datatypes/queues/comms.py +59 -0
  164. howler/remote/datatypes/queues/multi.py +32 -0
  165. howler/remote/datatypes/queues/named.py +93 -0
  166. howler/remote/datatypes/queues/priority.py +215 -0
  167. howler/remote/datatypes/set.py +118 -0
  168. howler/remote/datatypes/user_quota_tracker.py +54 -0
  169. howler/security/__init__.py +253 -0
  170. howler/security/socket.py +108 -0
  171. howler/security/utils.py +185 -0
  172. howler/services/__init__.py +0 -0
  173. howler/services/action_service.py +111 -0
  174. howler/services/analytic_service.py +128 -0
  175. howler/services/auth_service.py +323 -0
  176. howler/services/config_service.py +128 -0
  177. howler/services/dossier_service.py +252 -0
  178. howler/services/event_service.py +93 -0
  179. howler/services/hit_service.py +893 -0
  180. howler/services/jwt_service.py +158 -0
  181. howler/services/lucene_service.py +286 -0
  182. howler/services/notebook_service.py +119 -0
  183. howler/services/overview_service.py +44 -0
  184. howler/services/template_service.py +45 -0
  185. howler/services/user_service.py +330 -0
  186. howler/utils/__init__.py +0 -0
  187. howler/utils/annotations.py +28 -0
  188. howler/utils/chunk.py +38 -0
  189. howler/utils/dict_utils.py +200 -0
  190. howler/utils/isotime.py +17 -0
  191. howler/utils/list_utils.py +11 -0
  192. howler/utils/lucene.py +77 -0
  193. howler/utils/path.py +27 -0
  194. howler/utils/socket_utils.py +61 -0
  195. howler/utils/str_utils.py +256 -0
  196. howler/utils/uid.py +47 -0
  197. howler_api-2.13.0.dev329.dist-info/METADATA +71 -0
  198. howler_api-2.13.0.dev329.dist-info/RECORD +200 -0
  199. howler_api-2.13.0.dev329.dist-info/WHEEL +4 -0
  200. howler_api-2.13.0.dev329.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,46 @@
1
+ import sys
2
+ import time
3
+
4
+ DELAY = 5
5
+
6
+ if __name__ == "__main__":
7
+ print("This script will allow you to reindex all indexes in 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
+ answer = input("Are you sure you want to reindex all data in this cluster? [yes/NO]\n")
15
+
16
+ if not answer.startswith("y"):
17
+ print("Confirmation not provided, stopping.")
18
+ sys.exit(1)
19
+
20
+ from howler.datastore.collection import ESCollection
21
+
22
+ ESCollection.IGNORE_ENSURE_COLLECTION = True
23
+
24
+ from howler.common import loader
25
+
26
+ ds = loader.datastore(archive_access=False)
27
+
28
+ print("You will be reindexing the following indexes:")
29
+ print("\n".join(ds.hit.index_list_full))
30
+
31
+ answer = input(("\nAre you sure you want to reindex all indexes? [yes/NO]\n"))
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"Reindexing in {2 * DELAY - i}...", end="\r")
40
+ time.sleep(1)
41
+
42
+ print()
43
+
44
+ result = ds.hit.reindex()
45
+
46
+ 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]