pyoaev 1.18.20__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.
Files changed (72) hide show
  1. docs/conf.py +65 -0
  2. pyoaev/__init__.py +26 -0
  3. pyoaev/_version.py +6 -0
  4. pyoaev/apis/__init__.py +20 -0
  5. pyoaev/apis/attack_pattern.py +28 -0
  6. pyoaev/apis/collector.py +29 -0
  7. pyoaev/apis/cve.py +18 -0
  8. pyoaev/apis/document.py +29 -0
  9. pyoaev/apis/endpoint.py +38 -0
  10. pyoaev/apis/inject.py +29 -0
  11. pyoaev/apis/inject_expectation/__init__.py +1 -0
  12. pyoaev/apis/inject_expectation/inject_expectation.py +118 -0
  13. pyoaev/apis/inject_expectation/model/__init__.py +7 -0
  14. pyoaev/apis/inject_expectation/model/expectation.py +173 -0
  15. pyoaev/apis/inject_expectation_trace.py +36 -0
  16. pyoaev/apis/injector.py +26 -0
  17. pyoaev/apis/injector_contract.py +56 -0
  18. pyoaev/apis/inputs/__init__.py +0 -0
  19. pyoaev/apis/inputs/search.py +72 -0
  20. pyoaev/apis/kill_chain_phase.py +22 -0
  21. pyoaev/apis/me.py +17 -0
  22. pyoaev/apis/organization.py +11 -0
  23. pyoaev/apis/payload.py +27 -0
  24. pyoaev/apis/security_platform.py +33 -0
  25. pyoaev/apis/tag.py +19 -0
  26. pyoaev/apis/team.py +25 -0
  27. pyoaev/apis/user.py +31 -0
  28. pyoaev/backends/__init__.py +14 -0
  29. pyoaev/backends/backend.py +136 -0
  30. pyoaev/backends/protocol.py +32 -0
  31. pyoaev/base.py +320 -0
  32. pyoaev/client.py +596 -0
  33. pyoaev/configuration/__init__.py +3 -0
  34. pyoaev/configuration/configuration.py +188 -0
  35. pyoaev/configuration/sources.py +44 -0
  36. pyoaev/contracts/__init__.py +5 -0
  37. pyoaev/contracts/contract_builder.py +44 -0
  38. pyoaev/contracts/contract_config.py +292 -0
  39. pyoaev/contracts/contract_utils.py +22 -0
  40. pyoaev/contracts/variable_helper.py +124 -0
  41. pyoaev/daemons/__init__.py +4 -0
  42. pyoaev/daemons/base_daemon.py +131 -0
  43. pyoaev/daemons/collector_daemon.py +91 -0
  44. pyoaev/exceptions.py +219 -0
  45. pyoaev/helpers.py +451 -0
  46. pyoaev/mixins.py +242 -0
  47. pyoaev/signatures/__init__.py +0 -0
  48. pyoaev/signatures/signature_match.py +12 -0
  49. pyoaev/signatures/signature_type.py +51 -0
  50. pyoaev/signatures/types.py +17 -0
  51. pyoaev/utils.py +211 -0
  52. pyoaev-1.18.20.dist-info/METADATA +134 -0
  53. pyoaev-1.18.20.dist-info/RECORD +72 -0
  54. pyoaev-1.18.20.dist-info/WHEEL +5 -0
  55. pyoaev-1.18.20.dist-info/licenses/LICENSE +201 -0
  56. pyoaev-1.18.20.dist-info/top_level.txt +4 -0
  57. scripts/release.py +127 -0
  58. test/__init__.py +0 -0
  59. test/apis/__init__.py +0 -0
  60. test/apis/expectation/__init__.py +0 -0
  61. test/apis/expectation/test_expectation.py +338 -0
  62. test/apis/injector_contract/__init__.py +0 -0
  63. test/apis/injector_contract/test_injector_contract.py +58 -0
  64. test/configuration/__init__.py +0 -0
  65. test/configuration/test_configuration.py +257 -0
  66. test/configuration/test_sources.py +69 -0
  67. test/daemons/__init__.py +0 -0
  68. test/daemons/test_base_daemon.py +109 -0
  69. test/daemons/test_collector_daemon.py +39 -0
  70. test/signatures/__init__.py +0 -0
  71. test/signatures/test_signature_match.py +25 -0
  72. test/signatures/test_signature_type.py +57 -0
