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.
- docs/conf.py +65 -0
- pyoaev/__init__.py +26 -0
- pyoaev/_version.py +6 -0
- pyoaev/apis/__init__.py +20 -0
- pyoaev/apis/attack_pattern.py +28 -0
- pyoaev/apis/collector.py +29 -0
- pyoaev/apis/cve.py +18 -0
- pyoaev/apis/document.py +29 -0
- pyoaev/apis/endpoint.py +38 -0
- pyoaev/apis/inject.py +29 -0
- pyoaev/apis/inject_expectation/__init__.py +1 -0
- pyoaev/apis/inject_expectation/inject_expectation.py +118 -0
- pyoaev/apis/inject_expectation/model/__init__.py +7 -0
- pyoaev/apis/inject_expectation/model/expectation.py +173 -0
- pyoaev/apis/inject_expectation_trace.py +36 -0
- pyoaev/apis/injector.py +26 -0
- pyoaev/apis/injector_contract.py +56 -0
- pyoaev/apis/inputs/__init__.py +0 -0
- pyoaev/apis/inputs/search.py +72 -0
- pyoaev/apis/kill_chain_phase.py +22 -0
- pyoaev/apis/me.py +17 -0
- pyoaev/apis/organization.py +11 -0
- pyoaev/apis/payload.py +27 -0
- pyoaev/apis/security_platform.py +33 -0
- pyoaev/apis/tag.py +19 -0
- pyoaev/apis/team.py +25 -0
- pyoaev/apis/user.py +31 -0
- pyoaev/backends/__init__.py +14 -0
- pyoaev/backends/backend.py +136 -0
- pyoaev/backends/protocol.py +32 -0
- pyoaev/base.py +320 -0
- pyoaev/client.py +596 -0
- pyoaev/configuration/__init__.py +3 -0
- pyoaev/configuration/configuration.py +188 -0
- pyoaev/configuration/sources.py +44 -0
- pyoaev/contracts/__init__.py +5 -0
- pyoaev/contracts/contract_builder.py +44 -0
- pyoaev/contracts/contract_config.py +292 -0
- pyoaev/contracts/contract_utils.py +22 -0
- pyoaev/contracts/variable_helper.py +124 -0
- pyoaev/daemons/__init__.py +4 -0
- pyoaev/daemons/base_daemon.py +131 -0
- pyoaev/daemons/collector_daemon.py +91 -0
- pyoaev/exceptions.py +219 -0
- pyoaev/helpers.py +451 -0
- pyoaev/mixins.py +242 -0
- pyoaev/signatures/__init__.py +0 -0
- pyoaev/signatures/signature_match.py +12 -0
- pyoaev/signatures/signature_type.py +51 -0
- pyoaev/signatures/types.py +17 -0
- pyoaev/utils.py +211 -0
- pyoaev-1.18.20.dist-info/METADATA +134 -0
- pyoaev-1.18.20.dist-info/RECORD +72 -0
- pyoaev-1.18.20.dist-info/WHEEL +5 -0
- pyoaev-1.18.20.dist-info/licenses/LICENSE +201 -0
- pyoaev-1.18.20.dist-info/top_level.txt +4 -0
- scripts/release.py +127 -0
- test/__init__.py +0 -0
- test/apis/__init__.py +0 -0
- test/apis/expectation/__init__.py +0 -0
- test/apis/expectation/test_expectation.py +338 -0
- test/apis/injector_contract/__init__.py +0 -0
- test/apis/injector_contract/test_injector_contract.py +58 -0
- test/configuration/__init__.py +0 -0
- test/configuration/test_configuration.py +257 -0
- test/configuration/test_sources.py +69 -0
- test/daemons/__init__.py +0 -0
- test/daemons/test_base_daemon.py +109 -0
- test/daemons/test_collector_daemon.py +39 -0
- test/signatures/__init__.py +0 -0
- test/signatures/test_signature_match.py +25 -0
- 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
pyoaev/apis/__init__.py
ADDED
|
@@ -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
|
pyoaev/apis/collector.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
|
+
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
|
pyoaev/apis/document.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 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
|
pyoaev/apis/endpoint.py
ADDED
|
@@ -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,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
|
pyoaev/apis/injector.py
ADDED
|
@@ -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
|
+
)
|