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.

Files changed (198) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +168 -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/clue.py +99 -0
  21. howler/api/v1/configs.py +58 -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 +788 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +416 -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 +125 -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/loader.py +154 -0
  41. howler/common/logging/__init__.py +241 -0
  42. howler/common/logging/audit.py +138 -0
  43. howler/common/logging/format.py +38 -0
  44. howler/common/net.py +79 -0
  45. howler/common/net_static.py +1494 -0
  46. howler/common/random_user.py +316 -0
  47. howler/common/swagger.py +117 -0
  48. howler/config.py +64 -0
  49. howler/cronjobs/__init__.py +29 -0
  50. howler/cronjobs/retention.py +61 -0
  51. howler/cronjobs/rules.py +274 -0
  52. howler/cronjobs/view_cleanup.py +88 -0
  53. howler/datastore/README.md +112 -0
  54. howler/datastore/__init__.py +0 -0
  55. howler/datastore/bulk.py +72 -0
  56. howler/datastore/collection.py +2342 -0
  57. howler/datastore/constants.py +119 -0
  58. howler/datastore/exceptions.py +41 -0
  59. howler/datastore/howler_store.py +105 -0
  60. howler/datastore/migrations/fix_process.py +41 -0
  61. howler/datastore/operations.py +130 -0
  62. howler/datastore/schemas.py +90 -0
  63. howler/datastore/store.py +231 -0
  64. howler/datastore/support/__init__.py +0 -0
  65. howler/datastore/support/build.py +215 -0
  66. howler/datastore/support/schemas.py +90 -0
  67. howler/datastore/types.py +22 -0
  68. howler/error.py +91 -0
  69. howler/external/__init__.py +0 -0
  70. howler/external/generate_mitre.py +96 -0
  71. howler/external/generate_sigma_rules.py +31 -0
  72. howler/external/generate_tlds.py +47 -0
  73. howler/external/reindex_data.py +66 -0
  74. howler/external/wipe_databases.py +58 -0
  75. howler/gunicorn_config.py +25 -0
  76. howler/healthz.py +47 -0
  77. howler/helper/__init__.py +0 -0
  78. howler/helper/azure.py +50 -0
  79. howler/helper/discover.py +59 -0
  80. howler/helper/hit.py +236 -0
  81. howler/helper/oauth.py +247 -0
  82. howler/helper/search.py +92 -0
  83. howler/helper/workflow.py +110 -0
  84. howler/helper/ws.py +378 -0
  85. howler/odm/README.md +102 -0
  86. howler/odm/__init__.py +1 -0
  87. howler/odm/base.py +1543 -0
  88. howler/odm/charter.txt +146 -0
  89. howler/odm/helper.py +416 -0
  90. howler/odm/howler_enum.py +25 -0
  91. howler/odm/models/__init__.py +0 -0
  92. howler/odm/models/action.py +33 -0
  93. howler/odm/models/analytic.py +90 -0
  94. howler/odm/models/assemblyline.py +48 -0
  95. howler/odm/models/aws.py +23 -0
  96. howler/odm/models/azure.py +16 -0
  97. howler/odm/models/cbs.py +44 -0
  98. howler/odm/models/config.py +558 -0
  99. howler/odm/models/dossier.py +33 -0
  100. howler/odm/models/ecs/__init__.py +0 -0
  101. howler/odm/models/ecs/agent.py +17 -0
  102. howler/odm/models/ecs/autonomous_system.py +16 -0
  103. howler/odm/models/ecs/client.py +149 -0
  104. howler/odm/models/ecs/cloud.py +141 -0
  105. howler/odm/models/ecs/code_signature.py +27 -0
  106. howler/odm/models/ecs/container.py +32 -0
  107. howler/odm/models/ecs/dns.py +62 -0
  108. howler/odm/models/ecs/egress.py +10 -0
  109. howler/odm/models/ecs/elf.py +74 -0
  110. howler/odm/models/ecs/email.py +122 -0
  111. howler/odm/models/ecs/error.py +14 -0
  112. howler/odm/models/ecs/event.py +140 -0
  113. howler/odm/models/ecs/faas.py +24 -0
  114. howler/odm/models/ecs/file.py +84 -0
  115. howler/odm/models/ecs/geo.py +30 -0
  116. howler/odm/models/ecs/group.py +18 -0
  117. howler/odm/models/ecs/hash.py +16 -0
  118. howler/odm/models/ecs/host.py +17 -0
  119. howler/odm/models/ecs/http.py +37 -0
  120. howler/odm/models/ecs/ingress.py +12 -0
  121. howler/odm/models/ecs/interface.py +21 -0
  122. howler/odm/models/ecs/network.py +30 -0
  123. howler/odm/models/ecs/observer.py +45 -0
  124. howler/odm/models/ecs/organization.py +12 -0
  125. howler/odm/models/ecs/os.py +21 -0
  126. howler/odm/models/ecs/pe.py +17 -0
  127. howler/odm/models/ecs/process.py +216 -0
  128. howler/odm/models/ecs/registry.py +26 -0
  129. howler/odm/models/ecs/related.py +45 -0
  130. howler/odm/models/ecs/rule.py +51 -0
  131. howler/odm/models/ecs/server.py +24 -0
  132. howler/odm/models/ecs/threat.py +247 -0
  133. howler/odm/models/ecs/tls.py +58 -0
  134. howler/odm/models/ecs/url.py +51 -0
  135. howler/odm/models/ecs/user.py +57 -0
  136. howler/odm/models/ecs/user_agent.py +20 -0
  137. howler/odm/models/ecs/vulnerability.py +41 -0
  138. howler/odm/models/gcp.py +16 -0
  139. howler/odm/models/hit.py +356 -0
  140. howler/odm/models/howler_data.py +328 -0
  141. howler/odm/models/lead.py +24 -0
  142. howler/odm/models/localized_label.py +13 -0
  143. howler/odm/models/overview.py +16 -0
  144. howler/odm/models/pivot.py +40 -0
  145. howler/odm/models/template.py +24 -0
  146. howler/odm/models/user.py +83 -0
  147. howler/odm/models/view.py +34 -0
  148. howler/odm/random_data.py +888 -0
  149. howler/odm/randomizer.py +609 -0
  150. howler/patched.py +5 -0
  151. howler/plugins/__init__.py +25 -0
  152. howler/plugins/config.py +123 -0
  153. howler/remote/__init__.py +0 -0
  154. howler/remote/datatypes/README.md +355 -0
  155. howler/remote/datatypes/__init__.py +98 -0
  156. howler/remote/datatypes/counters.py +63 -0
  157. howler/remote/datatypes/events.py +66 -0
  158. howler/remote/datatypes/hash.py +206 -0
  159. howler/remote/datatypes/lock.py +42 -0
  160. howler/remote/datatypes/queues/__init__.py +0 -0
  161. howler/remote/datatypes/queues/comms.py +59 -0
  162. howler/remote/datatypes/queues/multi.py +32 -0
  163. howler/remote/datatypes/queues/named.py +93 -0
  164. howler/remote/datatypes/queues/priority.py +215 -0
  165. howler/remote/datatypes/set.py +118 -0
  166. howler/remote/datatypes/user_quota_tracker.py +54 -0
  167. howler/security/__init__.py +253 -0
  168. howler/security/socket.py +108 -0
  169. howler/security/utils.py +185 -0
  170. howler/services/__init__.py +0 -0
  171. howler/services/action_service.py +111 -0
  172. howler/services/analytic_service.py +128 -0
  173. howler/services/auth_service.py +323 -0
  174. howler/services/config_service.py +128 -0
  175. howler/services/dossier_service.py +252 -0
  176. howler/services/event_service.py +93 -0
  177. howler/services/hit_service.py +893 -0
  178. howler/services/jwt_service.py +158 -0
  179. howler/services/lucene_service.py +286 -0
  180. howler/services/notebook_service.py +119 -0
  181. howler/services/overview_service.py +44 -0
  182. howler/services/template_service.py +45 -0
  183. howler/services/user_service.py +331 -0
  184. howler/utils/__init__.py +0 -0
  185. howler/utils/annotations.py +28 -0
  186. howler/utils/chunk.py +38 -0
  187. howler/utils/dict_utils.py +200 -0
  188. howler/utils/isotime.py +17 -0
  189. howler/utils/list_utils.py +11 -0
  190. howler/utils/lucene.py +77 -0
  191. howler/utils/path.py +27 -0
  192. howler/utils/socket_utils.py +61 -0
  193. howler/utils/str_utils.py +256 -0
  194. howler/utils/uid.py +47 -0
  195. howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
  196. howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
  197. howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
  198. howler_api-3.0.0.dev374.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,107 @@
