howler-evidence-plugin 0.1.0.dev120__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Canadian Centre for Cyber Security
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: howler-evidence-plugin
3
+ Version: 0.1.0.dev120
4
+ Summary: A howler plugin to add additional nested ECS fields to the Howler ODM
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: CCCS
8
+ Author-email: analysis-development@cyber.gc.ca
9
+ Requires-Python: >=3.9.17, <4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Howler Evidence Plugin
20
+
21
+ A howler plugin to add additional nested ECS fields to the Howler ODM.
22
+
@@ -0,0 +1,3 @@
1
+ # Howler Evidence Plugin
2
+
3
+ A howler plugin to add additional nested ECS fields to the Howler ODM.
@@ -0,0 +1,38 @@
1
+ # mypy: ignore-errors
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from howler.plugins.config import BasePluginConfig
6
+ from pydantic_settings import SettingsConfigDict
7
+
8
+ APP_NAME = os.environ.get("APP_NAME", "howler")
9
+ PLUGIN_NAME = "evidence"
10
+
11
+ root_path = Path("/etc") / APP_NAME.replace("-dev", "").replace("-stg", "")
12
+
13
+ config_locations = [
14
+ Path(__file__).parent / "manifest.yml",
15
+ root_path / "conf" / f"{PLUGIN_NAME}.yml",
16
+ Path(os.environ.get("HWL_CONF_FOLDER", root_path)) / f"{PLUGIN_NAME}.yml",
17
+ ]
18
+
19
+
20
+ class EvidenceConfig(BasePluginConfig):
21
+ "Evidence Plugin Configuration Model"
22
+
23
+ model_config = SettingsConfigDict(
24
+ yaml_file=config_locations,
25
+ yaml_file_encoding="utf-8",
26
+ strict=True,
27
+ env_nested_delimiter="__",
28
+ env_prefix=f"{PLUGIN_NAME}_",
29
+ )
30
+
31
+
32
+ config = EvidenceConfig()
33
+
34
+ if __name__ == "__main__":
35
+ # When executed, the config model will print the default values of the configuration
36
+ import yaml
37
+
38
+ print(yaml.safe_dump(EvidenceConfig().model_dump(mode="json"))) # noqa: T201
@@ -0,0 +1,5 @@
1
+ name: evidence
2
+ modules:
3
+ odm:
4
+ modify_odm:
5
+ hit: true
@@ -0,0 +1,31 @@
1
+ from random import randint
2
+
3
+ import howler.odm as odm
4
+ from howler.common.logging import get_logger
5
+ from howler.odm.models.hit import Hit
6
+ from howler.odm.randomizer import random_model_obj
7
+
8
+ from evidence.odm.models.evidence import Evidence
9
+
10
+ logger = get_logger(__file__)
11
+
12
+
13
+ def modify_odm(target: odm.Model):
14
+ "Add additional internal fields to the ODM"
15
+ logger.info("Modifying ODM with additional fields")
16
+
17
+ target.add_namespace(
18
+ "evidence",
19
+ odm.List(
20
+ odm.Compound(Evidence),
21
+ default=[],
22
+ description="A list of additional ECS objects.",
23
+ ),
24
+ )
25
+
26
+
27
+ def generate_useful_hit(hit: Hit) -> Hit:
28
+ "Add cccs-specific changes to hits on generation"
29
+ hit.evidence = [random_model_obj(Evidence) for _ in range(randint(1, 3))]
30
+
31
+ return ["evidence"], hit
@@ -0,0 +1,292 @@
1
+ from howler import odm
2
+ from howler.odm.models.ecs.agent import Agent
3
+ from howler.odm.models.ecs.client import Client
4
+ from howler.odm.models.ecs.cloud import Cloud
5
+ from howler.odm.models.ecs.container import Container
6
+ from howler.odm.models.ecs.dns import DNS
7
+ from howler.odm.models.ecs.email import Email
8
+ from howler.odm.models.ecs.error import Error
9
+ from howler.odm.models.ecs.event import Event
10
+ from howler.odm.models.ecs.faas import FAAS
11
+ from howler.odm.models.ecs.file import File
12
+ from howler.odm.models.ecs.group import Group
13
+ from howler.odm.models.ecs.host import Host
14
+ from howler.odm.models.ecs.http import HTTP
15
+ from howler.odm.models.ecs.interface import Interface
16
+ from howler.odm.models.ecs.network import Network
17
+ from howler.odm.models.ecs.observer import Observer
18
+ from howler.odm.models.ecs.organization import Organization
19
+ from howler.odm.models.ecs.process import Process
20
+ from howler.odm.models.ecs.registry import Registry
21
+ from howler.odm.models.ecs.related import Related
22
+ from howler.odm.models.ecs.rule import Rule
23
+ from howler.odm.models.ecs.server import Server
24
+ from howler.odm.models.ecs.threat import Threat
25
+ from howler.odm.models.ecs.tls import TLS
26
+ from howler.odm.models.ecs.url import URL
27
+ from howler.odm.models.ecs.user import User
28
+ from howler.odm.models.ecs.user_agent import UserAgent
29
+ from howler.odm.models.ecs.vulnerability import Vulnerability
30
+ from howler.odm.models.hit import ECSVersion
31
+
32
+
33
+ @odm.model(
34
+ index=True,
35
+ store=True,
36
+ description="Evidence fields add a list of additional ECS objects.",
37
+ )
38
+ class Evidence(odm.Model):
39
+ # Base Fields
40
+ timestamp: str = odm.Date(
41
+ default="NOW",
42
+ description="Date/time when the event originated.",
43
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-base.html",
44
+ )
45
+ labels: dict[str, str] = odm.Mapping(
46
+ odm.Keyword(),
47
+ default={},
48
+ description="Custom key/value pairs.",
49
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-base.html",
50
+ )
51
+ tags: list[str] = odm.List(
52
+ odm.Keyword(),
53
+ default=[],
54
+ description="List of keywords used to tag each event.",
55
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-base.html",
56
+ )
57
+ message: str = odm.Keyword(
58
+ default="",
59
+ description="Log message for log events, optimized for viewing in a log viewer",
60
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-base.html",
61
+ )
62
+
63
+ # Field Sets
64
+ agent: Agent = odm.Optional(
65
+ odm.Compound(
66
+ Agent,
67
+ description="The agent fields contain the data about the software entity, "
68
+ "if any, that collects, detects, or observes events on a host, or takes measurements on a host.",
69
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-agent.html",
70
+ )
71
+ )
72
+ cloud: Cloud = odm.Optional(
73
+ odm.Compound(
74
+ Cloud,
75
+ description="Fields related to the cloud or infrastructure the events are coming from.",
76
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-cloud.html",
77
+ )
78
+ )
79
+ container: Container = odm.Optional(
80
+ odm.Compound(
81
+ Container,
82
+ description="Container fields are used for meta information about the specific container "
83
+ "that is the source of information.",
84
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-container.html",
85
+ )
86
+ )
87
+ destination: Client = odm.Optional(
88
+ odm.Compound(
89
+ Client,
90
+ description="Destination fields capture details about the receiver of a network exchange/packet.",
91
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-destination.html",
92
+ )
93
+ )
94
+ dns: DNS = odm.Optional(
95
+ odm.Compound(
96
+ DNS,
97
+ description="Fields describing DNS queries and answers.",
98
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-dns.html",
99
+ )
100
+ )
101
+ ecs: ECSVersion = odm.Compound(
102
+ ECSVersion,
103
+ default={},
104
+ description="Meta-information specific to ECS.",
105
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-ecs.html",
106
+ )
107
+ error: Error = odm.Optional(
108
+ odm.Compound(
109
+ Error,
110
+ description="These fields can represent errors of any kind.",
111
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-error.html",
112
+ )
113
+ )
114
+ event: Event = odm.Optional(
115
+ odm.Compound(
116
+ Event,
117
+ description="The event fields are used for context information about the log or metric event itself.",
118
+ )
119
+ )
120
+ email: Email = odm.Optional(
121
+ odm.Compound(
122
+ Email,
123
+ description="Event details relating to an email transaction.",
124
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-event.html",
125
+ )
126
+ )
127
+ faas: FAAS = odm.Optional(
128
+ odm.Compound(
129
+ FAAS,
130
+ description="The user fields describe information about the function as a service "
131
+ "(FaaS) that is relevant to the event.",
132
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-faas.html",
133
+ )
134
+ )
135
+ file: File = odm.Optional(
136
+ odm.Compound(
137
+ File,
138
+ description="A file is defined as a set of information that has been "
139
+ "created on, or has existed on a filesystem.",
140
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-file.html",
141
+ )
142
+ )
143
+ group: Group = odm.Optional(
144
+ odm.Compound(
145
+ Group,
146
+ description="The group fields are meant to represent groups that are relevant to the event.",
147
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-group.html",
148
+ )
149
+ )
150
+ host: Host = odm.Optional(
151
+ odm.Compound(Host),
152
+ description=(
153
+ "A host is defined as a general computing instance. ECS host.* fields should be populated with details "
154
+ "about the host on which the event happened, or from which the measurement was taken. Host types include "
155
+ "hardware, virtual machines, Docker containers, and Kubernetes nodes."
156
+ ),
157
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-host.html",
158
+ )
159
+ http: HTTP = odm.Optional(
160
+ odm.Compound(
161
+ HTTP,
162
+ description="Fields related to HTTP activity. Use the url field set to store the url of the request.",
163
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-http.html",
164
+ )
165
+ )
166
+ observer: Observer = odm.Optional(
167
+ odm.Compound(
168
+ Observer,
169
+ description=(
170
+ "Observer is defined as a special network, security, or application device used to detect, obs"
171
+ "erve, or create network, sercurity, or application event metrics"
172
+ ),
173
+ )
174
+ )
175
+ interface: Interface = odm.Optional(
176
+ odm.Compound(
177
+ Interface,
178
+ description=(
179
+ "The interface fields are used to record ingress and egress interface information when reported "
180
+ "by an observer (e.g. firewall, router, load balancer) in the context of the observer handling a "
181
+ "network connection. "
182
+ ),
183
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-interface.html",
184
+ )
185
+ )
186
+ network: Network = odm.Optional(
187
+ odm.Compound(
188
+ Network,
189
+ description=(
190
+ "The network is defined as the communication path over which a host or network event happens."
191
+ ),
192
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-network.html",
193
+ )
194
+ )
195
+ organization: Organization = odm.Optional(
196
+ odm.Compound(
197
+ Organization,
198
+ description="The organization fields enrich data with information "
199
+ "about the company or entity the data is associated with.",
200
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-organization.html",
201
+ )
202
+ )
203
+ process: Process = odm.Optional(
204
+ odm.Compound(
205
+ Process,
206
+ description="These fields contain information about a process.",
207
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-process.html",
208
+ )
209
+ )
210
+ registry: Registry = odm.Optional(
211
+ odm.Compound(
212
+ Registry,
213
+ description="Fields related to Windows Registry operations.",
214
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-registry.html",
215
+ )
216
+ )
217
+ related: Related = odm.Optional(
218
+ odm.Compound(
219
+ Related,
220
+ description="Fields related to Windows Registry operations.",
221
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-related.html",
222
+ )
223
+ )
224
+ rule: Rule = odm.Optional(
225
+ odm.Compound(
226
+ Rule,
227
+ description="Capture the specifics of any observer or agent rules",
228
+ )
229
+ )
230
+ server: Server = odm.Optional(
231
+ odm.Compound(Server),
232
+ description=(
233
+ "A Server is defined as the responder in a network connection for events regarding sessions, "
234
+ "connections, or bidirectional flow records."
235
+ ),
236
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-server.html",
237
+ )
238
+ source: Client = odm.Optional(
239
+ odm.Compound(
240
+ Client,
241
+ description="Source fields capture details about the sender of a network exchange/packet.",
242
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-source.html",
243
+ )
244
+ )
245
+ threat: Threat = odm.Optional(
246
+ odm.Compound(
247
+ Threat,
248
+ description="Fields to classify events and alerts according to a threat taxonomy such as the "
249
+ "MITRE ATT&CK® framework.",
250
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-threat.html",
251
+ )
252
+ )
253
+ tls: TLS = odm.Optional(
254
+ odm.Compound(
255
+ TLS,
256
+ description=(
257
+ "Fields related to a TLS connection. These fields focus on the TLS protocol itself and "
258
+ "intentionally avoids in-depth analysis of the related x.509 certificate files."
259
+ ),
260
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-tls.html",
261
+ )
262
+ )
263
+ url: URL = odm.Optional(
264
+ odm.Compound(
265
+ URL,
266
+ description="URL fields provide support for complete or partial URLs, and "
267
+ "supports the breaking down into scheme, domain, path, and so on.",
268
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-url.html",
269
+ )
270
+ )
271
+ user: User = odm.Optional(
272
+ odm.Compound(
273
+ User,
274
+ description="The user fields describe information about the user that is relevant to the event.",
275
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-user.html",
276
+ )
277
+ )
278
+ user_agent: UserAgent = odm.Optional(
279
+ odm.Compound(
280
+ UserAgent,
281
+ description="The user_agent fields normally come from a browser request.",
282
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-user_agent.html",
283
+ )
284
+ )
285
+ vulnerability: Vulnerability = odm.Optional(
286
+ odm.Compound(
287
+ Vulnerability,
288
+ description="The vulnerability fields describe information about a vulnerability that "
289
+ "is relevant to an event.",
290
+ reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-vulnerability.html",
291
+ )
292
+ )
@@ -0,0 +1,154 @@
1
+ [project]
2
+ name = "howler-evidence-plugin"
3
+ version = "0.1.0.dev120"
4
+ description = "A howler plugin to add additional nested ECS fields to the Howler ODM"
5
+ authors = [{ name = "CCCS", email = "analysis-development@cyber.gc.ca" }]
6
+ license = { text = "MIT" }
7
+ readme = "README.md"
8
+ requires-python = ">=3.9.17, <4.0"
9
+
10
+ ######################
11
+ # autoflake settings #
12
+ ######################
13
+ [tool.autoflake]
14
+ check = true
15
+
16
+ ##################
17
+ # Black settings #
18
+ ##################
19
+ [tool.black]
20
+ line-length = 120
21
+
22
+ ########################
23
+ # coverage.py settings #
24
+ ########################
25
+ [tool.coverage.run]
26
+ branch = true
27
+ sigterm = true
28
+ data_file = ".coverage"
29
+
30
+ [tool.coverage.report]
31
+ exclude_also = [
32
+ "def __repr__",
33
+ "if DEBUG:",
34
+ "raise AssertionError",
35
+ "raise NotImplementedError",
36
+ "if 0:",
37
+ "if __name__ == .__main__.:",
38
+ "if TYPE_CHECKING:",
39
+ "@(abc\\.)?abstractmethod]",
40
+ "if \"pytest\" in sys.modules:",
41
+ ]
42
+
43
+ #################
44
+ # Mypy settings #
45
+ #################
46
+ [tool.mypy]
47
+ warn_unused_configs = true
48
+ ignore_missing_imports = true
49
+ check_untyped_defs = true
50
+ disable_error_code = "no-redef"
51
+ exclude = "test"
52
+
53
+ [[tool.mypy.overrides]]
54
+ module = "howler.odm.models.*"
55
+ disable_error_code = "assignment"
56
+
57
+ [[tool.mypy.overrides]]
58
+ module = ["howler.odm.base", "test", "test.*"]
59
+ ignore_errors = true
60
+
61
+ [[tool.mypy.overrides]]
62
+ module = "howler.security"
63
+ disable_error_code = "attr-defined"
64
+
65
+ [[tool.mypy.overrides]]
66
+ module = "howler.odm.helper"
67
+ disable_error_code = "union-attr"
68
+
69
+ [[tool.mypy.overrides]]
70
+ module = "howler.common.classification"
71
+ disable_error_code = "assignment"
72
+
73
+ [[tool.mypy.overrides]]
74
+ module = "requests"
75
+ ignore_missing_imports = true
76
+
77
+ [tool.ruff]
78
+ line-length = 120
79
+ indent-width = 4
80
+ target-version = "py39"
81
+
82
+ [tool.ruff.lint]
83
+ select = [
84
+ "E",
85
+ "F",
86
+ "W",
87
+ "C901",
88
+ "I",
89
+ "N",
90
+ "D1",
91
+ "D2",
92
+ "ANN",
93
+ "S",
94
+ "T20",
95
+ "PIE",
96
+ "FLY",
97
+ "TRY",
98
+ ]
99
+ ignore = [
100
+ "ANN003",
101
+ "ANN201",
102
+ "ANN401",
103
+ "D100",
104
+ "D104",
105
+ "D105",
106
+ "D107",
107
+ "D203",
108
+ "D213",
109
+ "N818",
110
+ "S603",
111
+ "TRY003",
112
+ "TRY300",
113
+ ]
114
+ exclude = ["howler/patched.py"]
115
+
116
+ [tool.ruff.lint.flake8-annotations]
117
+ ignore-fully-untyped = true
118
+ mypy-init-return = true
119
+ suppress-dummy-args = true
120
+ suppress-none-returning = true
121
+
122
+ [tool.ruff.lint.per-file-ignores]
123
+ "evidence/**/__init__.py" = ["F401"]
124
+ "evidence/odm/hit.py" = ["S311"]
125
+ "evidence/odm/models/*" = ["D", "ANN", "C901"]
126
+ "test/*" = ["D", "ANN", "S", "N818", "TRY", "PIE", "E402"]
127
+ "build_scripts/*" = ["D", "ANN", "S", "N818", "T20", "TRY"]
128
+
129
+ [tool.poetry]
130
+ packages = [{ include = "evidence" }]
131
+
132
+
133
+ [tool.poetry.group.dev.dependencies]
134
+ ruff = "^0.11.7"
135
+ mypy = "^1.15.0"
136
+ pre-commit = "^4.2.0"
137
+ howler-api = { path = "../../api" }
138
+ pytest = "^8.3.5"
139
+ mock = "^5.2.0"
140
+ pytest-cov = "^6.1.1"
141
+ coverage = { extras = ["toml"], version = "^7.8.0" }
142
+ diff-cover = "^9.2.4"
143
+ python-dotenv = "^1.1.0"
144
+
145
+
146
+ [tool.poetry.group.types.dependencies]
147
+ types-mock = "^5.2.0.20250516"
148
+
149
+ [build-system]
150
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
151
+ build-backend = "poetry.core.masonry.api"
152
+
153
+ [tool.poetry.scripts]
154
+ type_check = "build_scripts.type_check:main"