-
Notifications
You must be signed in to change notification settings - Fork 174
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding support for GET /api/v1/vectors endpoint (#818)
* Vectors * Vectors * Vectors * Add test_vectors_api.py * Add import * Finalize VectorIterator * Finish tests * Style * Style * remove print * FIx
- Loading branch information
Showing
7 changed files
with
327 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.. automodule:: tenable.apa.vectors.api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |