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,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)))])
|
howler/common/loader.py
ADDED
|
@@ -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)
|