@@ -0,0 +1,188 @@
1
+ import os
2
+ import os.path
3
+ from typing import Any, Dict, Optional
4
+
5
+ import yaml
6
+ from pydantic import BaseModel, Field
7
+
8
+ from pyoaev.configuration.sources import DictionarySource, EnvironmentSource
9
+
10
+ CONFIGURATION_TYPES = str | int | bool | Any | None
11
+
12
+
13
+ def is_truthy(value: str) -> bool:
14
+ """Asserts whether a given string signals a "True" value
15
+
16
+ :param value: value to test
17
+ :type value: str
18
+
19
+ :return: whether the string represents True or not.
20
+ :rtype: bool
21
+ """
22
+ return value.lower() in ["yes", "true"]
23
+
24
+
25
+ def is_falsy(value: str) -> bool:
26
+ """Asserts whether a given string signals a "False" value
27
+
28
+ :param value: value to test
29
+ :type value: str
30
+
31
+ :return: whether the string represents False or not.
32
+ :rtype: bool
33
+ """
34
+ return value.lower() in ["no", "false"]
35
+
36
+
37
+ class ConfigurationHint(BaseModel):
38
+ """An individual configuration hint. This allows for specifying
39
+ where any given configuration key can be found, in env vars,
40
+ config files. Additionally, it may define a default value or
41
+ a discrete override value.
42
+ """
43
+
44
+ data: Optional[CONFIGURATION_TYPES] = Field(default=None)
45
+ """Override value; when set, getting the configuration value for
46
+ the key described in this instance returns this value.
47
+ """
48
+ env: Optional[str] = Field(default=None)
49
+ """Defines which env var should be read for getting the value
50
+ """
51
+ file_path: Optional[list[str]] = Field(default=None)
52
+ """Defines a JSON path (nested keys) to follow in the provided
53
+ config file for reaching the value.
54
+
55
+ Example: ["toplevel", "subkey"] will hint for searching for
56
+ the config key at { "toplevel": { "subkey": { "config_key"}}
57
+ """
58
+ is_number: Optional[bool] = Field(default=False)
59
+ """Hints at whteher the configuration value should be
60
+ interpreted as a number.
61
+ """
62
+ default: Optional[CONFIGURATION_TYPES] = Field(default=None)
63
+ """When defined, provides a default value for whenever none of the
64
+ hinted locations or the data field have a value.
65
+ """
66
+
67
+
68
+ class Configuration:
69
+ """A configuration object providing ways to an interface for getting
70
+ configuration values. It should be provided with a collection of hints
71
+ to enable its behaviour.
72
+
73
+ :param config_hints: a dictionary of hints, for which the key is the
74
+ desired configuration key (e.g. "log_level") and the value is either
75
+ a dictionary of hints (see ConfigurationHint) or a standalone string.
76
+ In the latter case, the string will be interpreted as a default value.
77
+
78
+ Example:
79
+ .. code-block:: python
80
+ {
81
+ "my_config_key": {
82
+ "env" : "MY_CONFIG_VALUE_ENV_VAR",
83
+ "file_path": ["first_level", "second_level"]
84
+ },
85
+ "my_other_config_key: "discrete value"
86
+ }
87
+ :type config_hints: Dict[str, dict | str]
88
+ :param config_values: dictionary of config values to preemptively load into the
89
+ Configuration object. The format of this dictionary should follow the patterns
90
+ chosen in the file_path property of ConfigurationHint object passed
91
+ as config_hints, defaults to None
92
+
93
+ Example:
94
+ .. code-block:: python
95
+ {
96
+ "first_level": {
97
+ "second_level": {
98
+ "my_config_key": "some value"
99
+ }
100
+ }
101
+ }
102
+ :type config_values: dict (json), optional
103
+ :param config_file_path: path to the configuration file. The file should
104
+ contain a json structure that matches the format of the config_values param,
105
+ defaults to './config.yml' (relative path).
106
+ :type config_file_path: str
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ config_hints: Dict[str, dict | str],
112
+ config_values: dict = None,
113
+ config_file_path: str = os.path.join(os.curdir, "config.yml"),
114
+ ):
115
+ self.__config_hints = {
116
+ key: (
117
+ ConfigurationHint(**value)
118
+ if isinstance(value, dict)
119
+ else ConfigurationHint(**{"default": value})
120
+ )
121
+ for key, value in config_hints.items()
122
+ }
123
+
124
+ file_contents = (
125
+ yaml.load(open(config_file_path), Loader=yaml.FullLoader)
126
+ if os.path.isfile(config_file_path)
127
+ else {}
128
+ )
129
+
130
+ self.__config_values = (config_values or {}) | file_contents
131
+
132
+ def get(self, config_key: str) -> CONFIGURATION_TYPES:
133
+ """Gets the value pointed to by the configuration key. If the key is defined
134
+ with actual hints (as opposed to a discrete value), it will use those hints to
135
+ potentially find a value. If the key was not defined as part of the supplied
136
+ config_hints, this will always return None.
137
+
138
+ :param config_key: the configuration key to search a value for.
139
+ :type config_key: str
140
+
141
+ :return: the value pointed to by the configuration key, or None if not found
142
+ :rtype: CONFIGURATION_TYPES
143
+ """
144
+ config = self.__config_hints.get(config_key)
145
+ if config is None:
146
+ return None
147
+
148
+ return self.__process_value_to_type(
149
+ config.data or self.__dig_config_sources_for_key(config), config.is_number
150
+ )
151
+
152
+ def set(self, config_key: str, value: CONFIGURATION_TYPES):
153
+ """Sets an arbitrary value in the Configuration object, for
154
+ the supplied configuration key, after which any request for the value
155
+ of that key will return this new value.
156
+
157
+ :param config_key: the configuration key to set a value for.
158
+ :type config_key: str
159
+ :param value: the new value to set for the configuration key.
160
+ :type value: CONFIGURATION_TYPES
161
+ """
162
+ if config_key not in self.__config_hints:
163
+ self.__config_hints[config_key] = ConfigurationHint(**{"data": value})
164
+ else:
165
+ self.__config_hints[config_key].data = value
166
+
167
+ @staticmethod
168
+ def __process_value_to_type(value: CONFIGURATION_TYPES, is_number_hint: bool):
169
+ if value is None:
170
+ return value
171
+ if isinstance(value, int) or is_number_hint:
172
+ return int(value)
173
+ if isinstance(value, str):
174
+ if is_truthy(value):
175
+ return True
176
+ if is_falsy(value):
177
+ return False
178
+ if len(value) == 0:
179
+ return None
180
+ return value
181
+
182
+ def __dig_config_sources_for_key(
183
+ self, config: ConfigurationHint
184
+ ) -> CONFIGURATION_TYPES:
185
+ result = EnvironmentSource.get(config.env) or DictionarySource.get(
186
+ config.file_path, self.__config_values
187
+ )
188
+ return result or config.default
@@ -0,0 +1,44 @@
1
+ import os
2
+
3
+
4
+ class EnvironmentSource:
5
+ """A utility for fecthing a value in the env vars."""
6
+
7
+ @classmethod
8
+ def get(cls, env_var: str) -> str | None:
9
+ """Gets the value for the specified env var
10
+
11
+ :param env_var: the name of the env var to query
12
+ :type env_var: str
13
+
14
+ :return: value of the env var, or None if not found
15
+ :rtype: str | None
16
+ """
17
+ return os.getenv(env_var)
18
+
19
+
20
+ class DictionarySource:
21
+ """A utility for fetching a value from within a JSON-like (nested dict) structure"""
22
+
23
+ # this is quite hacky
24
+ # it only strictly handles two levels of keys in a dict
25
+ @classmethod
26
+ def get(cls, config_key_path: list[str], source_dict: dict) -> str | None:
27
+ """Gets the value for the specified env var
28
+
29
+ :param config_key_path: the two-level dictionary path to the config key
30
+ :type config_key_path: list[str]
31
+ :param source_dict: JSON-like (nested dict) structure containing config values.
32
+ :type source_dict: dict
33
+
34
+ :return: value for the config key at specified path, or None if not found
35
+ :rtype: str | None
36
+ """
37
+ assert (
38
+ isinstance(config_key_path, list)
39
+ and len(config_key_path) == 2
40
+ and all([len(path_part) > 0 for path_part in config_key_path])
41
+ )
42
+ return source_dict.get(config_key_path[0], {config_key_path[1]: None}).get(
43
+ config_key_path[1]
44
+ )
@@ -0,0 +1,5 @@
1
+ from .contract_builder import ContractBuilder # noqa: F401,F403
2
+
3
+ __all__ = [
4
+ "ContractBuilder",
5
+ ]
@@ -0,0 +1,44 @@
1
+ from typing import List
2
+
3
+ from pyoaev.contracts.contract_config import ContractElement, ContractOutputElement
4
+
5
+
6
+ class ContractBuilder:
7
+ fields: List[ContractElement]
8
+ outputs: List[ContractOutputElement]
9
+
10
+ def __init__(self):
11
+ self.fields = []
12
+ self.outputs = []
13
+
14
+ def add_fields(self, fields: List[ContractElement]):
15
+ self.fields = self.fields + fields
16
+ return self
17
+
18
+ def add_outputs(self, outputs: List[ContractOutputElement]):
19
+ self.outputs = self.outputs + outputs
20
+ return self
21
+
22
+ def mandatory(self, element: ContractElement):
23
+ element.mandatory = True
24
+ self.fields.append(element)
25
+ return self
26
+
27
+ def optional(self, element: ContractElement):
28
+ element.mandatory = False
29
+ self.fields.append(element)
30
+ return self
31
+
32
+ def mandatory_group(self, elements: List[ContractElement]):
33
+ keys: List[str] = list(map(lambda iterable: iterable.key, elements))
34
+ for element in elements:
35
+ element.mandatory = True
36
+ element.mandatoryGroups = keys
37
+ self.fields.append(element)
38
+ return self
39
+
40
+ def build_fields(self) -> List[ContractElement]:
41
+ return self.fields
42
+
43
+ def build_outputs(self) -> List[ContractOutputElement]:
44
+ return self.outputs
@@ -0,0 +1,292 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Dict, List
6
+
7
+ from pyoaev import utils
8
+ from pyoaev.contracts.contract_utils import ContractCardinality, ContractVariable
9
+ from pyoaev.contracts.variable_helper import VariableHelper
10
+
11
+
12
+ class SupportedLanguage(str, Enum):
13
+ fr: str = "fr"
14
+ en: str = "en"
15
+
16
+
17
+ class ContractFieldType(str, Enum):
18
+ Text: str = "text"
19
+ Number: str = "number"
20
+ Tuple: str = "tuple"
21
+ Checkbox: str = "checkbox"
22
+ Textarea: str = "textarea"
23
+ Select: str = "select"
24
+ Article: str = "article"
25
+ Challenge: str = "challenge"
26
+ DependencySelect: str = "dependency-select"
27
+ Attachment: str = "attachment"
28
+ Team: str = "team"
29
+ Expectation: str = "expectation"
30
+ Asset: str = "asset"
31
+ AssetGroup: str = "asset-group"
32
+ Payload: str = "payload"
33
+
34
+
35
+ class ContractOutputType(str, Enum):
36
+ Text: str = "text"
37
+ Number: str = "number"
38
+ Port: str = "port"
39
+ PortsScan: str = "portscan"
40
+ IPv4: str = "ipv4"
41
+ IPv6: str = "ipv6"
42
+ CVE: str = "cve"
43
+
44
+
45
+ class ExpectationType(str, Enum):
46
+ text: str = "TEXT"
47
+ document: str = "DOCUMENT"
48
+ article: str = "ARTICLE"
49
+ challenge: str = "CHALLENGE"
50
+ manual: str = "MANUAL"
51
+ prevention: str = "PREVENTION"
52
+ detection: str = "DETECTION"
53
+ vulnerability: str = "VULNERABILITY"
54
+
55
+
56
+ @dataclass
57
+ class Expectation:
58
+ expectation_type: ExpectationType
59
+ expectation_name: str
60
+ expectation_description: str
61
+ expectation_score: int
62
+ expectation_expectation_group: bool
63
+
64
+
65
+ @dataclass
66
+ class LinkedFieldModel:
67
+ key: str
68
+ type: ContractFieldType
69
+
70
+
71
+ @dataclass
72
+ class ContractElement(ABC):
73
+ key: str
74
+ label: str
75
+ type: str = field(default="", init=False)
76
+ mandatoryGroups: List[str] = field(default_factory=list)
77
+ mandatoryConditionFields: List[str] = field(default_factory=list)
78
+ mandatoryConditionValues: Dict[str, any] = field(default_factory=list)
79
+ visibleConditionFields: List[str] = field(default_factory=list)
80
+ visibleConditionValues: Dict[str, any] = field(default_factory=list)
81
+ linkedFields: List[str] = field(default_factory=list)
82
+ mandatory: bool = False
83
+ readOnly: bool = False
84
+
85
+ @property
86
+ @abstractmethod
87
+ def get_type(self) -> str:
88
+ pass
89
+
90
+ def __post_init__(self):
91
+ self.type = self.get_type
92
+
93
+
94
+ @dataclass
95
+ class ContractCardinalityElement(ContractElement, ABC):
96
+ cardinality: str = ContractCardinality.One
97
+ defaultValue: List[str] = field(default_factory=list)
98
+
99
+
100
+ @dataclass
101
+ class ContractOutputElement(ABC):
102
+ type: str
103
+ field: str
104
+ labels: List[str]
105
+ isFindingCompatible: bool
106
+ isMultiple: bool
107
+
108
+
109
+ @dataclass
110
+ class ContractConfig:
111
+ type: str
112
+ expose: bool
113
+ label: dict[SupportedLanguage, str]
114
+ color_dark: str
115
+ color_light: str
116
+
117
+
118
+ @dataclass
119
+ class Contract:
120
+ contract_id: str
121
+ label: dict[SupportedLanguage, str]
122
+ fields: List[ContractElement]
123
+ outputs: List[ContractOutputElement]
124
+ config: ContractConfig
125
+ manual: bool
126
+ variables: List[ContractVariable] = field(
127
+ default_factory=lambda: [
128
+ VariableHelper.user_variable(),
129
+ VariableHelper.exercise_variable(),
130
+ VariableHelper.team_variable(),
131
+ ]
132
+ + VariableHelper.uri_variables()
133
+ )
134
+ contract_attack_patterns_external_ids: List[str] = field(default_factory=list)
135
+ contract_vulnerability_external_ids: List[str] = field(default_factory=list)
136
+ is_atomic_testing: bool = True
137
+ platforms: List[str] = field(default_factory=list)
138
+ external_id: str = None
139
+
140
+ def add_attack_pattern(self, var: str):
141
+ self.contract_attack_patterns_external_ids.append(var)
142
+
143
+ def add_vulnerability(self, var: str):
144
+ self.contract_vulnerability_external_ids.append(var)
145
+
146
+ def add_variable(self, var: ContractVariable):
147
+ self.variables.append(var)
148
+
149
+ def to_contract_add_input(self, source_id: str):
150
+ return {
151
+ "contract_id": self.contract_id,
152
+ "external_contract_id": self.external_id,
153
+ "injector_id": source_id,
154
+ "contract_manual": self.manual,
155
+ "contract_labels": self.label,
156
+ "contract_attack_patterns_external_ids": self.contract_attack_patterns_external_ids,
157
+ "contract_vulnerability_external_ids": self.contract_vulnerability_external_ids,
158
+ "contract_content": json.dumps(self, cls=utils.EnhancedJSONEncoder),
159
+ "is_atomic_testing": self.is_atomic_testing,
160
+ "contract_platforms": self.platforms,
161
+ }
162
+
163
+ def to_contract_update_input(self):
164
+ return {
165
+ "contract_manual": self.manual,
166
+ "contract_labels": self.label,
167
+ "contract_attack_patterns_external_ids": self.contract_attack_patterns_external_ids,
168
+ "contract_vulnerability_external_ids": self.contract_vulnerability_external_ids,
169
+ "contract_content": json.dumps(self, cls=utils.EnhancedJSONEncoder),
170
+ "is_atomic_testing": self.is_atomic_testing,
171
+ "contract_platforms": self.platforms,
172
+ }
173
+
174
+
175
+ @dataclass
176
+ class ContractTeam(ContractCardinalityElement):
177
+ @property
178
+ def get_type(self) -> str:
179
+ return ContractFieldType.Team.value
180
+
181
+
182
+ @dataclass
183
+ class ContractText(ContractCardinalityElement):
184
+
185
+ defaultValue: str = ""
186
+
187
+ @property
188
+ def get_type(self) -> str:
189
+ return ContractFieldType.Text.value
190
+
191
+
192
+ def prepare_contracts(contracts):
193
+ return list(
194
+ map(
195
+ lambda c: {
196
+ "contract_id": c.contract_id,
197
+ "contract_labels": c.label,
198
+ "contract_attack_patterns_external_ids": c.contract_attack_patterns_external_ids,
199
+ "contract_content": json.dumps(c, cls=utils.EnhancedJSONEncoder),
200
+ "contract_platforms": c.platforms,
201
+ },
202
+ contracts,
203
+ )
204
+ )
205
+
206
+
207
+ @dataclass
208
+ class ContractTuple(ContractCardinalityElement):
209
+ def __post_init__(self):
210
+ super().__post_init__()
211
+ self.cardinality = ContractCardinality.Multiple
212
+
213
+ attachmentKey: str = None
214
+ contractAttachment: bool = attachmentKey is not None
215
+ tupleFilePrefix: str = "file :: "
216
+
217
+ @property
218
+ def get_type(self) -> str:
219
+ return ContractFieldType.Tuple.value
220
+
221
+
222
+ @dataclass
223
+ class ContractTextArea(ContractCardinalityElement):
224
+
225
+ defaultValue: str = ""
226
+ richText: bool = False
227
+
228
+ @property
229
+ def get_type(self) -> str:
230
+ return ContractFieldType.Textarea.value
231
+
232
+
233
+ @dataclass
234
+ class ContractCheckbox(ContractElement):
235
+
236
+ defaultValue: bool = False
237
+
238
+ @property
239
+ def get_type(self) -> str:
240
+ return ContractFieldType.Checkbox.value
241
+
242
+
243
+ @dataclass
244
+ class ContractAttachment(ContractCardinalityElement):
245
+
246
+ @property
247
+ def get_type(self) -> str:
248
+ return ContractFieldType.Attachment.value
249
+
250
+
251
+ @dataclass
252
+ class ContractExpectations(ContractCardinalityElement):
253
+ cardinality = ContractCardinality.Multiple
254
+ predefinedExpectations: List[Expectation] = field(default_factory=list)
255
+
256
+ @property
257
+ def get_type(self) -> str:
258
+ return ContractFieldType.Expectation.value
259
+
260
+
261
+ @dataclass
262
+ class ContractSelect(ContractCardinalityElement):
263
+
264
+ choices: dict[str, str] = None
265
+
266
+ @property
267
+ def get_type(self) -> str:
268
+ return ContractFieldType.Select.value
269
+
270
+
271
+ @dataclass
272
+ class ContractAsset(ContractCardinalityElement):
273
+
274
+ @property
275
+ def get_type(self) -> str:
276
+ return ContractFieldType.Asset.value
277
+
278
+
279
+ @dataclass
280
+ class ContractAssetGroup(ContractCardinalityElement):
281
+
282
+ @property
283
+ def get_type(self) -> str:
284
+ return ContractFieldType.AssetGroup.value
285
+
286
+
287
+ @dataclass
288
+ class ContractPayload(ContractCardinalityElement):
289
+
290
+ @property
291
+ def get_type(self) -> str:
292
+ return ContractFieldType.Payload.value
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ from typing import List
4
+
5
+
6
+ class ContractCardinality(str, Enum):
7
+ One: str = "1"
8
+ Multiple: str = "n"
9
+
10
+
11
+ class VariableType(str, Enum):
12
+ String: str = "String"
13
+ Object: str = "Object"
14
+
15
+
16
+ @dataclass
17
+ class ContractVariable:
18
+ key: str
19
+ label: str
20
+ type: VariableType
21
+ cardinality: ContractCardinality
22
+ children: List["ContractVariable"] = field(default_factory=list)