1
+ # This the default classification engine provided,
2
+ # it showcases all the different features of the classification engine
3
+ # while providing a useful configuration
4
+
5
+ # Turn on/off classification enforcement. When this flag is off, this
6
+ # completely disables the classification engine, any documents added while
7
+ # the classification engine is off gets the default unrestricted value
8
+ enforce: false
9
+ dynamic_groups: false
10
+
11
+ # List of Classification level:
12
+ # Graded list were a smaller number is less restricted then an higher number.
13
+ levels:
14
+ # List of alternate names for the current marking
15
+ - aliases:
16
+ - UNRESTRICTED
17
+ - UNCLASSIFIED
18
+ - U
19
+ # Stylesheet applied in the UI for the different levels
20
+ css:
21
+ # Name of the color scheme used for display (default, primary, secondary, success, info, warning, error)
22
+ color: default
23
+ # Description of the classification level
24
+ description: Subject to standard copyright rules, TLP:WHITE information may be distributed without restriction.
25
+ # Interger value of the Classification level (higher is more classified)
26
+ lvl: 100
27
+ # Long name of the classification item
28
+ name: TLP:WHITE
29
+ # Short name of the classification item
30
+ short_name: TLP:W
31
+ - aliases: []
32
+ css:
33
+ color: success
34
+ description:
35
+ Recipients may share TLP:GREEN information with peers and partner organizations
36
+ within their sector or community, but not via publicly accessible channels. Information
37
+ in this category can be circulated widely within a particular community. TLP:GREEN
38
+ information may not be released outside of the community.
39
+ lvl: 110
40
+ name: TLP:GREEN
41
+ short_name: TLP:G
42
+ - aliases:
43
+ - RESTRICTED
44
+ css:
45
+ color: warning
46
+ description:
47
+ Recipients may only share TLP:AMBER information with members of their
48
+ own organization and with clients or customers who need to know the information
49
+ to protect themselves or prevent further harm.
50
+ lvl: 120
51
+ name: TLP:AMBER
52
+ short_name: TLP:A
53
+
54
+ # List of required tokens:
55
+ # A user requesting access to an item must have all the
56
+ # required tokens the item has to gain access to it
57
+ required:
58
+ - aliases: []
59
+ description: Produced using a commercial tool with limited distribution
60
+ name: COMMERCIAL
61
+ short_name: CMR
62
+ # The minimum classification level an item must have
63
+ # for this token to be valid. (optional)
64
+ # require_lvl: 100
65
+
66
+ # List of groups:
67
+ # A user requesting access to an item must be part of a least
68
+ # of one the group the item is part of to gain access
69
+ groups:
70
+ - aliases: []
71
+ # This is a special flag that when set to true, if any groups are selected
72
+ # in a classification. This group will automatically be selected too. (optional)
73
+ auto_select: true
74
+ description: Employees of CSE
75
+ name: CSE
76
+ short_name: CSE
77
+ # Assuming that this groups is the only group selected, this is the display name
78
+ # that will be used in the classification (that values has to be in the aliases
79
+ # of this group and only this group) (optional)
80
+ # solitary_display_name: ANY
81
+
82
+ # List of subgroups:
83
+ # A user requesting access to an item must be part of a least
84
+ # of one the subgroup the item is part of to gain access
85
+ subgroups:
86
+ - aliases: []
87
+ description: Member of Incident Response team
88
+ name: IR TEAM
89
+ short_name: IR
90
+ - aliases: []
91
+ description: Member of the Canadian Centre for Cyber Security
92
+ # This is a special flag that auto-select the corresponding group
93
+ # when this subgroup is selected (optional)
94
+ require_group: CSE
95
+ name: CCCS
96
+ short_name: CCCS
97
+ # This is a special flag that makes sure that none other then the
98
+ # corresponding group is selected when this subgroup is selected (optional)
99
+ # limited_to_group: CSE
100
+
101
+ # Default restricted classification
102
+ restricted: TLP:A//CMR
103
+
104
+ # Default unrestricted classification:
105
+ # When no classification are provided or that the classification engine is
106
+ # disabled, this is the classification value each items will get
107
+ unrestricted: TLP:W
@@ -0,0 +1,167 @@
1
+ from inspect import getmembers, isfunction
2
+ from sys import exc_info
3
+ from traceback import format_tb
4
+ from typing import Optional
5
+
6
+
7
+ class HowlerException(Exception):
8
+ """Wrapper for all exceptions thrown in howler's code"""
9
+
10
+ message: str
11
+ cause: Optional[Exception]
12
+
13
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
14
+ super().__init__(message)
15
+ self.message = message
16
+ self.cause = cause
17
+
18
+ def __repr__(self) -> str:
19
+ """String reproduction of the howler exception. Pass the message on"""
20
+ return self.message
21
+
22
+
23
+ class InvalidClassification(HowlerException):
24
+ """Exception for Invalid Classification"""
25
+
26
+
27
+ class InvalidDefinition(HowlerException):
28
+ """Exception for Invalid Definition"""
29
+
30
+
31
+ class InvalidRangeException(HowlerException):
32
+ """Exception for Invalid Range"""
33
+
34
+
35
+ class NonRecoverableError(HowlerException):
36
+ """Exception for an unrecoverable error"""
37
+
38
+
39
+ class RecoverableError(HowlerException):
40
+ """Exception for a recoverable error"""
41
+
42
+
43
+ class ConfigException(HowlerException):
44
+ """Exception thrown due to invalid configuration"""
45
+
46
+
47
+ class ResourceExists(HowlerException):
48
+ """Exception thrown due to a pre-existing resource"""
49
+
50
+
51
+ class VersionConflict(HowlerException):
52
+ """Exception thrown due to a version conflict"""
53
+
54
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
55
+ HowlerException.__init__(self, message, cause)
56
+
57
+
58
+ class HowlerTypeError(HowlerException, TypeError):
59
+ """TypeError child specifically for exceptions thrown by us"""
60
+
61
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
62
+ HowlerException.__init__(self, message, cause if cause is not None else TypeError(message))
63
+
64
+
65
+ class HowlerAttributeError(HowlerException, AttributeError):
66
+ """AttributeError child specifically for exceptions thrown by us"""
67
+
68
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
69
+ HowlerException.__init__(self, message, cause if cause is not None else AttributeError(message))
70
+
71
+
72
+ class HowlerValueError(HowlerException, ValueError):
73
+ """ValueError child specifically for exceptions thrown by us"""
74
+
75
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
76
+ HowlerException.__init__(self, message, cause if cause is not None else ValueError(message))
77
+
78
+
79
+ class HowlerNotImplementedError(HowlerException, NotImplementedError):
80
+ """NotImplementedError child specifically for exceptions thrown by us"""
81
+
82
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
83
+ HowlerException.__init__(self, message, cause if cause is not None else NotImplementedError(message))
84
+
85
+
86
+ class HowlerKeyError(HowlerException, KeyError):
87
+ """KeyError child specifically for exceptions thrown by us"""
88
+
89
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
90
+ HowlerException.__init__(self, message, cause if cause is not None else KeyError(message))
91
+
92
+
93
+ class HowlerRuntimeError(HowlerException, RuntimeError):
94
+ """RuntimeError child specifically for exceptions thrown by us"""
95
+
96
+ def __init__(self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
97
+ HowlerException.__init__(self, message, cause if cause is not None else RuntimeError(message))
98
+
99
+
100
+ class NotFoundException(HowlerException):
101
+ """Exception thrown when a resource cannot be found"""
102
+
103
+
104
+ class ForbiddenException(HowlerException):
105
+ """Exception thrown when a user is not permitted to perform an action"""
106
+
107
+
108
+ class AccessDeniedException(HowlerException):
109
+ """Exception thrown when a resource cannot be accessed by a user"""
110
+
111
+
112
+ class InvalidDataException(HowlerException):
113
+ """Exception thrown when user-provided data is invalid"""
114
+
115
+
116
+ class AuthenticationException(HowlerException):
117
+ """Exception thrown when a user cannot be authenticated"""
118
+
119
+
120
+ class Chain(object):
121
+ """This class can be used as a decorator to override the type of exceptions returned by a function"""
122
+
123
+ def __init__(self, exception):
124
+ self.exception = exception
125
+
126
+ def __call__(self, original):
127
+ """Execute a function and wrap any resulting exceptions"""
128
+
129
+ def wrapper(*args, **kwargs):
130
+ try:
131
+ return original(*args, **kwargs)
132
+ except Exception as e:
133
+ wrapped = self.exception(str(e), e)
134
+ raise wrapped.with_traceback(exc_info()[2])
135
+
136
+ wrapper.__name__ = original.__name__
137
+ wrapper.__doc__ = original.__doc__
138
+ wrapper.__dict__.update(original.__dict__)
139
+
140
+ return wrapper
141
+
142
+ def execute(self, func, *args, **kwargs):
143
+ """Execute a function and wrap any resulting exceptions"""
144
+ try:
145
+ return func(*args, **kwargs)
146
+ except Exception as e:
147
+ wrapped = self.exception(str(e), e)
148
+ raise wrapped.with_traceback(exc_info()[2])
149
+
150
+
151
+ class ChainAll:
152
+ """This class can be used as a decorator to override the type of exceptions returned by every method of a class"""
153
+
154
+ def __init__(self, exception):
155
+ self.exception = Chain(exception)
156
+
157
+ def __call__(self, cls):
158
+ """We can use an instance of this class as a decorator."""
159
+ for method in getmembers(cls, predicate=isfunction):
160
+ setattr(cls, method[0], self.exception(method[1]))
161
+
162
+ return cls
163
+
164
+
165
+ def get_stacktrace_info(ex: Exception) -> str:
166
+ """Get and format traceback information from a given exception"""
167
+ return "".join(format_tb(exc_info()[2]) + [": ".join((ex.__class__.__name__, str(ex)))])
@@ -0,0 +1,154 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+ from string import Template
5
+ from typing import TYPE_CHECKING, Optional, Union
6
+
7
+ import yaml
8
+
9
+ from howler.odm.models.config import config
10
+
11
+ if TYPE_CHECKING:
12
+ from howler.common.classification import Classification
13
+
14
+ APP_NAME = os.environ.get("APP_NAME", "howler")
15
+ APP_PREFIX = os.environ.get("APP_PREFIX", "hwl")
16
+ USER_TYPES = {"admin", "user", "automation_basic", "automation_advanced"}
17
+
18
+
19
+ def env_substitute(buffer):
20
+ """Replace environment variables in the buffer with their value.
21
+
22
+ Use the built in template expansion tool that expands environment variable style strings ${}
23
+ We set the idpattern to none so that $abc doesn't get replaced but ${abc} does.
24
+
25
+ Case insensitive.
26
+ Variables that are found in the buffer, but are not defined as environment variables are ignored.
27
+ """
28
+ return Template(buffer).safe_substitute(os.environ, idpattern=None, bracedidpattern="(?a:[_a-z][_a-z0-9]*)")
29
+
30
+
31
+ _CLASSIFICATIONS: dict[Union[str, Path], "Classification"] = {}
32
+
33
+
34
+ def get_classification(yml_config: Optional[str] = None): # noqa: C901
35
+ "Get the classification from a given classification.yml file, caching results"
36
+ if yml_config in _CLASSIFICATIONS:
37
+ return _CLASSIFICATIONS[yml_config]
38
+
39
+ log = logging.getLogger(f"{APP_NAME}.common.loader")
40
+
41
+ if not yml_config:
42
+ yml_config_path = Path("/etc") / APP_NAME.replace("-dev", "") / "conf" / "classification.yml"
43
+ if yml_config_path.is_symlink():
44
+ log.info("%s is a symbolic link!", yml_config_path)
45
+ if str(yml_config_path.readlink()).startswith("..data"):
46
+ yml_config_path = yml_config_path.parent / yml_config_path.readlink()
47
+ log.info(
48
+ "This symbolic link links to a configmap, handling accordingly. Reading from %s",
49
+ yml_config_path,
50
+ )
51
+ else:
52
+ yml_config_path = Path(os.path.realpath(yml_config_path.readlink()))
53
+ log.info(
54
+ "Reading from %s",
55
+ yml_config_path,
56
+ )
57
+
58
+ if not yml_config_path.exists():
59
+ log.warning(f"{yml_config_path} does not exist!")
60
+ yml_config_path = Path("/etc") / APP_NAME.replace("-dev", "") / "classification.yml"
61
+ log.warning(f"Checking at {yml_config_path} instead.")
62
+ else:
63
+ yml_config_path = Path(yml_config)
64
+
65
+ log.debug("Loading classification definition from %s", yml_config_path)
66
+
67
+ classification_definition = None
68
+ # Load modifiers from the yaml config
69
+ if yml_config_path.exists():
70
+ with yml_config_path.open() as yml_fh:
71
+ yml_data = yaml.safe_load(yml_fh.read())
72
+ if yml_data:
73
+ classification_definition = yml_data
74
+
75
+ if classification_definition is None:
76
+ log.warning(
77
+ "Specified classification file does not exist or does not contain data!"
78
+ " Defaulting to default classification file."
79
+ )
80
+ default_file = Path(__file__).parent / "classification.yml"
81
+ if default_file.exists():
82
+ with default_file.open() as default_fh:
83
+ default_yml_data = yaml.safe_load(default_fh.read())
84
+ if default_yml_data:
85
+ classification_definition = default_yml_data
86
+ else:
87
+ log.critical("%s was not accessible!", default_file)
88
+
89
+ from howler.common.classification import Classification, InvalidDefinition
90
+
91
+ if not classification_definition:
92
+ raise InvalidDefinition("Could not find any classification definition to load.")
93
+
94
+ _classification = Classification(classification_definition)
95
+
96
+ if yml_config:
97
+ _CLASSIFICATIONS[yml_config] = _classification
98
+
99
+ return _classification
100
+
101
+
102
+ def get_lookups(lookup_folder: Optional[str] = None):
103
+ """Get lookups from the specified lookup folder"""
104
+ from howler.config import config
105
+
106
+ if not lookup_folder:
107
+ lookup_folder_path = Path("/etc/") / APP_NAME.replace("-dev", "") / "lookups"
108
+ else:
109
+ lookup_folder_path = Path(lookup_folder)
110
+
111
+ lookups = {}
112
+
113
+ if lookup_folder_path.exists():
114
+ for file in lookup_folder_path.iterdir():
115
+ with file.open("r") as f:
116
+ data = yaml.safe_load(f)
117
+ lookups[file.stem] = data
118
+ local_path = Path(__file__).parent.parent.parent / "static/mitre"
119
+ config_path = Path(config.ui.static_folder) if config.ui.static_folder else None
120
+ if local_path.exists():
121
+ mitre_path = local_path
122
+ elif config_path and (config_path / "mitre").exists():
123
+ mitre_path = config_path / "mitre"
124
+ else:
125
+ mitre_path = None
126
+
127
+ if mitre_path:
128
+ lookups["icons"] = sorted(set(f.stem for f in mitre_path.iterdir()))
129
+ else:
130
+ lookups["icons"] = []
131
+
132
+ lookups["roles"] = sorted(USER_TYPES)
133
+
134
+ return lookups
135
+
136
+
137
+ # Lazy load the datastore
138
+ _datastore = None
139
+
140
+
141
+ def datastore(_config=None, archive_access=True):
142
+ """Get a datastore connection"""
143
+ global _datastore
144
+
145
+ from howler.datastore.howler_store import HowlerDatastore
146
+ from howler.datastore.store import ESStore
147
+
148
+ if not _config:
149
+ _config = config
150
+
151
+ if _datastore is None:
152
+ _datastore = HowlerDatastore(ESStore(config=config, archive_access=archive_access))
153
+
154
+ return _datastore
@@ -0,0 +1,241 @@
1
+ import json
2
+ import logging
3
+ import logging.handlers
4
+ import os
5
+ import re
6
+ from traceback import format_exception
7
+ from typing import TYPE_CHECKING, Optional, Union
8
+
9
+ from flask import request
10
+ from typing_extensions import override # type: ignore
11
+
12
+ from howler.common import loader
13
+ from howler.common.logging.format import (
14
+ HWL_DATE_FORMAT,
15
+ HWL_JSON_FORMAT,
16
+ HWL_LOG_FORMAT,
17
+ HWL_SYSLOG_FORMAT,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from howler.odm.models.config import Config
22
+
23
+ LOG_LEVEL_MAP = {
24
+ "DEBUG": logging.DEBUG,
25
+ "INFO": logging.INFO,
26
+ "WARNING": logging.WARNING,
27
+ "ERROR": logging.ERROR,
28
+ "CRITICAL": logging.CRITICAL,
29
+ "DISABLED": 60,
30
+ }
31
+
32
+ DEBUG = False
33
+
34
+
35
+ class JsonFormatter(logging.Formatter):
36
+ """logging Formatter to output in JSON"""
37
+
38
+ @override
39
+ def formatMessage(self, record):
40
+ if record.exc_info:
41
+ record.exc_text = self.formatException(record.exc_info)
42
+ record.exc_info = None
43
+
44
+ if record.exc_text:
45
+ record.message += "\n" + record.exc_text
46
+ record.exc_text = None
47
+
48
+ record.message = json.dumps(record.message)
49
+ return self._style.format(record)
50
+
51
+ @override
52
+ def formatException(self, exc_info):
53
+ return "".join(format_exception(*exc_info))
54
+
55
+
56
+ def init_log_to_file(logger: logging.Logger, log_level: int, name: str, config: "Config"):
57
+ """Initialize file-based logging"""
58
+ if not os.path.isdir(config.logging.log_directory):
59
+ logger.warning(
60
+ "Log directory does not exist. Will try to create %s",
61
+ config.logging.log_directory,
62
+ )
63
+ os.makedirs(config.logging.log_directory)
64
+
65
+ if log_level <= logging.DEBUG:
66
+ dbg_file_handler = logging.handlers.RotatingFileHandler(
67
+ os.path.join(config.logging.log_directory, f"{name}.dbg"),
68
+ maxBytes=10485760,
69
+ backupCount=5,
70
+ )
71
+ dbg_file_handler.setLevel(logging.DEBUG)
72
+ if config.logging.log_as_json:
73
+ dbg_file_handler.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
74
+ else:
75
+ dbg_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
76
+ logger.addHandler(dbg_file_handler)
77
+
78
+ if log_level <= logging.INFO:
79
+ op_file_handler = logging.handlers.RotatingFileHandler(
80
+ os.path.join(config.logging.log_directory, f"{name}.log"),
81
+ maxBytes=10485760,
82
+ backupCount=5,
83
+ )
84
+ op_file_handler.setLevel(logging.INFO)
85
+ if config.logging.log_as_json:
86
+ op_file_handler.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
87
+ else:
88
+ op_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
89
+ logger.addHandler(op_file_handler)
90
+
91
+ if log_level <= logging.ERROR:
92
+ err_file_handler = logging.handlers.RotatingFileHandler(
93
+ os.path.join(config.logging.log_directory, f"{name}.err"),
94
+ maxBytes=10485760,
95
+ backupCount=5,
96
+ )
97
+ err_file_handler.setLevel(logging.ERROR)
98
+ if config.logging.log_as_json:
99
+ err_file_handler.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
100
+ else:
101
+ err_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
102
+ err_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
103
+ logger.addHandler(err_file_handler)
104
+
105
+
106
+ def init_logging(name: str, log_level: Optional[int] = None):
107
+ """Initialize the logger"""
108
+ from howler.config import config
109
+
110
+ logger = logging.getLogger(loader.APP_NAME)
111
+
112
+ # Test if we've initialized the log handler already.
113
+ if len(logger.handlers) != 0:
114
+ return logger.getChild(name)
115
+
116
+ if name.startswith(f"{loader.APP_NAME}."):
117
+ name = name[len(loader.APP_NAME) + 1 :]
118
+
119
+ config.logging.log_to_console = config.logging.log_to_console or config.ui.debug
120
+
121
+ if log_level is None:
122
+ log_level = LOG_LEVEL_MAP[config.logging.log_level]
123
+
124
+ logging.root.setLevel(logging.CRITICAL)
125
+ logger.setLevel(log_level)
126
+
127
+ if config.logging.log_level == "DISABLED":
128
+ # While log_level is set to disable, we will not create any handlers
129
+ return logger.getChild(name)
130
+
131
+ if config.logging.log_to_file:
132
+ init_log_to_file(logger, log_level, name, config)
133
+
134
+ if config.logging.log_to_console:
135
+ console = logging.StreamHandler()
136
+ if config.logging.log_as_json:
137
+ console.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
138
+ else:
139
+ console.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
140
+ logger.addHandler(console)
141
+
142
+ if config.logging.log_to_syslog and config.logging.syslog_host and config.logging.syslog_port:
143
+ syslog_handler = logging.handlers.SysLogHandler(
144
+ address=(config.logging.syslog_host, config.logging.syslog_port)
145
+ )
146
+ syslog_handler.formatter = logging.Formatter(HWL_SYSLOG_FORMAT)
147
+ logger.addHandler(syslog_handler)
148
+
149
+ logger.debug("Logger ready!")
150
+ return logger.getChild(name)
151
+
152
+
153
+ def get_logger(name: str = "default") -> logging.Logger:
154
+ """Get a logger with a useful name given a filename"""
155
+ name = re.sub(r".+(howler|test)/", "", name).replace("/", ".").replace(".__init__", "").replace(".py", "")
156
+ name = re.sub(r"^api\.?", "", name)
157
+ logger = init_logging("api")
158
+ if name:
159
+ logger = logger.getChild(name)
160
+ return logger
161
+
162
+
163
+ def get_traceback_info(tb):
164
+ """Prase the traceback information for a given traceback"""
165
+ tb_list = []
166
+ tb_id = 0
167
+ last_ui = None
168
+ while tb is not None:
169
+ f = tb.tb_frame
170
+ line_no = tb.tb_lineno
171
+ tb_list.append((f, line_no))
172
+ tb = tb.tb_next
173
+ if "/ui/" in f.f_code.co_filename:
174
+ last_ui = tb_id
175
+ tb_id += 1
176
+
177
+ if last_ui is not None:
178
+ tb_frame, line = tb_list[last_ui]
179
+ user = tb_frame.f_locals.get("kwargs", {}).get("user", None)
180
+
181
+ if not user:
182
+ temp = tb_frame.f_locals.get("_", {})
183
+ if isinstance(temp, dict):
184
+ user = temp.get("user", None)
185
+
186
+ if not user:
187
+ user = tb_frame.f_locals.get("user", None)
188
+
189
+ if not user:
190
+ user = tb_frame.f_locals.get("impersonator", None)
191
+
192
+ if user:
193
+ return user, tb_frame.f_code.co_filename, tb_frame.f_code.co_name, line
194
+
195
+ return None
196
+
197
+ return None
198
+
199
+
200
+ def __dumb_log(log, msg, is_exception=False):
201
+ """Dumb logger for use with log_with_traceback"""
202
+ args: Union[str, bytes] = request.query_string
203
+ if isinstance(args, bytes):
204
+ args = args.decode()
205
+
206
+ if args:
207
+ args = f"?{args}"
208
+
209
+ message = f"{msg} - {request.path}{args}"
210
+ if is_exception:
211
+ log.exception(message)
212
+ else:
213
+ log.warning(message)
214
+
215
+
216
+ def log_with_traceback(traceback, msg, is_exception=False, audit=False):
217
+ """Log a message along with the stacktrace"""
218
+ log = get_logger("traceback") if not audit else logging.getLogger("howler.api.audit")
219
+
220
+ tb_info = get_traceback_info(traceback)
221
+ if tb_info:
222
+ tb_user, tb_file, tb_function, tb_line_no = tb_info
223
+ args: Optional[Union[str, bytes]] = request.query_string
224
+ if args:
225
+ args = f"?{args if isinstance(args, str) else args.decode()}"
226
+ else:
227
+ args = ""
228
+
229
+ try:
230
+ message = (
231
+ f'{tb_user["uname"]} [{tb_user["classification"]}] :: {msg} - {tb_file}:{tb_function}:{tb_line_no}'
232
+ f'[{os.environ.get("HOWLER_VERSION", "0.0.0.dev0")}] ({request.path}{args})'
233
+ )
234
+ if is_exception:
235
+ log.exception(message)
236
+ else:
237
+ log.warning(message)
238
+ except Exception:
239
+ __dumb_log(log, msg, is_exception=is_exception)
240
+ else:
241
+ __dumb_log(log, msg, is_exception=is_exception)