Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/tenable.io.update_acr : Added ACR Update feature. #654

Merged
merged 3 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion tenable/io/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
'''
from tenable.io.base import TIOEndpoint


class AssetsAPI(TIOEndpoint):
'''
This will contain all methods related to Assets
'''

def list(self):
'''
Returns a list of assets.
Expand Down Expand Up @@ -284,7 +286,7 @@ def bulk_delete(self, *filters, hard_delete=None, filter_type=None):

# run the rules through the filter parser...
filter_type = self._check('filter_type', filter_type, str,
choices=['and', 'or'], default='and', case='lower')
choices=['and', 'or'], default='and', case='lower')
parsed = self._parse_filters(
filters, self._api.filters.workbench_asset_filters(), rtype='assets')['asset']

Expand All @@ -293,3 +295,36 @@ def bulk_delete(self, *filters, hard_delete=None, filter_type=None):
payload['query'] = {filter_type: parsed}

return self._api.post('api/v2/assets/bulk-jobs/delete', json=payload).json()

def update_acr(self, assets_uuid_list, reason, value, note=""):
"""
Updates ACR for the provided asset UUID's with reason(s).

Args:
assets_uuid_list (list):
Asset UUID's which are being updated.
reason (list):
List of reason(s).
value (str):
New ACR value for assets, ranging from 1 - 10.
note (str):
Additional note if any.

Returns:
:obj:`int`:
Status code for the request.

Examples:
>>> tio.assets.update_acr(
... ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
... ['Other'], 1, 'some notes')
"""
asset_uuids = []

for asset_uuid in assets_uuid_list:
asset_uuids.append({"id": asset_uuid})

note = note + " - pyTenable"
payload = [{"acr_score": int(value), "reason": reason, "asset": asset_uuids, "note": note}]

return self._api.post('api/v2/assets/bulk-jobs/acr', json=payload).status_code
94 changes: 62 additions & 32 deletions tenable/io/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from tenable.utils import dict_merge
from tenable.io.base import TIOEndpoint, TIOIterator


class TagsIterator(TIOIterator):
'''
The tags iterator provides a scalable way to work through tag list result
Expand All @@ -38,6 +39,7 @@ class TagsIterator(TIOIterator):
'''
pass


class TagsAPI(TIOEndpoint):
'''
This will contain all methods related to tags
Expand Down Expand Up @@ -74,7 +76,7 @@ class TagsAPI(TIOEndpoint):
},
'updated_by': {
'operators': ['eq'], 'pattern': None, 'choices': None
} # Add UUID regex here
} # Add UUID regex here
}

def _permission_constructor(self, items):
Expand All @@ -91,24 +93,24 @@ def _permission_constructor(self, items):
'id': self._check('id', item[0], 'uuid'),
"name": self._check('name', item[1], str),
"type": self._check('type', item[2], str,
choices=['user', 'group'], case='upper'),
choices=['user', 'group'], case='upper'),
"permissions": [
self._check('i', i, str,
choices=['ALL', 'CAN_EDIT', 'CAN_SET_PERMISSIONS'], case='upper')
choices=['ALL', 'CAN_EDIT', 'CAN_SET_PERMISSIONS'], case='upper')
for i in self._check('permissions', item[3], list)],
})
else:
data = dict()
data['id'] = self._check('id', item['id'], 'uuid')
data['name'] = self._check('name', item['name'], str)
data['type'] = self._check('type', item['type'], str,
choices=['user', 'group'], case='upper')
choices=['user', 'group'], case='upper')
data['permissions'] = [
self._check('i', i, str,
choices=['ALL', 'CAN_EDIT', 'CAN_SET_PERMISSIONS'], case='upper')
choices=['ALL', 'CAN_EDIT', 'CAN_SET_PERMISSIONS'], case='upper')
for i in self._check('permissions', item['permissions']
if 'permissions' in item else None, list,
default=list())]
if 'permissions' in item else None, list,
default=list())]
resp.append(data)

return resp
Expand All @@ -119,7 +121,7 @@ def _tag_value_constructor(self, filters, filterdefs, filter_type):
create and edit tag value.
'''
filter_type = self._check('filter_type', filter_type, str,
choices=['and', 'or'], default='and', case='lower')
choices=['and', 'or'], default='and', case='lower')

