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
docs/conf.py ADDED
@@ -0,0 +1,65 @@
1
+ # Configuration file for the Sphinx documentation builder.
2
+ #
3
+ # This file only contains a selection of the most common options. For a full
4
+ # list see the documentation:
5
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
6
+
7
+ # -- Path setup --------------------------------------------------------------
8
+
9
+ # If extensions (or modules to document with autodoc) are in another directory,
10
+ # add these directories to sys.path here. If the directory is relative to the
11
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
12
+ #
13
+ import os
14
+ import sys
15
+
16
+ sys.path.insert(0, os.path.abspath(".."))
17
+
18
+
19
+ # -- Project information -----------------------------------------------------
20
+
21
+ project = "OpenAEV client for Python"
22
+ copyright = "2024, Filigran"
23
+ author = "OpenAEV Project"
24
+
25
+ # The full version, including alpha/beta/rc tags
26
+ release = "1.10.1"
27
+
28
+ master_doc = "index"
29
+
30
+ autoapi_modules = {"pyoaev": {"prune": True}}
31
+
32
+ pygments_style = "sphinx"
33
+
34
+ # -- General configuration ---------------------------------------------------
35
+
36
+ # Add any Sphinx extension module names here, as strings. They can be
37
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
38
+ # ones.
39
+ extensions = [
40
+ "sphinx.ext.autodoc",
41
+ "sphinx.ext.inheritance_diagram",
42
+ "autoapi.sphinx",
43
+ "sphinx_autodoc_typehints",
44
+ ]
45
+
46
+ # Add any paths that contain templates here, relative to this directory.
47
+ templates_path = ["_templates"]
48
+
49
+ # List of patterns, relative to source directory, that match files and
50
+ # directories to ignore when looking for source files.
51
+ # This pattern also affects html_static_path and html_extra_path.
52
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
53
+
54
+
55
+ # -- Options for HTML output -------------------------------------------------
56
+
57
+ # The theme to use for HTML and HTML Help pages. See the documentation for
58
+ # a list of builtin themes.
59
+ #
60
+ html_theme = "sphinx_rtd_theme"
61
+
62
+ # Add any paths that contain custom static files (such as style sheets) here,
63
+ # relative to this directory. They are copied after the builtin static files,
64
+ # so a file named "default.css" will overwrite the builtin "default.css".
65
+ html_static_path = ["_static"]
pyoaev/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ __version__ = "1.18.20"
3
+
4
+ from pyoaev._version import ( # noqa: F401
5
+ __author__,
6
+ __copyright__,
7
+ __email__,
8
+ __license__,
9
+ __title__,
10
+ )
11
+ from pyoaev.client import OpenAEV # noqa: F401
12
+ from pyoaev.configuration import * # noqa: F401,F403,F405
13
+ from pyoaev.contracts import * # noqa: F401,F403,F405
14
+ from pyoaev.exceptions import * # noqa: F401,F403,F405
15
+ from pyoaev.signatures import * # noqa: F401,F403,F405
16
+
17
+ __all__ = [
18
+ "__author__",
19
+ "__copyright__",
20
+ "__email__",
21
+ "__license__",
22
+ "__title__",
23
+ "__version__",
24
+ "OpenAEV",
25
+ ]
26
+ __all__.extend(exceptions.__all__) # noqa: F405
pyoaev/_version.py ADDED
@@ -0,0 +1,6 @@
1
+ __author__ = "Filigran team"
2
+ __copyright__ = "Copyright 2025 Filigran"
3
+ __email__ = "contact@filigran.io"
4
+ __license__ = "Apache 2.0"
5
+ __title__ = "python-openaev"
6
+ __version__ = "1.18.20"
@@ -0,0 +1,20 @@
1
+ from .attack_pattern import * # noqa: F401,F403
2
+ from .collector import * # noqa: F401,F403
3
+ from .cve import * # noqa: F401,F403
4
+ from .document import * # noqa: F401,F403
5
+ from .endpoint import * # noqa: F401,F403
6
+ from .inject import * # noqa: F401,F403
7
+ from .inject_expectation import * # noqa: F401,F403
8
+ from .inject_expectation_trace import * # noqa: F401,F403
9
+ from .injector import * # noqa: F401,F403
10
+ from .injector_contract import * # noqa: F401,F403
11
+ from .kill_chain_phase import * # noqa: F401,F403
12
+ from .me import * # noqa: F401,F403
13
+ from .organization import * # noqa: F401,F403
14
+ from .payload import * # noqa: F401,F403
15
+ from .security_platform import * # noqa: F401,F403
16
+ from .tag import * # noqa: F401,F403
17
+ from .team import * # noqa: F401,F403
18
+ from .user import * # noqa: F401,F403
19
+
20
+ __all__ = [name for name in dir() if not name.startswith("_")]
@@ -0,0 +1,28 @@
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 AttackPattern(RESTObject):
8
+ _id_attr = "attack_pattern_id"
9
+
10
+
11
+ class AttackPatternManager(RESTManager):
12
+ _path = "/attack_patterns"
13
+ _obj_cls = AttackPattern
14
+
15
+ @exc.on_http_error(exc.OpenAEVUpdateError)
16
+ def upsert(
17
+ self,
18
+ attack_patterns: List[Dict[str, Any]],
19
+ ignore_dependencies: bool = False,
20
+ **kwargs: Any,
21
+ ) -> Dict[str, Any]:
22
+ data = {
23
+ "attack_patterns": attack_patterns,
24
+ "ignore_dependencies": ignore_dependencies,
25
+ }
26
+ path = f"{self.path}/upsert"
27
+ result = self.openaev.http_post(path, post_data=data, **kwargs)
28
+ return result
@@ -0,0 +1,29 @@
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 Collector(RESTObject):
10
+ pass
11
+
12
+
13
+ class CollectorManager(GetMixin, ListMixin, CreateMixin, UpdateMixin, RESTManager):
14
+ _path = "/collectors"
15
+ _obj_cls = Collector
16
+ _create_attrs = RequiredOptional(
17
+ required=(
18
+ "collector_id",
19
+ "collector_name",
20
+ "collector_type",
21
+ "collector_period",
22
+ )
23
+ )
24
+
25
+ @exc.on_http_error(exc.OpenAEVUpdateError)
26
+ def get(self, collector_id: str, **kwargs: Any) -> Dict[str, Any]:
27
+ path = f"{self.path}/" + collector_id
28
+ result = self.openaev.http_get(path, **kwargs)
29
+ return result
pyoaev/apis/cve.py ADDED
@@ -0,0 +1,18 @@
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 Cve(RESTObject):
8
+ _id_attr = "cve_id"
9
+
10
+
11
+ class CveManager(RESTManager):
12
+ _path = "/cves"
13
+
14
+ @exc.on_http_error(exc.OpenAEVUpdateError)
15
+ def upsert(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
16
+ path = f"{self.path}/bulk"
17
+ result = self.openaev.http_post(path, post_data=data, **kwargs)
18
+ return result
@@ -0,0 +1,29 @@
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 Document(RESTObject):
8
+ _id_attr = "document_id"
9
+
10
+
11
+ class DocumentManager(RESTManager):
12
+ _path = "/documents"
13
+ _obj_cls = Document
14
+
15
+ @exc.on_http_error(exc.OpenAEVUpdateError)
16
+ def download(self, document_id: str, **kwargs: Any) -> Dict[str, Any]:
17
+ path = f"{self.path}/" + document_id + "/file"
18
+ result = self.openaev.http_get(path, **kwargs)
19
+ return result
20
+
21
+ @exc.on_http_error(exc.OpenAEVUpdateError)
22
+ def upsert(
23
+ self, document: Dict[str, Any], file: tuple, **kwargs: Any
24
+ ) -> Dict[str, Any]:
25
+ path = f"{self.path}/upsert"
26
+ result = self.openaev.http_post(
27
+ path, post_data=document, files={"file": file}, **kwargs
28
+ )
29
+ return result
@@ -0,0 +1,38 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+ from pyoaev.utils import RequiredOptional
6
+
7
+
8
+ class Endpoint(RESTObject):
9
+ _id_attr = "asset_id"
10
+
11
+
12
+ class EndpointManager(RESTManager):
13
+ _path = "/endpoints"
14
+ _obj_cls = Endpoint
15
+ _create_attrs = RequiredOptional(
16
+ required=(
17
+ "endpoint_hostname",
18
+ "endpoint_platform",
19
+ "endpoint_arch",
20
+ ),
21
+ optional=(
22
+ "endpoint_mac_addresses",
23
+ "endpoint_ips",
24
+ "asset_external_reference",
25
+ ),
26
+ )
27
+
28
+ @exc.on_http_error(exc.OpenAEVUpdateError)
29
+ def get(self, asset_id: str, **kwargs: Any) -> Dict[str, Any]:
30
+ path = f"{self.path}/" + asset_id
31
+ result = self.openaev.http_get(path, **kwargs)
32
+ return result
33
+
34
+ @exc.on_http_error(exc.OpenAEVUpdateError)
35
+ def upsert(self, endpoint: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
36
+ path = f"{self.path}/agentless/upsert"
37
+ result = self.openaev.http_post(path, post_data=endpoint, **kwargs)
38
+ return result
pyoaev/apis/inject.py ADDED
@@ -0,0 +1,29 @@
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 Inject(RESTObject):
8
+ _id_attr = None
9
+
10
+
11
+ class InjectManager(RESTManager):
12
+ _path = "/injects"
13
+ _obj_cls = Inject
14
+
15
+ @exc.on_http_error(exc.OpenAEVUpdateError)
16
+ def execution_callback(
17
+ self, inject_id: str, data: Dict[str, Any], **kwargs: Any
18
+ ) -> Dict[str, Any]:
19
+ path = f"{self.path}/execution/callback/{inject_id}"
20
+ result = self.openaev.http_post(path, post_data=data, **kwargs)
21
+ return result
22
+
23
+ @exc.on_http_error(exc.OpenAEVUpdateError)
24
+ def execution_reception(
25
+ self, inject_id: str, data: Dict[str, Any], **kwargs: Any
26
+ ) -> Dict[str, Any]:
27
+ path = f"{self.path}/execution/reception/{inject_id}"
28
+ result = self.openaev.http_post(path, post_data=data, **kwargs)
29
+ return result
@@ -0,0 +1 @@
1
+ from .inject_expectation import * # noqa: F401,F403
@@ -0,0 +1,118 @@
1
+ from typing import Any, Dict
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.apis.inject_expectation.model import (
5
+ DetectionExpectation,
6
+ ExpectationTypeEnum,
7
+ PreventionExpectation,
8
+ )
9
+ from pyoaev.base import RESTManager, RESTObject
10
+ from pyoaev.mixins import ListMixin, UpdateMixin
11
+ from pyoaev.utils import RequiredOptional
12
+
13
+
14
+ class InjectExpectation(RESTObject):
15
+ _id_attr = "inject_expectation_id"
16
+
17
+
18
+ class InjectExpectationManager(ListMixin, UpdateMixin, RESTManager):
19
+ _path = "/injects/expectations"
20
+ _obj_cls = InjectExpectation
21
+ _update_attrs = RequiredOptional(required=("collector_id", "result", "is_success"))
22
+
23
+ @exc.on_http_error(exc.OpenAEVUpdateError)
24
+ def expectations_assets_for_source(
25
+ self, source_id: str, expiration_time: int = None, **kwargs: Any
26
+ ) -> Dict[str, Any]:
27
+ path = f"{self.path}/assets/" + source_id
28
+ result = self.openaev.http_get(
29
+ path,
30
+ query_data=(
31
+ {"expiration_time": expiration_time} if expiration_time else None
32
+ ),
33
+ **kwargs,
34
+ )
35
+ return result
36
+
37
+ def expectations_models_for_source(self, source_id: str, **kwargs: Any):
38
+ """Returns all expectations from OpenAEV that have had no result yet
39
+ from the source_id (e.g. collector).
40
+
41
+ :param source_id: the identifier of the collector requesting expectations
42
+ :type source_id: str
43
+ :param kwargs: additional data to pass to the endpoint
44
+ :type kwargs: dict, optional
45
+
46
+ :return: a list of expectation objects
47
+ :rtype: list[DetectionExpectation|PreventionExpectation]
48
+ """
49
+ # TODO: we should implement a more clever mechanism to obtain
50
+ # specialised Expectation instances rather than just if/elseing
51
+ # through this list of possibilities.
52
+ expectations = []
53
+ for expectation_dict in self.expectations_assets_for_source(
54
+ source_id=source_id, **kwargs
55
+ ):
56
+ if (
57
+ expectation_dict["inject_expectation_type"]
58
+ == ExpectationTypeEnum.Detection.value
59
+ ):
60
+ expectations.append(
61
+ DetectionExpectation(**expectation_dict, api_client=self)
62
+ )
63
+ elif (
64
+ expectation_dict["inject_expectation_type"]
65
+ == ExpectationTypeEnum.Prevention.value
66
+ ):
67
+ expectations.append(
68
+ PreventionExpectation(**expectation_dict, api_client=self)
69
+ )
70
+ else:
71
+ expectations.append(
72
+ PreventionExpectation(**expectation_dict, api_client=self)
73
+ )
74
+ return expectations
75
+
76
+ @exc.on_http_error(exc.OpenAEVUpdateError)
77
+ def prevention_expectations_for_source(
78
+ self, source_id: str, **kwargs: Any
79
+ ) -> Dict[str, Any]:
80
+ path = f"{self.path}/prevention" + source_id
81
+ result = self.openaev.http_get(path, **kwargs)
82
+ return result
83
+
84
+ @exc.on_http_error(exc.OpenAEVUpdateError)
85
+ def detection_expectations_for_source(
86
+ self, source_id: str, expiration_time: int = None, **kwargs: Any
87
+ ) -> Dict[str, Any]:
88
+ path = f"{self.path}/detection/" + source_id
89
+ result = self.openaev.http_get(
90
+ path,
91
+ query_data=(
92
+ {"expiration_time": expiration_time} if expiration_time else None
93
+ ),
94
+ **kwargs,
95
+ )
96
+ return result
97
+
98
+ @exc.on_http_error(exc.OpenAEVUpdateError)
99
+ def update(
100
+ self,
101
+ inject_expectation_id: str,
102
+ inject_expectation: Dict[str, Any],
103
+ **kwargs: Any,
104
+ ) -> Dict[str, Any]:
105
+ path = f"{self.path}/{inject_expectation_id}"
106
+ result = self.openaev.http_put(path, post_data=inject_expectation, **kwargs)
107
+ return result
108
+
109
+ @exc.on_http_error(exc.OpenAEVUpdateError)
110
+ def bulk_update(
111
+ self,
112
+ inject_expectation_input_by_id: Dict[str, Dict[str, Any]],
113
+ **kwargs: Any,
114
+ ) -> None:
115
+ path = f"{self.path}/bulk"
116
+ self.openaev.http_put(
117
+ path, post_data={"inputs": inject_expectation_input_by_id}, **kwargs
118
+ )
@@ -0,0 +1,7 @@
1
+ from .expectation import (
2
+ DetectionExpectation,
3
+ ExpectationTypeEnum,
4
+ PreventionExpectation,
5
+ )
6
+
7
+ __all__ = ["DetectionExpectation", "ExpectationTypeEnum", "PreventionExpectation"]
@@ -0,0 +1,173 @@
1
+ from enum import Enum
2
+ from typing import List
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel
6
+ from thefuzz import fuzz
7
+
8
+ from pyoaev.signatures.signature_type import SignatureType
9
+ from pyoaev.signatures.types import MatchTypes, SignatureTypes
10
+
11
+
12
+ class ExpectationTypeEnum(str, Enum):
13
+ """Types of Expectations"""
14
+
15
+ Detection = "DETECTION"
16
+ Prevention = "PREVENTION"
17
+ Vulnerability = "VULNERABILITY"
18
+ Other = "other"
19
+
20
+ @classmethod
21
+ def _missing_(cls, value):
22
+ return cls.Other
23
+
24
+
25
+ class ExpectationSignature(BaseModel):
26
+ """An expectation signature describes a known marker potentially
27
+ found in alerting data in security software. For example, an
28
+ expectation signature can be a process image name, a command
29
+ line, or any other relevant piece of data.
30
+ """
31
+
32
+ type: SignatureTypes
33
+ value: str
34
+
35
+
36
+ class Expectation(BaseModel):
37
+ """An expectation represents an expected outcome of a BAS run.
38
+ For example, in the case of running an attack command line, the
39
+ expectation may be that security software has _detected_ it, while
40
+ another expectation may be that the attack was _prevented_.
41
+ """
42
+
43
+ inject_expectation_id: UUID
44
+ inject_expectation_signatures: List[ExpectationSignature]
45
+
46
+ success_label: str = "Success"
47
+ failure_label: str = "Failure"
48
+
49
+ def __init__(self, *a, **kw):
50
+ super().__init__(*a, **kw)
51
+ self.__api_client = kw["api_client"]
52
+
53
+ def update(self, success, sender_id, metadata):
54
+ """Update the expectation object in OpenAEV with the supplied outcome.
55
+
56
+ :param success: whether the expectation was fulfilled (true) or not (false)
57
+ :type success: bool
58
+ :param sender_id: identifier of the collector that is updating the expectation
59
+ :type sender_id: string
60
+ :param metadata: arbitrary dictionary of additional data relevant to updating the expectation
61
+ :type metadata: dict[string,string]
62
+ """
63
+ self.__api_client.update(
64
+ self.inject_expectation_id,
65
+ inject_expectation={
66
+ "collector_id": sender_id,
67
+ "result": (self.success_label if success else self.failure_label),
68
+ "is_success": success,
69
+ "metadata": metadata,
70
+ },
71
+ )
72
+
73
+ def match_alert(self, relevant_signature_types: list[SignatureType], alert_data):
74
+ """Matches an alert's data against the current expectation signatures
75
+ to see if the alert is relevant to the current expectation's inject,
76
+ i.e. this alert was triggered by the execution of the inject to which
77
+ belongs the expectation.
78
+
79
+ :param relevant_signature_types: filter of signature types that we want to consider.
80
+ Only the signature types listed in this collection may be checked for matching.
81
+ :type relevant_signature_types: list[SignatureType]
82
+ :param alert_data: list of possibly relevant markers found in an alert.
83
+ :type alert_data: dict[SignatureTypes, dict]
84
+
85
+ :return: whether the alert matches the expectation signatures or not.
86
+ :rtype: bool
87
+ """
88
+ relevant_expectation_signatures = [
89
+ signature
90
+ for signature in self.inject_expectation_signatures
91
+ if signature.type in [type.label for type in relevant_signature_types]
92
+ ]
93
+ if not any(relevant_expectation_signatures):
94
+ return False
95
+
96
+ for relevant_expectation_signature in relevant_expectation_signatures:
97
+ if not (
98
+ alert_signature_for_type := alert_data.get(
99
+ relevant_expectation_signature.type.value
100
+ )
101
+ ):
102
+ return False
103
+
104
+ if alert_signature_for_type[
105
+ "type"
106
+ ] == MatchTypes.MATCH_TYPE_FUZZY and not self.match_fuzzy(
107
+ alert_signature_for_type["data"],
108
+ relevant_expectation_signature.value,
109
+ alert_signature_for_type["score"],
110
+ ):
111
+ return False
112
+ if alert_signature_for_type[
113
+ "type"
114
+ ] == MatchTypes.MATCH_TYPE_SIMPLE and not self.match_simple(
115
+ alert_signature_for_type["data"], relevant_expectation_signature.value
116
+ ):
117
+ return False
118
+
119
+ return True
120
+
121
+ @staticmethod
122
+ def match_fuzzy(tested: list[str], reference: str, threshold: int):
123
+ """Applies a fuzzy match against a known reference to a list of candidates
124
+
125
+ :param tested: list of strings candidate for fuzzy matching
126
+ :type tested: list[str]
127
+ :param reference: the reference against which to try to fuzzy match
128
+ :type reference: str
129
+ :param threshold: string overlap percentage threshold above which to declare a match
130
+ :type threshold: int
131
+
132
+ :return: whether any of the candidate is a match against the reference
133
+ :rtype: bool
134
+ """
135
+ actual_tested = [tested] if isinstance(tested, str) else tested
136
+ for value in actual_tested:
137
+ ratio = fuzz.ratio(value, reference)
138
+ if ratio >= threshold:
139
+ return True
140
+ return False
141
+
142
+ @staticmethod
143
+ def match_simple(tested: list[str], reference: str):
144
+ """A simple strict, case-sensitive string matching between a list of
145
+ candidates and a reference.
146
+
147
+ :param tested: list of strings candidate for fuzzy matching
148
+ :type tested: list[str]
149
+ :param reference: the reference against which to try to fuzzy match
150
+ :type reference: str
151
+
152
+ :return: whether any of the candidate is a match against the reference
153
+ :rtype: bool
154
+ """
155
+ return Expectation.match_fuzzy(tested, reference, threshold=100)
156
+
157
+
158
+ class DetectionExpectation(Expectation):
159
+ """An expectation that is specific to Detection, i.e. that is used
160
+ by OpenAEV to assert that an inject's execution was detected.
161
+ """
162
+
163
+ success_label: str = "Detected"
164
+ failure_label: str = "Not Detected"
165
+
166
+
167
+ class PreventionExpectation(Expectation):
168
+ """An expectation that is specific to Prevention, i.e. that is used
169
+ by OpenAEV to assert that an inject's execution was prevented.
170
+ """
171
+
172
+ success_label: str = "Prevented"
173
+ failure_label: str = "Not Prevented"
@@ -0,0 +1,36 @@
1
+ from typing import Any, Dict, List
2
+
3
+ from pyoaev import exceptions as exc
4
+ from pyoaev.base import RESTManager, RESTObject
5
+ from pyoaev.mixins import CreateMixin
6
+ from pyoaev.utils import RequiredOptional
7
+
8
+
9
+ class InjectExpectationTrace(RESTObject):
10
+ _id_attr = "inject_expectation_trace_id"
11
+
12
+
13
+ class InjectExpectationTraceManager(CreateMixin, RESTManager):
14
+ _path = "/inject-expectations-traces"
15
+ _obj_cls = InjectExpectationTrace
16
+ _create_attrs = RequiredOptional(
17
+ required=(
18
+ "inject_expectation_trace_expectation",
19
+ "inject_expectation_trace_source_id",
20
+ "inject_expectation_trace_alert_name",
21
+ "inject_expectation_trace_alert_link",
22
+ "inject_expectation_trace_date",
23
+ ),
24
+ )
25
+
26
+ @exc.on_http_error(exc.OpenAEVUpdateError)
27
+ def bulk_create(
28
+ self, payload: Dict[str, List[Dict[str, str]]], **kwargs: Any
29
+ ) -> dict[str, Any]:
30
+ path = f"{self.path}/bulk"
31
+ result = self.openaev.http_post(
32
+ path,
33
+ post_data=payload,
34
+ **kwargs,
35
+ )
36
+ return result
@@ -0,0 +1,26 @@
1
+ from pyoaev.base import RESTManager, RESTObject
2
+ from pyoaev.mixins import CreateMixin, GetMixin, ListMixin, UpdateMixin
3
+ from pyoaev.utils import RequiredOptional
4
+
5
+
6
+ class Injector(RESTObject):
7
+ pass
8
+
9
+
10
+ class InjectorManager(GetMixin, ListMixin, CreateMixin, UpdateMixin, RESTManager):
11
+ _path = "/injectors"
12
+ _obj_cls = Injector
13
+ _create_attrs = RequiredOptional(
14
+ required=(
15
+ "injector_id",
16
+ "injector_name",
17
+ "injector_type",
18
+ "injector_contracts",
19
+ ),
20
+ optional=(
21
+ "injector_custom_contracts",
22
+ "injector_category",
23
+ "injector_executor_commands",
24
+ "injector_executor_clear_commands",
25
+ ),
26
+ )