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,56 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.apis.inputs.search import InjectorContractSearchPaginationInput
5
+ from pyoaev.base import RESTManager, RESTObject
6
+ from pyoaev.mixins import CreateMixin, DeleteMixin, UpdateMixin
7
+ from pyoaev.utils import RequiredOptional
8
+
9
+
10
+ class InjectorContract(RESTObject):
11
+ pass
12
+
13
+
14
+ class InjectorContractManager(CreateMixin, UpdateMixin, DeleteMixin, RESTManager):
15
+ _path = "/injector_contracts"
16
+ _obj_cls = InjectorContract
17
+ _create_attrs = RequiredOptional(
18
+ required=(
19
+ "contract_content",
20
+ "contract_id",
21
+ "contract_labels",
22
+ "injector_id",
23
+ ),
24
+ optional=(
25
+ "contract_attack_patterns_ids",
26
+ "contract_attack_patterns_external_ids",
27
+ "contract_vulnerability_external_ids",
28
+ "contract_manual",
29
+ "contract_platforms",
30
+ "external_contract_id",
31
+ "is_atomic_testing",
32
+ ),
33
+ )
34
+ _update_attrs = RequiredOptional(
35
+ required=(
36
+ "contract_content",
37
+ "contract_labels",
38
+ ),
39
+ optional=(
40
+ "contract_attack_patterns_ids",
41
+ "contract_vulnerability_ids",
42
+ "contract_vulnerability_external_ids",
43
+ "contract_manual",
44
+ "contract_platforms",
45
+ "is_atomic_testing",
46
+ ),
47
+ )
48
+
49
+ @exc.on_http_error(exc.OpenAEVUpdateError)
50
+ def search(
51
+ self, input: InjectorContractSearchPaginationInput, **kwargs: Any
52
+ ) -> Dict[str, Any]:
53
+ path = f"{self.path}/search"
54
+ # force the serialisation here since we only need a naive serialisation to json
55
+ result = self.openaev.http_post(path, post_data=input.to_dict(), **kwargs)
56
+ return result
File without changes
@@ -0,0 +1,72 @@
1
+ from typing import Any, Dict, List
2
+
3
+
4
+ class Filter:
5
+ def __init__(self, key: str, mode: str, operator: str, values: List[str]):
6
+ self.key = key
7
+ self.mode = mode
8
+ self.operator = operator
9
+ self.values = values
10
+
11
+ def to_dict(self) -> dict[str, Any]:
12
+ return {k: v for k, v in self.__dict__.items() if v is not None}
13
+
14
+
15
+ class FilterGroup:
16
+ def __init__(self, mode: str, filters: List[Filter]):
17
+ self.mode = mode
18
+ self.filters = filters
19
+
20
+ def to_dict(self) -> dict[str, Any]:
21
+ dictionary: dict[str, Any] = {"mode": self.mode}
22
+ if self.filters:
23
+ filter_dicts: List[dict[str, Any]] = []
24
+ for filter_ in self.filters:
25
+ filter_dicts.append(filter_.to_dict())
26
+ dictionary["filters"] = filter_dicts
27
+ return dictionary
28
+
29
+
30
+ class SearchPaginationInput:
31
+ def __init__(
32
+ self,
33
+ page: int,
34
+ size: int,
35
+ filter_group: FilterGroup,
36
+ text_search: str,
37
+ sorts: Dict[str, str],
38
+ ):
39
+ self.size = size
40
+ self.page = page
41
+ self.filterGroup = filter_group
42
+ self.text_search = text_search
43
+ self.sorts = sorts
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ dictionary: dict[str, Any] = {"page": self.page, "size": self.size}
47
+ if self.sorts:
48
+ dictionary["sorts"] = self.sorts
49
+ if self.text_search:
50
+ dictionary["textSearch"] = self.text_search
51
+ if self.filterGroup:
52
+ dictionary["filterGroup"] = self.filterGroup.to_dict()
53
+ return dictionary
54
+
55
+
56
+ class InjectorContractSearchPaginationInput(SearchPaginationInput):
57
+ def __init__(
58
+ self,
59
+ page: int,
60
+ size: int,
61
+ filter_group: FilterGroup,
62
+ text_search: str = None,
63
+ sorts: Dict[str, str] = None,
64
+ include_full_details: bool = True,
65
+ ):
66
+ super().__init__(page, size, filter_group, text_search, sorts)
67
+ self.include_full_details = include_full_details
68
+
69
+ def to_dict(self) -> dict[str, Any]:
70
+ dictionary: dict[str, Any] = super().to_dict()
71
+ dictionary["include_full_details"] = self.include_full_details
72
+ return dictionary
@@ -0,0 +1,22 @@
1
+ from typing import Any, Dict, List
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+
6
+
7
+ class KillChainPhase(RESTObject):
8
+ _id_attr = "phase_id"
9
+
10
+
11
+ class KillChainPhaseManager(RESTManager):
12
+ _path = "/kill_chain_phases"
13
+ _obj_cls = KillChainPhase
14
+
15
+ @exc.on_http_error(exc.OpenAEVUpdateError)
16
+ def upsert(
17
+ self, kill_chain_phases: List[Dict[str, Any]], **kwargs: Any
18
+ ) -> Dict[str, Any]:
19
+ data = {"kill_chain_phases": kill_chain_phases}
20
+ path = f"{self.path}/upsert"
21
+ result = self.openaev.http_post(path, post_data=data, **kwargs)
22
+ return result
pyoaev/apis/me.py ADDED
@@ -0,0 +1,17 @@
1
+ from typing import Any, cast
2
+
3
+ from pyoaev.base import RESTManager, RESTObject
4
+ from pyoaev.mixins import GetWithoutIdMixin, UpdateMixin
5
+
6
+
7
+ class Me(RESTObject):
8
+ _id_attr = None
9
+ _repr_attr = "user_email"
10
+
11
+
12
+ class MeManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
13
+ _path = "/me"
14
+ _obj_cls = Me
15
+
16
+ def get(self, **kwargs: Any) -> Me:
17
+ return cast(Me, super().get(**kwargs))
@@ -0,0 +1,11 @@
1
+ from pyoaev.base import RESTManager, RESTObject
2
+ from pyoaev.mixins import ListMixin, UpdateMixin
3
+
4
+
5
+ class Organization(RESTObject):
6
+ pass
7
+
8
+
9
+ class OrganizationManager(ListMixin, UpdateMixin, RESTManager):
10
+ _path = "/organizations"
11
+ _obj_cls = Organization
pyoaev/apis/payload.py ADDED
@@ -0,0 +1,27 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+
6
+
7
+ class Payload(RESTObject):
8
+ _id_attr = "payload_id"
9
+
10
+
11
+ class PayloadManager(RESTManager):
12
+ _path = "/payloads"
13
+ _obj_cls = Payload
14
+
15
+ @exc.on_http_error(exc.OpenAEVUpdateError)
16
+ def upsert(self, payload: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
17
+ path = f"{self.path}/upsert"
18
+ result = self.openaev.http_post(path, post_data=payload, **kwargs)
19
+ return result
20
+
21
+ @exc.on_http_error(exc.OpenAEVUpdateError)
22
+ def deprecate(
23
+ self, payloads_processed: Dict[str, Any], **kwargs: Any
24
+ ) -> Dict[str, Any]:
25
+ path = f"{self.path}/deprecate"
26
+ result = self.openaev.http_post(path, post_data=payloads_processed, **kwargs)
27
+ return result
@@ -0,0 +1,33 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+ from pyoaev.mixins import CreateMixin, GetMixin, ListMixin, UpdateMixin
6
+ from pyoaev.utils import RequiredOptional
7
+
8
+
9
+ class SecurityPlatform(RESTObject):
10
+ pass
11
+
12
+
13
+ class SecurityPlatformManager(
14
+ GetMixin, ListMixin, CreateMixin, UpdateMixin, RESTManager
15
+ ):
16
+ _path = "/security_platforms"
17
+ _obj_cls = SecurityPlatform
18
+ _create_attrs = RequiredOptional(
19
+ required=("asset_name", "security_platform_type"),
20
+ optional=(
21
+ "asset_description",
22
+ "security_platform_logo_light",
23
+ "security_platform_logo_dark",
24
+ ),
25
+ )
26
+
27
+ @exc.on_http_error(exc.OpenAEVUpdateError)
28
+ def upsert(
29
+ self, security_platform: Dict[str, Any], **kwargs: Any
30
+ ) -> Dict[str, Any]:
31
+ path = f"{self.path}/upsert"
32
+ result = self.openaev.http_post(path, post_data=security_platform, **kwargs)
33
+ return result
pyoaev/apis/tag.py ADDED
@@ -0,0 +1,19 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+
6
+
7
+ class Tag(RESTObject):
8
+ _id_attr = "tag_id"
9
+
10
+
11
+ class TagManager(RESTManager):
12
+ _path = "/tags"
13
+ _obj_cls = Tag
14
+
15
+ @exc.on_http_error(exc.OpenAEVUpdateError)
16
+ def upsert(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
17
+ path = f"{self.path}/upsert"
18
+ result = self.openaev.http_post(path, post_data=data, **kwargs)
19
+ return result
pyoaev/apis/team.py ADDED
@@ -0,0 +1,25 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+ from pyoaev.mixins import CreateMixin, ListMixin, UpdateMixin
6
+ from pyoaev.utils import RequiredOptional
7
+
8
+
9
+ class Team(RESTObject):
10
+ _id_attr = "team_id"
11
+
12
+
13
+ class TeamManager(CreateMixin, ListMixin, UpdateMixin, RESTManager):
14
+ _path = "/teams"
15
+ _obj_cls = Team
16
+ _create_attrs = RequiredOptional(
17
+ required=("team_name",),
18
+ optional=("team_description", "team_organization", "team_tags"),
19
+ )
20
+
21
+ @exc.on_http_error(exc.OpenAEVUpdateError)
22
+ def upsert(self, team: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
23
+ path = f"{self.path}/upsert"
24
+ result = self.openaev.http_post(path, post_data=team, **kwargs)
25
+ return result
pyoaev/apis/user.py ADDED
@@ -0,0 +1,31 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+ from pyoaev.mixins import CreateMixin, ListMixin, UpdateMixin
6
+ from pyoaev.utils import RequiredOptional
7
+
8
+
9
+ class User(RESTObject):
10
+ _id_attr = "user_id"
11
+
12
+
13
+ class UserManager(CreateMixin, ListMixin, UpdateMixin, RESTManager):
14
+ _path = "/players"
15
+ _obj_cls = User
16
+ _create_attrs = RequiredOptional(
17
+ required=("user_email",),
18
+ optional=(
19
+ "user_firstname",
20
+ "user_lastname",
21
+ "user_organization",
22
+ "user_country",
23
+ "user_tags",
24
+ ),
25
+ )
26
+
27
+ @exc.on_http_error(exc.OpenAEVUpdateError)
28
+ def upsert(self, user: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
29
+ path = f"{self.path}/upsert"
30
+ result = self.openaev.http_post(path, post_data=user, **kwargs)
31
+ return result
@@ -0,0 +1,14 @@
1
+ """
2
+ Defines http backends for processing http requests
3
+ """
4
+
5
+ from .backend import RequestsBackend, RequestsResponse, TokenAuth
6
+
7
+ DefaultBackend = RequestsBackend
8
+ DefaultResponse = RequestsResponse
9
+
10
+ __all__ = [
11
+ "DefaultBackend",
12
+ "DefaultResponse",
13
+ "TokenAuth",
14
+ ]
@@ -0,0 +1,136 @@
1
+ import dataclasses
2
+ import json
3
+ from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
4
+
5
+ import requests
6
+ from requests import PreparedRequest
7
+ from requests.auth import AuthBase
8
+ from requests.structures import CaseInsensitiveDict
9
+ from requests_toolbelt.multipart.encoder import MultipartEncoder # type: ignore
10
+
11
+ from pyoaev.backends import protocol
12
+
13
+
14
+ class Auth:
15
+ def __init__(self, token: str):
16
+ self.token = token
17
+
18
+
19
+ class TokenAuth(Auth, AuthBase):
20
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
21
+ r.headers["Authorization"] = f"Bearer {self.token}"
22
+ return r
23
+
24
+
25
+ @dataclasses.dataclass
26
+ class SendData:
27
+ content_type: str
28
+ data: Optional[Union[Dict[str, Any], MultipartEncoder]] = None
29
+ json: Optional[Union[Dict[str, Any], bytes]] = None
30
+
31
+ def __post_init__(self) -> None:
32
+ if self.json is not None and self.data is not None:
33
+ raise ValueError(
34
+ f"`json` and `data` are mutually exclusive. Only one can be set. "
35
+ f"json={self.json!r} data={self.data!r}"
36
+ )
37
+
38
+
39
+ class RequestsResponse(protocol.BackendResponse):
40
+ def __init__(self, response: requests.Response) -> None:
41
+ self._response: requests.Response = response
42
+
43
+ @property
44
+ def response(self) -> requests.Response:
45
+ return self._response
46
+
47
+ @property
48
+ def status_code(self) -> int:
49
+ return self._response.status_code
50
+
51
+ @property
52
+ def headers(self) -> CaseInsensitiveDict[str]:
53
+ return self._response.headers
54
+
55
+ @property
56
+ def content(self) -> bytes:
57
+ return self._response.content
58
+
59
+ @property
60
+ def reason(self) -> str:
61
+ return self._response.reason
62
+
63
+ def json(self) -> Any:
64
+ return self._response.json()
65
+
66
+
67
+ class RequestsBackend(protocol.Backend):
68
+ def __init__(self, session: Optional[requests.Session] = None) -> None:
69
+ self._client: requests.Session = session or requests.Session()
70
+
71
+ @property
72
+ def client(self) -> requests.Session:
73
+ return self._client
74
+
75
+ @staticmethod
76
+ def prepare_send_data(
77
+ files: Optional[Dict[str, Any]] = None,
78
+ post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
79
+ raw: bool = False,
80
+ ) -> SendData:
81
+ if files is not None:
82
+ json_data = {"input": (None, json.dumps(post_data), "application/json")}
83
+ post_data = {**files, **json_data}
84
+ multipart_encoder = MultipartEncoder(fields=post_data)
85
+ return SendData(
86
+ data=multipart_encoder, content_type=multipart_encoder.content_type
87
+ )
88
+
89
+ if raw and post_data:
90
+ return SendData(data=post_data, content_type="application/octet-stream")
91
+
92
+ if TYPE_CHECKING:
93
+ assert not isinstance(post_data, BinaryIO)
94
+
95
+ return SendData(json=post_data, content_type="application/json")
96
+
97
+ def http_request(
98
+ self,
99
+ method: str,
100
+ url: str,
101
+ json: Optional[Union[Dict[str, Any], bytes]] = None,
102
+ data: Optional[Union[Dict[str, Any], MultipartEncoder]] = None,
103
+ params: Optional[Any] = None,
104
+ timeout: Optional[float] = None,
105
+ verify: Optional[Union[bool, str]] = True,
106
+ stream: Optional[bool] = False,
107
+ **kwargs: Any,
108
+ ) -> RequestsResponse:
109
+ """Make HTTP request
110
+
111
+ Args:
112
+ method: The HTTP method to call ('get', 'post', 'put', 'delete', etc.)
113
+ url: The full URL
114
+ data: The data to send to the server in the body of the request
115
+ json: Data to send in the body in json by default
116
+ timeout: The timeout, in seconds, for the request
117
+ verify: Whether SSL certificates should be validated. If
118
+ the value is a string, it is the path to a CA file used for
119
+ certificate validation.
120
+ stream: Whether the data should be streamed
121
+
122
+ Returns:
123
+ A requests Response object.
124
+ """
125
+ response: requests.Response = self._client.request(
126
+ method=method,
127
+ url=url,
128
+ params=params,
129
+ data=data,
130
+ timeout=timeout,
131
+ stream=stream,
132
+ verify=verify,
133
+ json=json,
134
+ **kwargs,
135
+ )
136
+ return RequestsResponse(response=response)
@@ -0,0 +1,32 @@
1
+ import abc
2
+ import sys
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ import requests
6
+ from requests_toolbelt.multipart.encoder import MultipartEncoder # type: ignore
7
+
8
+ if sys.version_info >= (3, 8):
9
+ from typing import Protocol
10
+ else:
11
+ from typing_extensions import Protocol
12
+
13
+
14
+ class BackendResponse(Protocol):
15
+ @abc.abstractmethod
16
+ def __init__(self, response: requests.Response) -> None: ...
17
+
18
+
19
+ class Backend(Protocol):
20
+ @abc.abstractmethod
21
+ def http_request(
22
+ self,
23
+ method: str,
24
+ url: str,
25
+ json: Optional[Union[Dict[str, Any], bytes]],
26
+ data: Optional[Union[Dict[str, Any], MultipartEncoder]],
27
+ params: Optional[Any],
28
+ timeout: Optional[float],
29
+ verify: Optional[Union[bool, str]],
30
+ stream: Optional[bool],
31
+ **kwargs: Any,
32
+ ) -> BackendResponse: ...