Skip to content

Commit 1ddf2fc

Browse files
authored
Tenable OT Security Export Helper sub-pkg (#742)
* Added Export’s sub-pkg with updated queries.
1 parent 43cbed0 commit 1ddf2fc

File tree

7 files changed

+475
-8
lines changed

7 files changed

+475
-8
lines changed

docs/api/ot/exports.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.. automodule:: tenable.ot.exports.api

docs/api/ot/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Tenable OT Security
55
:glob:
66

77
graphql
8+
exports
89
schema
910

1011
.. automodule:: tenable.ot

tenable/ot/exports/__init__.py

Whitespace-only changes.

tenable/ot/exports/api.py

+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""
2+
Data Export Helper
3+
==================
4+
5+
The methods exposed within the export module are designed to mimick the same
6+
structure that Tenable Vulnerability Management uses for exporting data.
7+
These methods ultimately translate to GraphQL queries and then are fed to the
8+
Tenable OT API.
9+
10+
.. rst-class:: hide-signature
11+
.. autoclass:: ExportsAPI
12+
:members:
13+
"""
14+
from typing import List, Dict, Optional, Union, Any
15+
from tenable.base.endpoint import APIEndpoint
16+
from tenable.ot.exports.iterator import OTExportsIterator, OTFindingsIterator
17+
from tenable.ot.exports import queries
18+
19+
20+
class ExportsAPI(APIEndpoint):
21+
def _list(self,
22+
query: str,
23+
model: str,
24+
limit: int = 200,
25+
return_json: bool = False,
26+
filter_type: str = 'And',
27+
filters: Optional[List[Dict]] = None,
28+
default_filters: Optional[List[Dict]] = None,
29+
sort: Optional[List[Dict]] = None,
30+
search: Optional[str] = None,
31+
start_at: Optional[str] = None,
32+
iterable: Optional[Any] = OTExportsIterator,
33+
**kwargs
34+
) -> Any:
35+
"""
36+
Base listing method to be used for the exports.
37+
38+
Args:
39+
query (str): The GraphQL Query to run
40+
model (str):
41+
The GraphQL Model that is to be returned from the API. This
42+
name is what is used by the iterator to traverse the data
43+
page.
44+
limit (int, optional):
45+
The number of objects to be returned per page.
46+
sort (list[dict], optional):
47+
A list of sort parameters to be passed to sort the responses.
48+
search (str, optional):
49+
Search string
50+
start_at (str, optional):
51+
Start returning data after this object id.
52+
filters (list[dict], optional):
53+
List of filters to apply to restict the response to only the
54+
desired items.
55+
filter_type (str, optional):
56+
When passing multiple filters, how should the filters be
57+
applied to the dataset? Acceptable values are `And` and `Or`.
58+
return_json (bool, optional):
59+
If `True`, then the instead of an iterator, the json response
60+
will be returned instead.
61+
default_filters (list[dict], optional):
62+
The default filters to appllied to the query first. This is
63+
mainly used by the caller as part of passing through the
64+
filters parameter as well.
65+
iterable: (object, optional):
66+
The iterable object to return to the caller.
67+
68+
Returns:
69+
Union[OTExportsIterator, dict]:
70+
By default the method will return an iterator that will handle
71+
pagination and return a single item at a time. If return_json
72+
is set to `True` however, then the JSON response will be
73+
returned instead for that page.
74+
"""
75+
default_filters = [] if default_filters is None else default_filters
76+
filters = [] if filters is None else filters
77+
sort = [] if sort is None else sort
78+
79+
# Iterate over the default filters and add them to the filter list
80+
# if they don't exist.
81+
for default_filter in default_filters:
82+
field = default_filter['field']
83+
if field not in [f.get('field') for f in filters]:
84+
filters.append(default_filter)
85+
86+
filter = filters[0] if filters else None
87+
if len(filters) > 1:
88+
filter = {
89+
'op': 'And',
90+
'expressions': filters
91+
}
92+
93+
variables = {
94+
'search': search,
95+
'sort': sort,
96+
'startAt': start_at,
97+
'limit': limit,
98+
'filter': filter,
99+
}
100+
101+
if return_json:
102+
return self._api.query(query=query,
103+
variables=variables,
104+
**kwargs
105+
)
106+
return iterable(self._api,
107+
_model=model,
108+
_query=query,
109+
_variables=variables,
110+
**kwargs
111+
)
112+
113+
def assets(self,
114+
filters: Optional[List[Dict]] = None,
115+
sort: Optional[List[Dict]] = None,
116+
search: Optional[str] = None,
117+
start_at: Optional[str] = None,
118+
limit: int = 200,
119+
filter_type: str = 'And',
120+
return_json: bool = False,
121+
) -> Union[OTExportsIterator, Dict]:
122+
"""
123+
Assets Export
124+
125+
Args:
126+
limit (int, optional):
127+
The number of objects to be returned per page.
128+
sort (list[dict], optional):
129+
A list of sort parameters to be passed to sort the responses.
130+
search (str, optional):
131+
Search string
132+
start_at (str, optional):
133+
Start returning data after this object id.
134+
filters (list[dict], optional):
135+
List of filters to apply to restict the response to only the
136+
desired items.
137+
filter_type (str, optional):
138+
When passing multiple filters, how should the filters be
139+
applied to the dataset? Acceptable values are `And` and `Or`.
140+
return_json (bool, optional):
141+
If `True`, then the instead of an iterator, the json response
142+
will be returned instead.
143+
144+
Returns:
145+
Union[OTExportsIterator, dict]:
146+
By default the method will return an iterator that will handle
147+
pagination and return a single item at a time. If return_json
148+
is set to `True` however, then the JSON response will be
149+
returned instead for that page.
150+
151+
Example:
152+
153+
>>> for asset in tot.exports.assets():
154+
... print(asset)
155+
"""
156+
default_filters = [
157+
{'op': 'Equal', 'field': 'hidden', 'values': 'false'}
158+
]
159+
default_sort = [
160+
{'field': 'risk', 'direction': 'DescNullLast'},
161+
{'field': 'id', 'direction': 'AscNullLast'},
162+
]
163+
sort = default_sort if sort is None else sort
164+
return self._list(query=queries.ASSETS,
165+
model='assets',
166+
filters=filters,
167+
default_filters=default_filters,
168+
sort=sort,
169+
search=search,
170+
start_at=start_at,
171+
limit=limit,
172+
filter_type=filter_type,
173+
return_json=return_json,
174+
)
175+
176+
def plugins(self,
177+
filters: Optional[List[Dict]] = None,
178+
sort: Optional[List[Dict]] = None,
179+
search: Optional[str] = None,
180+
start_at: Optional[str] = None,
181+
limit: int = 200,
182+
filter_type: str = 'And',
183+
return_json: bool = False,
184+
) -> Union[OTExportsIterator, Dict]:
185+
"""
186+
Plugin Export
187+
188+
Args:
189+
limit (int, optional):
190+
The number of objects to be returned per page.
191+
sort (list[dict], optional):
192+
A list of sort parameters to be passed to sort the responses.
193+
search (str, optional):
194+
Search string
195+
start_at (str, optional):
196+
Start returning data after this object id.
197+
filters (list[dict], optional):
198+
List of filters to apply to restict the response to only the
199+
desired items.
200+
filter_type (str, optional):
201+
When passing multiple filters, how should the filters be
202+
applied to the dataset? Acceptable values are `And` and `Or`.
203+
return_json (bool, optional):
204+
If `True`, then the instead of an iterator, the json response
205+
will be returned instead.
206+
207+
Returns:
208+
Union[OTExportsIterator, dict]:
209+
By default the method will return an iterator that will handle
210+
pagination and return a single item at a time. If return_json
211+
is set to `True` however, then the JSON response will be
212+
returned instead for that page.
213+
214+
Example:
215+
216+
>>> for plugin in tot.exports.plugins():
217+
... print(plugin)
218+
"""
219+
return self._list(query=queries.PLUGINS,
220+
model='plugins',
221+
filters=filters,
222+
sort=sort,
223+
search=search,
224+
start_at=start_at,
225+
limit=limit,
226+
filter_type=filter_type,
227+
return_json=return_json,
228+
)
229+
230+
def findings(self,
231+
filters: Optional[List[Dict]] = None,
232+
sort: Optional[List[Dict]] = None,
233+
search: Optional[str] = None,
234+
start_at: Optional[str] = None,
235+
limit: int = 200,
236+
filter_type: str = 'And',
237+
return_json: bool = False,
238+
) -> Union[OTExportsIterator, Dict]:
239+
"""
240+
Findings Export
241+
242+
Args:
243+
sorted (list[dict], optional):
244+
A list of asset sort parameters to be passed to sort the
245+
responses.
246+
search (str, optional):
247+
Asset Search string
248+
filters (list[dict], optional):
249+
List of asset filters to apply to restict the response to only
250+
the desired assets.
251+
filter_type (str, optional):
252+
When passing multiple filters, how should the filters be
253+
applied to the dataset? Acceptable values are `And` and `Or`.
254+
255+
Returns:
256+
OTFindingsIterator:
257+
The Iterable that hadles the more complex logic of gathering
258+
the findings for each asset and presenting them to the caller.
259+
260+
Example:
261+
262+
>>> for finding in tot.exports.findings():
263+
... print(finding)
264+
"""
265+
266+
assets = self._list(query=queries.FINDING_ASSETS,
267+
filters=filters,
268+
model='assets',
269+
sort=sort,
270+
search=search,
271+
limit=1000,
272+
filter_type=filter_type,
273+
return_json=return_json,
274+
)
275+
return OTFindingsIterator(self._api, _assets=assets)

tenable/ot/exports/iterator.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import Dict
2+
from restfly.iterator import APIIterator
3+
4+
5+
class OTExportsIterator(APIIterator):
6+
"""
7+
Tenable OT Security Exports Iterator
8+
"""
9+
_model: str
10+
_query: str
11+
_variables: Dict
12+
13+
def _get_page(self):
14+
"""
15+
Fetches the next page of data from the GraphQL API
16+
"""
17+
resp = self._api.graphql(query=self._query,
18+
variables=self._variables,
19+
)
20+
raw_page = resp.get('data', {}).get(self._model, {})
21+
self.page = raw_page.get('nodes', [])
22+
self._variables['startAt'] = raw_page.get('pageInfo', {})\
23+
.get('endCursor', None)
24+
self.total = raw_page.get('count')
25+
return self.page
26+
27+
28+
class OTFindingsIterator(APIIterator):
29+
"""
30+
Tenable OT Security Findings Iterator
31+
"""
32+
empty_asset_count: int = 0
33+
_assets: OTExportsIterator
34+
35+
def _get_page(self):
36+
"""
37+
Fetches the next page of findings from the
38+
v1 REST API.
39+
"""
40+
items = []
41+
counter = -1
42+
while len(items) == 0:
43+
counter += 1
44+
try:
45+
asset = self._assets.next()
46+
except StopIteration:
47+
raise StopIteration()
48+
self._asset_id = asset['id']
49+
items = self._api.get(f'v1/assets/{self._asset_id}/plugin_hits')
50+
self.page = items
51+
self.empty_asset_count += counter
52+
return self.page

0 commit comments

Comments
 (0)