Skip to content

Commit

Permalink
Adding support for GET /api/v1/vectors endpoint (#818)
Browse files Browse the repository at this point in the history
* Vectors

* Vectors

* Vectors

* Add test_vectors_api.py

* Add import

* Finalize VectorIterator

* Finish tests

* Style

* Style

* remove print

* FIx
  • Loading branch information
naumraviz authored Jul 23, 2024
1 parent 121ce0c commit c66a00b
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/api/apa/vectors.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. automodule:: tenable.apa.vectors.api
10 changes: 10 additions & 0 deletions tenable/apa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
:glob:
findings
vectors
"""

from typing import Optional

from tenable.base.platform import APIPlatform
from .findings.api import FindingsAPI
from .vectors.api import VectorsAPI


class TenableAPA(APIPlatform):
Expand Down Expand Up @@ -65,3 +67,11 @@ def findings(self):
:doc:`Tenable Attack Path Analysis APA Findings APIs <findings>`.
"""
return FindingsAPI(self)

@property
def vectors(self):
"""
The interface object for the
:doc:`Tenable Attack Path Analysis APA Findings APIs <findings>`.
"""
return VectorsAPI(self)
4 changes: 1 addition & 3 deletions tenable/apa/findings/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ def list(
linked above, however some examples are as such:
- ``{"operator":"==", "key":"state", "value":"open"}``
- ``{"operator":">",
"key":"last_updated_at",
"value":"2024-05-30T12:28:11.528118"}``
- ``{"operator":">", "key":"last_updated_at", "value":"2024-05-30T12:28:11.528118"}``
sort_filed (optional, str):
The field you want to use to sort the results by.
Expand Down
Empty file added tenable/apa/vectors/__init__.py
Empty file.
124 changes: 124 additions & 0 deletions tenable/apa/vectors/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Vectors
=============
Methods described in this section relate to the vectors API.
These methods can be accessed at ``TenableAPA.vectors``.
.. rst-class:: hide-signature
.. autoclass:: VectorsAPI
:members:
"""

from copy import copy
from typing import Dict, Optional, Union

from restfly import APIIterator

from tenable.apa.vectors.schema import VectorsPageSchema
from tenable.base.endpoint import APIEndpoint


class VectorIterator(APIIterator):
"""
Vector Iterator
"""

_next_page: str = None
_payload: Dict

def _get_page(self) -> None:
"""
Request the next page of data
"""
payload = copy(self._payload)
payload["pageNumber"] = self._next_page

resp = self._api.get("apa/api/discover/v1/vectors",
params=payload, box=True)
self._next_page = resp.get("pageNumber") + 1
self.page = resp.data
self.total = resp.get("total")


class VectorsAPI(APIEndpoint):
_schema = VectorsPageSchema()

def list(
self,
page_number: Optional[int] = None,
limit: int = 10,
filter: Optional[dict] = None,
sort_field: Optional[str] = None,
sort_order: Optional[str] = None,
return_iterator=True,
) -> Union[VectorIterator, VectorsPageSchema]:
"""
Retrieve vectors
Args:
page_number (optional, int):
For offset-based pagination, the requested page to retrieve.
If this parameter is omitted,
Tenable uses the default value of 1.
limit (optional, int):
The number of records to retrieve.
If this parameter is omitted,
Tenable uses the default value of 25.
The maximum number of events that can be retrieved is 25.
For example: limit=25.
filter (optional, dict):
A document as defined by Tenable APA online documentation.
Filters to allow the user to get
to a specific subset of Findings.
For a more detailed listing of what filters are available,
please refer to the API documentation
linked above, however some examples are as such:
- ``{"operator":"==", "key":"name", "value":"nice name"}``
- ``{"operator":">", "key":"critical_asset", "value": 10}``
sort_field (optional, str):
The field you want to use to sort the results by.
Accepted values are ``name``, ``priority``
sort_order (optional, str):
The sort order
Accepted values are ``desc`` or ``acs``
return_iterator (bool, optional):
Should we return the response instead of iterable?
Returns:
:obj:`VectorsIterator`:
List of vectors records
Examples:
>>> vectors = tapa.vectors.list()
>>> for f in vectors:
... pprint(f)
Examples:
>>> tapa.vectors.list(
... limit='10',
... sort_field='name',
... sort_order='desc',
... filter={"operator":"==", "key":"name", "value":"nice name"},
... return_iterator=False
... )
"""

payload = {
"pageNumber": page_number,
"limit": limit,
"filter": filter,
"sort_field": sort_field,
"sort_order": sort_order}
if return_iterator:
return VectorIterator(self._api, _payload=payload)
return self._schema.load(
self._get(path="apa/api/discover/v1/vectors", params=payload)
)
51 changes: 51 additions & 0 deletions tenable/apa/vectors/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from marshmallow import fields, Schema, validates_schema, ValidationError


class SourceInformationSchema(Schema):
provider_detection_id = fields.Str(allow_none=True)
detection_code = fields.Str(allow_none=True)
reason_code_name = fields.Str(allow_none=True)
asset_id = fields.Str(allow_none=True)
id = fields.Str(allow_none=True)
provider_code = fields.Str(allow_none=True)
type = fields.Str(allow_none=True)
reason_id = fields.Str(allow_none=True)
plugin_name = fields.Str(allow_none=True)


class TechniqueSchema(Schema):
source_information = fields.Str(allow_none=True)
name = fields.Str(allow_none=True)
fullName = fields.Str(allow_none=True)
asset_id = fields.Str(allow_none=True)
id = fields.Int(allow_none=True)
labels = fields.List(fields.Str(), allow_none=True)
procedure_uuid = fields.Str(allow_none=True)


class NodeSchema(Schema):
name = fields.Str(allow_none=True)
fullName = fields.Str(allow_none=True)
asset_id = fields.Str(allow_none=True)
id = fields.Int(allow_none=True)
labels = fields.List(fields.Str(), allow_none=True)


class VectorSchema(Schema):
isNew = fields.Bool(allow_none=True)
vectorId = fields.Str(allow_none=True)
path = fields.Raw(allow_none=True)
techniques = fields.List(fields.Nested(TechniqueSchema), allow_none=True)
nodes = fields.List(fields.Nested(NodeSchema), allow_none=True)
findingsNames = fields.List(fields.Str(), allow_none=True)
name = fields.Str(allow_none=True)
summary = fields.Str(allow_none=True)
firstAES = fields.Raw(allow_none=True)
lastACR = fields.Int(allow_none=True)


class VectorsPageSchema(Schema):
data = fields.List(fields.Nested(VectorSchema), allow_none=True)
pageNumber = fields.Int(allow_none=True)
count = fields.Int(allow_none=True)
total = fields.Int(allow_none=True)
140 changes: 140 additions & 0 deletions tests/apa/vectors/test_vectors_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import pytest
import responses

from tenable.apa.vectors.api import VectorIterator
from tenable.apa.vectors.schema import VectorsPageSchema


@pytest.fixture
def vector():
return {
"isNew": False,
"vectorId": "FFF93960363C0755F8C9D93E241DD26E",
"path": None,
"techniques": [
{
"source_information": "[{\"provider_detection_id\":\"71246\",\"detection_code\":\"Enumerate Local Group Memberships\",\"reason_code_name\":null,\"asset_id\":\"0bd1382d-8ba7-41d7-bc27-0e874c655737\",\"id\":\"nessus:71246\",\"provider_code\":\"NESSUS\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"44401\",\"detection_code\":\"Microsoft Windows SMB Service Config Enumeration\",\"reason_code_name\":null,\"asset_id\":\"0bd1382d-8ba7-41d7-bc27-0e874c655737\",\"id\":\"nessus:44401\",\"provider_code\":\"NESSUS\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"64582\",\"detection_code\":\"Netstat Connection Information\",\"reason_code_name\":null,\"asset_id\":\"0bd1382d-8ba7-41d7-bc27-0e874c655737\",\"id\":\"nessus:64582\",\"provider_code\":\"NESSUS\",\"type\":\"nessus plugin\",\"reason_id\":null}]",
"name": "Remote Desktop Protocol-251714",
"fullName": "Remote Desktop Protocol-251714",
"asset_id": "",
"id": 82656,
"labels": [
"Procedure"
],
"procedure_uuid": "b5277de3-2f05-4cf0-96a3-6764c3d230e1"
},
{
"source_information": "[{\"provider_detection_id\":\"64582\",\"detection_code\":\"Netstat Connection Information\",\"reason_code_name\":null,\"asset_id\":\"fa6ed6d3-9426-4f7e-b414-373eace37f5f\",\"id\":\"nessus:64582\",\"provider_code\":\"NESSUS\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"191947\",\"detection_code\":null,\"reason_code_name\":null,\"asset_id\":\"fa6ed6d3-9426-4f7e-b414-373eace37f5f\",\"id\":\"nessus:191947\",\"provider_code\":\"NESSUS\",\"plugin_name\":\"\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"191947\",\"detection_code\":null,\"reason_code_name\":null,\"asset_id\":\"fa6ed6d3-9426-4f7e-b414-373eace37f5f\",\"id\":\"nessus:191947\",\"provider_code\":\"NESSUS\",\"plugin_name\":\"KB5035857: Windows 2022 / Azure Stack HCI 22H2 Security Update (March 2024)\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"CVE-2024-21444\",\"detection_code\":\"CVE-2024-21444\",\"reason_code_name\":null,\"id\":\"CVE-2024-21444\",\"provider_code\":\"NVD\",\"type\":\"CVE\",\"reason_id\":null}]",
"name": "Exploitation of Remote Services-20940:251097",
"fullName": "Exploitation of Remote Services-20940:251097",
"asset_id": "",
"id": 117132,
"labels": [
"Procedure"
],
"procedure_uuid": "6b2c9d79-3e10-4a6b-b5c2-0c60c453533b"
},
{
"source_information": "[{\"provider_detection_id\":\"64582\",\"detection_code\":\"Netstat Connection Information\",\"reason_code_name\":null,\"asset_id\":\"037cb20a-5b0a-40d2-b5cc-3306ee005429\",\"id\":\"nessus:64582\",\"provider_code\":\"NESSUS\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"160937\",\"detection_code\":null,\"reason_code_name\":null,\"asset_id\":\"037cb20a-5b0a-40d2-b5cc-3306ee005429\",\"id\":\"nessus:160937\",\"provider_code\":\"NESSUS\",\"plugin_name\":\"\",\"type\":\"nessus plugin\",\"reason_id\":null},{\"provider_detection_id\":\"CVE-2022-26936\",\"detection_code\":\"CVE-2022-26936\",\"reason_code_name\":null,\"id\":\"CVE-2022-26936\",\"provider_code\":\"NVD\",\"type\":\"CVE\",\"reason_id\":null}]",
"name": "Exploitation of Remote Services-12298:19222",
"fullName": "Exploitation of Remote Services-12298:19222",
"asset_id": "",
"id": 27049,
"labels": [
"Procedure"
],
"procedure_uuid": "eb10d48d-9d4d-4ca2-bfa4-3ec6d76cbec5"
}
],
"nodes": [
{
"name": "Domain Users",
"fullName": "APADOMAIN\\domain users",
"asset_id": "",
"id": 252949,
"labels": [
"WindowsObject",
"Domain",
"Group"
]
},
{
"name": "APAENG",
"fullName": "apaeng.apadomain.internal",
"asset_id": "0bd1382d-8ba7-41d7-bc27-0e874c655737",
"id": 251098,
"labels": [
"WindowsObject",
"Domain",
"Computer",
"WindowsServer"
]
},
{
"name": "APADC",
"fullName": "apadc.apadomain.internal",
"asset_id": "fa6ed6d3-9426-4f7e-b414-373eace37f5f",
"id": 251097,
"labels": [
"WindowsObject",
"Domain",
"Computer",
"WindowsServer",
"DomainController"
]
},
{
"name": "baaaaacnet",
"fullName": "baaaaacnet.indegy.local",
"asset_id": "037cb20a-5b0a-40d2-b5cc-3306ee005429",
"id": 19222,
"labels": [
"Computer",
"WindowsServer"
]
}
],
"findingsNames": [],
"name": "Domain Users can reach baaaaacnet by exploiting CVE-2024-21444 and CVE-2022-26936",
"summary": "An attacker can use Domain Users to access baaaaacnet by exploiting two vulnerabilities. First, the attacker exploits CVE-2024-21444 on APAENG to gain access to APADC. Then, the attacker exploits CVE-2022-26936 on APADC to gain access to baaaaacnet. This attack path is possible because Domain Users is a member of Remote Desktop Users, which has remote desktop access to APAENG. This attack path is dangerous because it allows an attacker to gain access to a critical asset, baaaaacnet, by exploiting two vulnerabilities.",
"firstAES": None,
"lastACR": 9
}


@responses.activate
def test_vectors_list_iterator(api, vector):
responses.get('https://cloud.tenable.com/apa/api/discover/v1/vectors',
json={"pageNumber": 1, "count": 10, "total": 21,
"data": [vector for _ in range(10)]},
match=[responses.matchers.query_param_matcher({"limit": 10})])

responses.get('https://cloud.tenable.com/apa/api/discover/v1/vectors',
json={"pageNumber": 2, "count": 10, "total": 21,
"data": [vector for _ in range(10)]},
match=[responses.matchers.query_param_matcher({"limit": 10, "pageNumber": 2})])

responses.get('https://cloud.tenable.com/apa/api/discover/v1/vectors',
json={"pageNumber": 3, "count": 10, "total": 21,
"data": [vector for _ in range(1)]},
match=[responses.matchers.query_param_matcher({"limit": 10, "pageNumber": 3})])

vectors: VectorIterator = api.vectors.list()

for v in vectors:
assert v == vector
assert vectors.total == 21
assert vectors.count == 21


@responses.activate
def test_vectors_list_vector_page_response(api, vector):
vectors_page_response = {"pageNumber": 1, "count": 10, "total": 10,
"data": [vector for _ in range(10)]}
responses.get('https://cloud.tenable.com/apa/api/discover/v1/vectors',
json=vectors_page_response,
match=[responses.matchers.query_param_matcher({"limit": 10})])

vectors_page: VectorsPageSchema = api.vectors.list(return_iterator=False)

assert vectors_page == VectorsPageSchema().load(vectors_page_response)

0 comments on commit c66a00b

Please sign in to comment.