# created default dictionary for payload filters key
payload_filters = dict({
Expand Down Expand Up @@ -248,7 +250,7 @@ def create(self, category, value, description=None, category_description=None,
'all_users_permissions': [
self._check('i', i, str, choices=['ALL', 'CAN_EDIT', 'CAN_SET_PERMISSIONS'])
for i in self._check('all_users_permissions', all_users_permissions, list,
default=list(), case='upper')],
default=list(), case='upper')],

# run the current_domain_permissions through the permission_constructor
'current_domain_permissions': self._permission_constructor(
Expand Down Expand Up @@ -313,9 +315,9 @@ def delete(self, *tag_value_uuids):
self._check('tag_value_uuid', tag_value_uuids[0], 'uuid')))
else:
self._api.post('tags/values/delete-requests',
json={'values': [
self._check('tag_value_uuid', i, 'uuid') for i in tag_value_uuids
]})
json={'values': [
self._check('tag_value_uuid', i, 'uuid') for i in tag_value_uuids
]})

def delete_category(self, tag_category_uuid):
'''
Expand Down Expand Up @@ -448,7 +450,7 @@ def edit(self, tag_value_uuid, value=None, description=None, filters=None, filte
current_access_control['all_users_permissions'] = [
self._check('i', i, str, choices=['ALL', 'CAN_EDIT', 'CAN_SET_PERMISSIONS'])
for i in self._check('all_users_permissions', all_users_permissions, list,
case='upper')]
case='upper')]

# run current_domain_permissions through permission parser
if current_domain_permissions is not None:
Expand Down Expand Up @@ -521,7 +523,7 @@ def _tag_list_constructor(self, filters, filterdefs, filter_type, sort):
query = self._parse_filters(filters, filterdefs, rtype='colon')
if filter_type:
query['ft'] = self._check('filter_type', filter_type, str,
choices=['AND', 'OR'], case='upper')
choices=['AND', 'OR'], case='upper')
if sort and self._check('sort', sort, tuple):
query['sort'] = ','.join(['{}:{}'.format(
self._check('sort_field', i[0], str, choices=[k for k in filterdefs.keys()]),
Expand Down Expand Up @@ -574,16 +576,16 @@ def list(self, *filters, **kw):
... pprint(tag)
'''
query = self._tag_list_constructor(filters, self._filterset_tags,
kw['filter_type'] if 'filter_type' in kw else None,
kw['sort'] if 'sort' in kw else None)
kw['filter_type'] if 'filter_type' in kw else None,
kw['sort'] if 'sort' in kw else None)
return TagsIterator(self._api,
_limit=self._check('limit', kw.get('limit', 1000), int),
_offset=self._check('offset', kw.get('offset', 0), int),
_pages_total=self._check('pages', kw.get('pages'), int),
_query=query,
_path='tags/values',
_resource='values'
)
_limit=self._check('limit', kw.get('limit', 1000), int),
_offset=self._check('offset', kw.get('offset', 0), int),
_pages_total=self._check('pages', kw.get('pages'), int),
_query=query,
_path='tags/values',
_resource='values'
)

def list_categories(self, *filters, **kw):
'''
Expand Down Expand Up @@ -631,16 +633,16 @@ def list_categories(self, *filters, **kw):
... pprint(tag)
'''
query = self._tag_list_constructor(filters, self._filterset_categories,
kw['filter_type'] if 'filter_type' in kw else None,
kw['sort'] if 'sort' in kw else None)
kw['filter_type'] if 'filter_type' in kw else None,
kw['sort'] if 'sort' in kw else None)
return TagsIterator(self._api,
_limit=self._check('limit', kw.get('limit', 1000), int),
_offset=self._check('offset', kw.get('offset', 0), int),
_pages_total=self._check('pages', kw.get('pages'), int),
_query=query,
_path='tags/categories',
_resource='categories'
)
_limit=self._check('limit', kw.get('limit', 1000), int),
_offset=self._check('offset', kw.get('offset', 0), int),
_pages_total=self._check('pages', kw.get('pages'), int),
_query=query,
_path='tags/categories',
_resource='categories'
)

