pyoaev 2.0.13__tar.gz → 2.1.0__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.
- {pyoaev-2.0.13/pyoaev.egg-info → pyoaev-2.1.0}/PKG-INFO +5 -4
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/__init__.py +1 -1
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/_version.py +1 -1
- pyoaev-2.1.0/pyoaev/configuration/__init__.py +15 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/configuration/configuration.py +26 -1
- pyoaev-2.1.0/pyoaev/configuration/connector_config_schema_generator.py +127 -0
- pyoaev-2.1.0/pyoaev/configuration/settings_loader.py +125 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/contracts/contract_config.py +13 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/daemons/base_daemon.py +8 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/helpers.py +17 -7
- pyoaev-2.1.0/pyoaev/security_domain/types.py +16 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0/pyoaev.egg-info}/PKG-INFO +5 -4
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev.egg-info/SOURCES.txt +4 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev.egg-info/requires.txt +4 -3
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyproject.toml +4 -3
- pyoaev-2.1.0/test/signatures/__init__.py +0 -0
- pyoaev-2.0.13/pyoaev/configuration/__init__.py +0 -3
- {pyoaev-2.0.13 → pyoaev-2.1.0}/LICENSE +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/README.md +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/docs/conf.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/attack_pattern.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/collector.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/cve.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/document.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/endpoint.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inject.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inject_expectation/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inject_expectation/inject_expectation.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inject_expectation/model/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inject_expectation/model/expectation.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inject_expectation_trace.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/injector.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/injector_contract.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inputs/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/inputs/search.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/kill_chain_phase.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/me.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/organization.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/payload.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/security_platform.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/tag.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/team.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/apis/user.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/backends/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/backends/backend.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/backends/protocol.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/base.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/client.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/configuration/sources.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/contracts/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/contracts/contract_builder.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/contracts/contract_utils.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/contracts/variable_helper.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/daemons/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/daemons/collector_daemon.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/exceptions.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/mixins.py +0 -0
- {pyoaev-2.0.13/pyoaev/signatures → pyoaev-2.1.0/pyoaev/security_domain}/__init__.py +0 -0
- {pyoaev-2.0.13/test → pyoaev-2.1.0/pyoaev/signatures}/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/signatures/signature_match.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/signatures/signature_type.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/signatures/types.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev/utils.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev.egg-info/dependency_links.txt +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/pyoaev.egg-info/top_level.txt +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/scripts/release.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/setup.cfg +0 -0
- {pyoaev-2.0.13/test/apis → pyoaev-2.1.0/test}/__init__.py +0 -0
- {pyoaev-2.0.13/test/apis/endpoint → pyoaev-2.1.0/test/apis}/__init__.py +0 -0
- {pyoaev-2.0.13/test/apis/expectation → pyoaev-2.1.0/test/apis/endpoint}/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/apis/endpoint/test_endpoint.py +0 -0
- {pyoaev-2.0.13/test/apis/injector_contract → pyoaev-2.1.0/test/apis/expectation}/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/apis/expectation/test_expectation.py +0 -0
- {pyoaev-2.0.13/test/configuration → pyoaev-2.1.0/test/apis/injector_contract}/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/apis/injector_contract/test_injector_contract.py +0 -0
- {pyoaev-2.0.13/test/daemons → pyoaev-2.1.0/test/configuration}/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/configuration/test_configuration.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/configuration/test_sources.py +0 -0
- {pyoaev-2.0.13/test/signatures → pyoaev-2.1.0/test/daemons}/__init__.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/daemons/test_base_daemon.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/daemons/test_collector_daemon.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/signatures/test_signature_match.py +0 -0
- {pyoaev-2.0.13 → pyoaev-2.1.0}/test/signatures/test_signature_type.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyoaev
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python API client for OpenAEV.
|
|
5
5
|
Author-email: Filigran <contact@filigran.io>
|
|
6
6
|
Maintainer-email: Filigran <contact@filigran.io>
|
|
@@ -26,6 +26,7 @@ Requires-Dist: python-magic-bin<0.5,>=0.4.14; sys_platform == "win32"
|
|
|
26
26
|
Requires-Dist: python_json_logger<3.4.0,>=3.3.0
|
|
27
27
|
Requires-Dist: PyYAML<6.1,>=6.0
|
|
28
28
|
Requires-Dist: pydantic<2.12.0,>=2.11.3
|
|
29
|
+
Requires-Dist: pydantic-settings<2.12.0,>=2.11.0
|
|
29
30
|
Requires-Dist: requests<2.33.0,>=2.32.3
|
|
30
31
|
Requires-Dist: setuptools<80.10.0,>=80.9.0
|
|
31
32
|
Requires-Dist: cachetools<5.6.0,>=5.5.0
|
|
@@ -36,9 +37,9 @@ Requires-Dist: requests-toolbelt<1.1.0,>=1.0.0
|
|
|
36
37
|
Requires-Dist: dataclasses-json<0.7.0,>=0.6.4
|
|
37
38
|
Requires-Dist: thefuzz<0.23,>=0.22
|
|
38
39
|
Provides-Extra: dev
|
|
39
|
-
Requires-Dist: black<25.
|
|
40
|
-
Requires-Dist: build<1.
|
|
41
|
-
Requires-Dist: isort<6.
|
|
40
|
+
Requires-Dist: black<25.12.0,>=25.11.0; extra == "dev"
|
|
41
|
+
Requires-Dist: build<1.4.0,>=1.3.0; extra == "dev"
|
|
42
|
+
Requires-Dist: isort<6.2.0,>=6.1.0; extra == "dev"
|
|
42
43
|
Requires-Dist: types-pytz<2025.3.0.0,>=2025.2.0.20250326; extra == "dev"
|
|
43
44
|
Requires-Dist: pre-commit<4.3.0,>=4.2.0; extra == "dev"
|
|
44
45
|
Requires-Dist: types-python-dateutil<2.10.0,>=2.9.0; extra == "dev"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .configuration import Configuration
|
|
2
|
+
from .settings_loader import (
|
|
3
|
+
BaseConfigModel,
|
|
4
|
+
ConfigLoaderCollector,
|
|
5
|
+
ConfigLoaderOAEV,
|
|
6
|
+
SettingsLoader,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Configuration",
|
|
11
|
+
"ConfigLoaderOAEV",
|
|
12
|
+
"ConfigLoaderCollector",
|
|
13
|
+
"SettingsLoader",
|
|
14
|
+
"BaseConfigModel",
|
|
15
|
+
]
|
|
@@ -4,7 +4,11 @@ from typing import Any, Dict, Optional
|
|
|
4
4
|
|
|
5
5
|
import yaml
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
7
8
|
|
|
9
|
+
from pyoaev.configuration.connector_config_schema_generator import (
|
|
10
|
+
ConnectorConfigSchemaGenerator,
|
|
11
|
+
)
|
|
8
12
|
from pyoaev.configuration.sources import DictionarySource, EnvironmentSource
|
|
9
13
|
|
|
10
14
|
CONFIGURATION_TYPES = str | int | bool | Any | None
|
|
@@ -111,6 +115,7 @@ class Configuration:
|
|
|
111
115
|
config_hints: Dict[str, dict | str],
|
|
112
116
|
config_values: dict = None,
|
|
113
117
|
config_file_path: str = os.path.join(os.curdir, "config.yml"),
|
|
118
|
+
config_base_model: BaseSettings = None,
|
|
114
119
|
):
|
|
115
120
|
self.__config_hints = {
|
|
116
121
|
key: (
|
|
@@ -129,6 +134,8 @@ class Configuration:
|
|
|
129
134
|
|
|
130
135
|
self.__config_values = (config_values or {}) | file_contents
|
|
131
136
|
|
|
137
|
+
self.__base_model = config_base_model
|
|
138
|
+
|
|
132
139
|
def get(self, config_key: str) -> CONFIGURATION_TYPES:
|
|
133
140
|
"""Gets the value pointed to by the configuration key. If the key is defined
|
|
134
141
|
with actual hints (as opposed to a discrete value), it will use those hints to
|
|
@@ -146,7 +153,12 @@ class Configuration:
|
|
|
146
153
|
return None
|
|
147
154
|
|
|
148
155
|
return self.__process_value_to_type(
|
|
149
|
-
|
|
156
|
+
(
|
|
157
|
+
self.__dig_config_sources_for_key(config)
|
|
158
|
+
if config.data is None
|
|
159
|
+
else config.data
|
|
160
|
+
),
|
|
161
|
+
config.is_number,
|
|
150
162
|
)
|
|
151
163
|
|
|
152
164
|
def set(self, config_key: str, value: CONFIGURATION_TYPES):
|
|
@@ -164,6 +176,19 @@ class Configuration:
|
|
|
164
176
|
else:
|
|
165
177
|
self.__config_hints[config_key].data = value
|
|
166
178
|
|
|
179
|
+
def schema(self):
|
|
180
|
+
"""
|
|
181
|
+
Generates the complete connector schema using a custom schema generator compatible with Pydantic.
|
|
182
|
+
Isolate custom class generator, Pydantic expects a class, not an instance
|
|
183
|
+
Always subclass GenerateJsonSchema and pass the class to Pydantic, not an instance
|
|
184
|
+
:return: The generated connector schema as a dictionary.
|
|
185
|
+
"""
|
|
186
|
+
return self.__base_model.model_json_schema(
|
|
187
|
+
by_alias=False,
|
|
188
|
+
schema_generator=ConnectorConfigSchemaGenerator,
|
|
189
|
+
mode="validation",
|
|
190
|
+
)
|
|
191
|
+
|
|
167
192
|
@staticmethod
|
|
168
193
|
def __process_value_to_type(value: CONFIGURATION_TYPES, is_number_hint: bool):
|
|
169
194
|
if value is None:
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
## ADAPTED FROM https://github.com/OpenCTI-Platform/connectors/blob/5c8cf1235f62f5651c9c08d0b67f1bd182662c8a/shared/tools/composer/generate_connectors_config_schemas/generate_connector_config_json_schema.py.sample
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
from pydantic.json_schema import GenerateJsonSchema
|
|
7
|
+
|
|
8
|
+
# attributes filtered from the connector configuration before generating the manifest
|
|
9
|
+
__FILTERED_ATTRIBUTES__ = [
|
|
10
|
+
# connector id is generated
|
|
11
|
+
"CONNECTOR_ID",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConnectorConfigSchemaGenerator(GenerateJsonSchema):
|
|
16
|
+
@staticmethod
|
|
17
|
+
def dereference_schema(schema_with_refs):
|
|
18
|
+
"""Return a new schema with all internal $ref resolved."""
|
|
19
|
+
|
|
20
|
+
def _resolve(schema, root):
|
|
21
|
+
if isinstance(schema, dict):
|
|
22
|
+
if "$ref" in schema:
|
|
23
|
+
ref_path = schema["$ref"]
|
|
24
|
+
if ref_path.startswith("#/$defs/"):
|
|
25
|
+
def_name = ref_path.split("/")[-1]
|
|
26
|
+
# Deep copy to avoid mutating $defs
|
|
27
|
+
resolved = deepcopy(root["$defs"][def_name])
|
|
28
|
+
return _resolve(resolved, root)
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(f"Unsupported ref format: {ref_path}")
|
|
31
|
+
else:
|
|
32
|
+
return {
|
|
33
|
+
schema_key: _resolve(schema_value, root)
|
|
34
|
+
for schema_key, schema_value in schema.items()
|
|
35
|
+
}
|
|
36
|
+
elif isinstance(schema, list):
|
|
37
|
+
return [_resolve(item, root) for item in schema]
|
|
38
|
+
else:
|
|
39
|
+
return schema
|
|
40
|
+
|
|
41
|
+
return _resolve(deepcopy(schema_with_refs), schema_with_refs)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def flatten_config_loader_schema(root_schema: dict):
|
|
45
|
+
"""
|
|
46
|
+
Flatten config loader schema so all config vars are described at root level.
|
|
47
|
+
|
|
48
|
+
:param root_schema: Original schema.
|
|
49
|
+
:return: Flatten schema.
|
|
50
|
+
"""
|
|
51
|
+
flat_json_schema = {
|
|
52
|
+
"$schema": root_schema["$schema"],
|
|
53
|
+
"$id": root_schema["$id"],
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {},
|
|
56
|
+
"required": [],
|
|
57
|
+
"additionalProperties": root_schema.get("additionalProperties", True),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (
|
|
61
|
+
config_loader_namespace_name,
|
|
62
|
+
config_loader_namespace_schema,
|
|
63
|
+
) in root_schema["properties"].items():
|
|
64
|
+
config_schema = config_loader_namespace_schema.get("properties", {})
|
|
65
|
+
required_config_vars = config_loader_namespace_schema.get("required", [])
|
|
66
|
+
|
|
67
|
+
for config_var_name, config_var_schema in config_schema.items():
|
|
68
|
+
property_name = (
|
|
69
|
+
f"{config_loader_namespace_name.upper()}_{config_var_name.upper()}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
config_var_schema.pop("title", None)
|
|
73
|
+
|
|
74
|
+
flat_json_schema["properties"][property_name] = config_var_schema
|
|
75
|
+
|
|
76
|
+
if config_var_name in required_config_vars:
|
|
77
|
+
flat_json_schema["required"].append(property_name)
|
|
78
|
+
|
|
79
|
+
return flat_json_schema
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def filter_schema(schema):
|
|
83
|
+
for filtered_attribute in __FILTERED_ATTRIBUTES__:
|
|
84
|
+
if filtered_attribute in schema["properties"]:
|
|
85
|
+
del schema["properties"][filtered_attribute]
|
|
86
|
+
schema.update(
|
|
87
|
+
{
|
|
88
|
+
"required": [
|
|
89
|
+
item
|
|
90
|
+
for item in schema["required"]
|
|
91
|
+
if item != filtered_attribute
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return schema
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
def generate(self, schema, mode="validation"):
|
|
100
|
+
json_schema = super().generate(schema, mode=mode)
|
|
101
|
+
|
|
102
|
+
json_schema["$schema"] = self.schema_dialect
|
|
103
|
+
json_schema["$id"] = "config.schema.json"
|
|
104
|
+
dereferenced_schema = self.dereference_schema(json_schema)
|
|
105
|
+
flattened_schema = self.flatten_config_loader_schema(dereferenced_schema)
|
|
106
|
+
return self.filter_schema(flattened_schema)
|
|
107
|
+
|
|
108
|
+
@override
|
|
109
|
+
def nullable_schema(self, schema):
|
|
110
|
+
"""Generates a JSON schema that matches a schema that allows null values.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
schema: The core schema.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The generated JSON schema.
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
This method overrides `GenerateJsonSchema.nullable_schema` to generate schemas without `anyOf` keyword.
|
|
120
|
+
"""
|
|
121
|
+
null_schema = {"type": "null"}
|
|
122
|
+
inner_json_schema = self.generate_inner(schema["schema"])
|
|
123
|
+
|
|
124
|
+
if inner_json_schema == null_schema:
|
|
125
|
+
return null_schema
|
|
126
|
+
else:
|
|
127
|
+
return inner_json_schema
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, PlainSerializer
|
|
8
|
+
from pydantic_settings import (
|
|
9
|
+
BaseSettings,
|
|
10
|
+
DotEnvSettingsSource,
|
|
11
|
+
PydanticBaseSettingsSource,
|
|
12
|
+
SettingsConfigDict,
|
|
13
|
+
YamlConfigSettingsSource,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseConfigModel(BaseModel, ABC):
|
|
18
|
+
"""Base class for global config models
|
|
19
|
+
To prevent attributes from being modified after initialization.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(extra="allow", frozen=True, validate_default=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SettingsLoader(BaseSettings):
|
|
26
|
+
model_config = SettingsConfigDict(
|
|
27
|
+
frozen=True,
|
|
28
|
+
extra="allow",
|
|
29
|
+
env_nested_delimiter="_",
|
|
30
|
+
env_nested_max_split=1,
|
|
31
|
+
enable_decoding=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def settings_customise_sources(
|
|
36
|
+
cls,
|
|
37
|
+
settings_cls: type[BaseSettings],
|
|
38
|
+
init_settings: PydanticBaseSettingsSource,
|
|
39
|
+
env_settings: PydanticBaseSettingsSource,
|
|
40
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
41
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
42
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
43
|
+
"""Customise the sources of settings for the connector.
|
|
44
|
+
|
|
45
|
+
This method is called by the Pydantic BaseSettings class to determine the order of sources.
|
|
46
|
+
The configuration come in this order either from:
|
|
47
|
+
1. Environment variables
|
|
48
|
+
2. YAML file
|
|
49
|
+
3. .env file
|
|
50
|
+
4. Default values
|
|
51
|
+
|
|
52
|
+
The variables loading order will remain the same as in `pycti.get_config_variable()`:
|
|
53
|
+
1. If a config.yml file is found, the order will be: `ENV VAR` → config.yml → default value
|
|
54
|
+
2. If a .env file is found, the order will be: `ENV VAR` → .env → default value
|
|
55
|
+
"""
|
|
56
|
+
_main_path = os.curdir
|
|
57
|
+
|
|
58
|
+
settings_cls.model_config["env_file"] = f"{_main_path}/../.env"
|
|
59
|
+
|
|
60
|
+
if not settings_cls.model_config["yaml_file"]:
|
|
61
|
+
if Path(f"{_main_path}/config.yml").is_file():
|
|
62
|
+
settings_cls.model_config["yaml_file"] = f"{_main_path}/config.yml"
|
|
63
|
+
if Path(f"{_main_path}/../config.yml").is_file():
|
|
64
|
+
settings_cls.model_config["yaml_file"] = f"{_main_path}/../config.yml"
|
|
65
|
+
|
|
66
|
+
if Path(settings_cls.model_config["yaml_file"] or "").is_file(): # type: ignore
|
|
67
|
+
return (
|
|
68
|
+
env_settings,
|
|
69
|
+
YamlConfigSettingsSource(settings_cls),
|
|
70
|
+
)
|
|
71
|
+
if Path(settings_cls.model_config["env_file"] or "").is_file(): # type: ignore
|
|
72
|
+
return (
|
|
73
|
+
env_settings,
|
|
74
|
+
DotEnvSettingsSource(settings_cls),
|
|
75
|
+
)
|
|
76
|
+
return (env_settings,)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
LogLevelToLower = Annotated[
|
|
80
|
+
Literal["debug", "info", "warn", "error"],
|
|
81
|
+
PlainSerializer(lambda v: "".join(v), return_type=str),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
HttpUrlToString = Annotated[HttpUrl, PlainSerializer(str, return_type=str)]
|
|
85
|
+
TimedeltaInSeconds = Annotated[
|
|
86
|
+
timedelta, PlainSerializer(lambda v: int(v.total_seconds()), return_type=int)
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ConfigLoaderOAEV(BaseConfigModel):
|
|
91
|
+
"""OpenAEV/OpenAEV platform configuration settings.
|
|
92
|
+
|
|
93
|
+
Contains URL and authentication token for connecting to the OpenAEV platform.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
url: HttpUrlToString = Field(
|
|
97
|
+
description="The OpenAEV platform URL.",
|
|
98
|
+
)
|
|
99
|
+
token: str = Field(
|
|
100
|
+
description="The token for the OpenAEV platform.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConfigLoaderCollector(BaseConfigModel):
|
|
105
|
+
"""Base collector configuration settings.
|
|
106
|
+
|
|
107
|
+
Contains common collector settings including identification, logging,
|
|
108
|
+
scheduling, and platform information.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
id: str = Field(description="ID of the collector.")
|
|
112
|
+
|
|
113
|
+
name: str = Field(description="Name of the collector")
|
|
114
|
+
|
|
115
|
+
log_level: LogLevelToLower | None = Field(
|
|
116
|
+
default="error",
|
|
117
|
+
description="Determines the verbosity of the logs.",
|
|
118
|
+
)
|
|
119
|
+
period: timedelta | None = Field(
|
|
120
|
+
default=timedelta(minutes=1),
|
|
121
|
+
description="Duration between two scheduled runs of the collector (ISO 8601 format).",
|
|
122
|
+
)
|
|
123
|
+
icon_filepath: str | None = Field(
|
|
124
|
+
description="Path to the icon file of the collector.",
|
|
125
|
+
)
|
|
@@ -120,6 +120,15 @@ class ContractConfig:
|
|
|
120
120
|
color_light: str
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
@dataclass
|
|
124
|
+
class Domain:
|
|
125
|
+
domain_id: str
|
|
126
|
+
domain_name: str
|
|
127
|
+
domain_color: str
|
|
128
|
+
domain_created_at: str
|
|
129
|
+
domain_updated_at: str
|
|
130
|
+
|
|
131
|
+
|
|
123
132
|
@dataclass
|
|
124
133
|
class Contract:
|
|
125
134
|
contract_id: str
|
|
@@ -141,6 +150,7 @@ class Contract:
|
|
|
141
150
|
is_atomic_testing: bool = True
|
|
142
151
|
platforms: List[str] = field(default_factory=list)
|
|
143
152
|
external_id: str = None
|
|
153
|
+
domains: List[Domain] = None
|
|
144
154
|
|
|
145
155
|
def add_attack_pattern(self, var: str):
|
|
146
156
|
self.contract_attack_patterns_external_ids.append(var)
|
|
@@ -163,6 +173,7 @@ class Contract:
|
|
|
163
173
|
"contract_content": json.dumps(self, cls=utils.EnhancedJSONEncoder),
|
|
164
174
|
"is_atomic_testing": self.is_atomic_testing,
|
|
165
175
|
"contract_platforms": self.platforms,
|
|
176
|
+
"contract_domains": self.domains,
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
def to_contract_update_input(self):
|
|
@@ -174,6 +185,7 @@ class Contract:
|
|
|
174
185
|
"contract_content": json.dumps(self, cls=utils.EnhancedJSONEncoder),
|
|
175
186
|
"is_atomic_testing": self.is_atomic_testing,
|
|
176
187
|
"contract_platforms": self.platforms,
|
|
188
|
+
"contract_domains": self.domains,
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
|
|
@@ -203,6 +215,7 @@ def prepare_contracts(contracts):
|
|
|
203
215
|
"contract_attack_patterns_external_ids": c.contract_attack_patterns_external_ids,
|
|
204
216
|
"contract_content": json.dumps(c, cls=utils.EnhancedJSONEncoder),
|
|
205
217
|
"contract_platforms": c.platforms,
|
|
218
|
+
"contract_domains": c.domains,
|
|
206
219
|
},
|
|
207
220
|
contracts,
|
|
208
221
|
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
from inspect import signature
|
|
3
4
|
from types import FunctionType
|
|
@@ -101,6 +102,13 @@ class BaseDaemon(ABC):
|
|
|
101
102
|
follow-up with the main execution loop. Note that at this point, if there is no
|
|
102
103
|
configured callback, the method will abort and kill the daemon.
|
|
103
104
|
"""
|
|
105
|
+
parser = argparse.ArgumentParser(description="parse daemon options")
|
|
106
|
+
parser.add_argument("--dump-config-schema", action="store_true")
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
if args.dump_config_schema:
|
|
109
|
+
print(self._configuration.schema())
|
|
110
|
+
return
|
|
111
|
+
|
|
104
112
|
if self._callback is None:
|
|
105
113
|
raise OpenAEVError("This daemon has no configured callback.")
|
|
106
114
|
self._setup()
|
|
@@ -228,13 +228,23 @@ class PingAlive(utils.PingAlive):
|
|
|
228
228
|
|
|
229
229
|
### DEPRECATED
|
|
230
230
|
class OpenAEVConfigHelper:
|
|
231
|
-
def __init__(self, base_path, variables: Dict):
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
def __init__(self, base_path, variables: Dict | None, config_obj: Configuration):
|
|
232
|
+
if config_obj is not None:
|
|
233
|
+
self.__config_obj = config_obj
|
|
234
|
+
else:
|
|
235
|
+
self.__config_obj = Configuration(
|
|
236
|
+
config_hints=variables,
|
|
237
|
+
config_file_path=os.path.join(
|
|
238
|
+
os.path.dirname(os.path.abspath(base_path)), "config.yml"
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def from_configuration_object(config: Configuration):
|
|
244
|
+
return OpenAEVConfigHelper(None, None, config)
|
|
245
|
+
|
|
246
|
+
def get_config_obj(self) -> Configuration:
|
|
247
|
+
return self.__config_obj
|
|
238
248
|
|
|
239
249
|
def get_conf(self, variable, is_number=None, default=None, required=None):
|
|
240
250
|
result = None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SecurityDomains(Enum):
|
|
5
|
+
ENDPOINT = {"domain_name": "Endpoint", "domain_color": "#389CFF"}
|
|
6
|
+
NETWORK = {"domain_name": "Network", "domain_color": "#009933"}
|
|
7
|
+
WEB_APP = {"domain_name": "Web App", "domain_color": "#FF9933"}
|
|
8
|
+
EMAIL_INFILTRATION = {
|
|
9
|
+
"domain_name": "E-mail Infiltration",
|
|
10
|
+
"domain_color": "#FF6666",
|
|
11
|
+
}
|
|
12
|
+
DATA_EXFILTRATION = {"domain_name": "Data Exfiltration", "domain_color": "#9933CC"}
|
|
13
|
+
URL_FILTERING = {"domain_name": "URL Filtering", "domain_color": "#66CCFF"}
|
|
14
|
+
CLOUD = {"domain_name": "Cloud", "domain_color": "#9999CC"}
|
|
15
|
+
TABLE_TOP = {"domain_name": "Tabletop", "domain_color": "#FFCC33"}
|
|
16
|
+
TOCLASSIFY = {"domain_name": "To classify", "domain_color": "#FFFFFF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyoaev
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python API client for OpenAEV.
|
|
5
5
|
Author-email: Filigran <contact@filigran.io>
|
|
6
6
|
Maintainer-email: Filigran <contact@filigran.io>
|
|
@@ -26,6 +26,7 @@ Requires-Dist: python-magic-bin<0.5,>=0.4.14; sys_platform == "win32"
|
|
|
26
26
|
Requires-Dist: python_json_logger<3.4.0,>=3.3.0
|
|
27
27
|
Requires-Dist: PyYAML<6.1,>=6.0
|
|
28
28
|
Requires-Dist: pydantic<2.12.0,>=2.11.3
|
|
29
|
+
Requires-Dist: pydantic-settings<2.12.0,>=2.11.0
|
|
29
30
|
Requires-Dist: requests<2.33.0,>=2.32.3
|
|
30
31
|
Requires-Dist: setuptools<80.10.0,>=80.9.0
|
|
31
32
|
Requires-Dist: cachetools<5.6.0,>=5.5.0
|
|
@@ -36,9 +37,9 @@ Requires-Dist: requests-toolbelt<1.1.0,>=1.0.0
|
|
|
36
37
|
Requires-Dist: dataclasses-json<0.7.0,>=0.6.4
|
|
37
38
|
Requires-Dist: thefuzz<0.23,>=0.22
|
|
38
39
|
Provides-Extra: dev
|
|
39
|
-
Requires-Dist: black<25.
|
|
40
|
-
Requires-Dist: build<1.
|
|
41
|
-
Requires-Dist: isort<6.
|
|
40
|
+
Requires-Dist: black<25.12.0,>=25.11.0; extra == "dev"
|
|
41
|
+
Requires-Dist: build<1.4.0,>=1.3.0; extra == "dev"
|
|
42
|
+
Requires-Dist: isort<6.2.0,>=6.1.0; extra == "dev"
|
|
42
43
|
Requires-Dist: types-pytz<2025.3.0.0,>=2025.2.0.20250326; extra == "dev"
|
|
43
44
|
Requires-Dist: pre-commit<4.3.0,>=4.2.0; extra == "dev"
|
|
44
45
|
Requires-Dist: types-python-dateutil<2.10.0,>=2.9.0; extra == "dev"
|
|
@@ -44,6 +44,8 @@ pyoaev/backends/backend.py
|
|
|
44
44
|
pyoaev/backends/protocol.py
|
|
45
45
|
pyoaev/configuration/__init__.py
|
|
46
46
|
pyoaev/configuration/configuration.py
|
|
47
|
+
pyoaev/configuration/connector_config_schema_generator.py
|
|
48
|
+
pyoaev/configuration/settings_loader.py
|
|
47
49
|
pyoaev/configuration/sources.py
|
|
48
50
|
pyoaev/contracts/__init__.py
|
|
49
51
|
pyoaev/contracts/contract_builder.py
|
|
@@ -53,6 +55,8 @@ pyoaev/contracts/variable_helper.py
|
|
|
53
55
|
pyoaev/daemons/__init__.py
|
|
54
56
|
pyoaev/daemons/base_daemon.py
|
|
55
57
|
pyoaev/daemons/collector_daemon.py
|
|
58
|
+
pyoaev/security_domain/__init__.py
|
|
59
|
+
pyoaev/security_domain/types.py
|
|
56
60
|
pyoaev/signatures/__init__.py
|
|
57
61
|
pyoaev/signatures/signature_match.py
|
|
58
62
|
pyoaev/signatures/signature_type.py
|
|
@@ -3,6 +3,7 @@ pika<1.4.0,>=1.3.0
|
|
|
3
3
|
python_json_logger<3.4.0,>=3.3.0
|
|
4
4
|
PyYAML<6.1,>=6.0
|
|
5
5
|
pydantic<2.12.0,>=2.11.3
|
|
6
|
+
pydantic-settings<2.12.0,>=2.11.0
|
|
6
7
|
requests<2.33.0,>=2.32.3
|
|
7
8
|
setuptools<80.10.0,>=80.9.0
|
|
8
9
|
cachetools<5.6.0,>=5.5.0
|
|
@@ -20,9 +21,9 @@ python-magic<0.5,>=0.4.27
|
|
|
20
21
|
python-magic-bin<0.5,>=0.4.14
|
|
21
22
|
|
|
22
23
|
[dev]
|
|
23
|
-
black<25.
|
|
24
|
-
build<1.
|
|
25
|
-
isort<6.
|
|
24
|
+
black<25.12.0,>=25.11.0
|
|
25
|
+
build<1.4.0,>=1.3.0
|
|
26
|
+
isort<6.2.0,>=6.1.0
|
|
26
27
|
types-pytz<2025.3.0.0,>=2025.2.0.20250326
|
|
27
28
|
pre-commit<4.3.0,>=4.2.0
|
|
28
29
|
types-python-dateutil<2.10.0,>=2.9.0
|
|
@@ -32,6 +32,7 @@ dependencies = [
|
|
|
32
32
|
"python_json_logger (>=3.3.0,<3.4.0)",
|
|
33
33
|
"PyYAML (>=6.0,<6.1)",
|
|
34
34
|
"pydantic (>=2.11.3,<2.12.0)",
|
|
35
|
+
"pydantic-settings (>=2.11.0,<2.12.0)",
|
|
35
36
|
"requests (>=2.32.3,<2.33.0)",
|
|
36
37
|
"setuptools (>=80.9.0,<80.10.0)",
|
|
37
38
|
"cachetools (>=5.5.0,<5.6.0)",
|
|
@@ -46,9 +47,9 @@ dependencies = [
|
|
|
46
47
|
|
|
47
48
|
[project.optional-dependencies]
|
|
48
49
|
dev = [
|
|
49
|
-
"black (>=25.
|
|
50
|
-
"build (>=1.
|
|
51
|
-
"isort (>=6.
|
|
50
|
+
"black (>=25.11.0,<25.12.0)",
|
|
51
|
+
"build (>=1.3.0,<1.4.0)",
|
|
52
|
+
"isort (>=6.1.0,<6.2.0)",
|
|
52
53
|
"types-pytz (>=2025.2.0.20250326,<2025.3.0.0)",
|
|
53
54
|
"pre-commit (>=4.2.0,<4.3.0)",
|
|
54
55
|
"types-python-dateutil (>=2.9.0,<2.10.0)",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyoaev-2.0.13/test/apis/injector_contract → pyoaev-2.1.0/test/apis/expectation}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|