def assign(self, assets, tags):
'''
Expand Down Expand Up @@ -699,3 +701,31 @@ def unassign(self, assets, tags):
'assets': [self._check('asset', a, 'uuid') for a in assets],
'tags': [self._check('tag', t, 'uuid') for t in tags],
}).json()['job_uuid']

def get_tag_uuid(self, category, value):
"""
Fetches tag UUID using category/value pairs.

Args:
category (str):
The category name for the tag.
value (str):
The value for the tag.

Returns:
:obj:`str`:
Tag UUID.

Examples:
>>> tio.tags.get_tag_uuid('test_category','test_value')
"""
response = self._api.get('tags/categories/filter-categories')
tag_uuid = None

for cat in response.json()['categories']:
if cat['name'].casefold() == category.casefold():
for val in cat['values']:
if val['value'].casefold() == value.casefold():
tag_uuid = val['uuid']

return tag_uuid
10 changes: 9 additions & 1 deletion tenable/io/v3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"""
from tenable.base.endpoint import APIEndpoint
from tenable.io.v3.access_control import AccessControlAPI
from tenable.io.v3.explore import Explore
from tenable.io.v3.explore import Explore, AssetsAPI


class Version3API(APIEndpoint):
Expand All @@ -42,3 +42,11 @@ def access_control(self):
:doc:`Tenable Vulnerability Management v3 access control <access_control>`
"""
return AccessControlAPI(self._api)

@property
def assets(self):
"""
The interface object for the
:doc:`Tenable.io v3 assets <explore/assets>`
"""
return AssetsAPI(self._api)
30 changes: 30 additions & 0 deletions tenable/io/v3/explore/assets/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Union

from requests import Response
from restfly.errors import ForbiddenError

from tenable.io.v3.base.endpoints.explore import ExploreBaseEndpoint
from tenable.io.v3.base.iterators.explore_iterator import (CSVChunkIterator, SearchIterator)
Expand Down Expand Up @@ -337,3 +338,32 @@ def search_all(self, **kw) -> Union[SearchIterator, CSVChunkIterator, Response]:
iterator_cls=iclass,
api_path=f'{self._path}/search',
**kw)

def get_asset_uuids(self, **kw):
"""
Retrieves all the assets UUID's for the matching filter tags.

Args:
filter (tuple, dict, optional):
A nestable filter object detailing how to filter the results
down to the desired subset.
Examples:
>>> ('and', ('tags','eq', ['00000000-0000-0000-0000-000000000000']),
... ('tags', 'neq', ['00000000-0000-0000-0000-000000000001'])
... )
Returns:
:obj:`list`:
List of asset UUID's.

Examples:
>>> tio.v3.assets.get_asset_uuids(filter=('and', ('tags','eq', ['00000000-0000-0000-0000-000000000000']),
... ('tags', 'neq', ['00000000-0000-0000-0000-000000000001'])
... )
...)
"""
items = []

iterator = self.search_all(**kw)
for item in iterator:
items.append(item['id'])
return items
59 changes: 59 additions & 0 deletions tests/io/cassettes/test_get_tag_uuid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.10
(Restfly/1.4.7; Python/3.9.6; Darwin/arm64)
X-APIKeys:
- accessKey=12326e7a15c1c8684d598ac0018a211db16eb05b1e913a3c53deb23064168497;secretKey=f854d0a89b4cc7293ae6ef6e7f61d709744f62bf331cc96c1c0c118dc0cc16b5
method: GET
uri: https://cloud.tenable.com/tags/categories/filter-categories
response:
body:
string: '{"categories":[{"uuid":"2225679d-cbde-4e44-a201-dadbd94fd700","name":"test
tag","value_count":1,"values":[{"uuid":"de2e56a2-6a0e-4757-8d00-e9ad635f6231","value":"acr"}]},{"uuid":"71b6d9f4-cf29-4ec9-8d83-ba00575e9b67","name":"NO","value_count":1,"values":[{"uuid":"f038fd3a-a844-438f-b7a7-7acc6369f3e9","value":"UPDATE"}]}]}'
headers:
Accept-Ranges:
- bytes
Cache-Control:
- no-store
Connection:
- keep-alive
Content-Length:
- '323'
Content-Type:
- application/json; charset=utf-8
Date:
- Tue, 29 Nov 2022 14:08:35 GMT
Expect-CT:
- enforce, max-age=86400
Pragma:
- no-cache
Set-Cookie:
- nginx-cloud-site-id=us-2b; path=/; HttpOnly; SameSite=Strict; Secure
- nginx-cloud-site-id=us-2b; path=/; HttpOnly; SameSite=Strict; Secure
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- origin
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
X-Gateway-Site-ID:
- service-nginx-router-ap-northeast-1-prod-7bfcc7b48d-4tqbn
X-Request-Uuid:
- d4d2a9f029a2985b819cb82b17aa1253
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1
Loading
Loading