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

Added support for TenableAD attack type option APIs #516

Merged
merged 2 commits into from
May 21, 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
1 change: 1 addition & 0 deletions docs/api/ad/attack_type_options.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. automodule:: tenable.ad.attack_type_options.api
1 change: 1 addition & 0 deletions tenable/ad/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
api_keys
attacks
attack_types
attack_type_options
application_settings
category
checker
Expand Down
Empty file.
102 changes: 102 additions & 0 deletions tenable/ad/attack_type_options/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'''
Attack Type Options
=============

Methods described in this section relate to the attack type options API.
These methods can be accessed at ``TenableAD.attack_type_options``.

.. rst-class:: hide-signature
.. autoclass:: AttackTypeOptionsAPI
:members:
'''
from typing import List, Dict
from tenable.ad.attack_type_options.schema import AttackTypeOptionsSchema
from tenable.base.endpoint import APIEndpoint


class AttackTypeOptionsAPI(APIEndpoint):
_schema = AttackTypeOptionsSchema()

def list(self,
profile_id: str,
attack_type_id: str,
**kwargs
) -> List[Dict]:
'''
Get all attack type options related to a profile and attack type.

Args:
profile_id (str):
The attack profile identifier.
attack_type_id (str):
The attack type identifier.
staged (optional, bool):
Get only objects that are staged.
Accepted values are ``True``, ``False``.

Returns:
list:
The list of attack type options objects

Examples:
>>> tad.attack_type_options.list(
... profile_id='1',
... attack_type_id='1',
... staged=False
... )
'''
partial = ('codename', 'value', 'value_type', )
params = self._schema.dump(self._schema.load(kwargs, partial=partial))

return self._schema.load(
self._api.get(f'profiles/{profile_id}/'
f'attack-types/{attack_type_id}/'
f'attack-type-options', params=params),
many=True)

def create(self,
profile_id: str,
attack_type_id: str,
**kwargs
) -> List:
'''
Create attack type options related to a profile and attack type.

Args:
profile_id (str):
The attack profile identifier.
attack_type_id (str):
The attack type identifier.
codename (str):
The codename of attack type option.
value (str):
The new value of the option.
value_type (str):
The type of option. possible values are ``string``, ``regex``,
``float``, ``integer``, ``boolean``, ``date``, ``object``,
``array/string``, ``array/regex``, ``array/integer``,
``array/boolean``, ``array/select``, ``array/object``
directory_id (optional, int):
The directory identifier.

Return:
list:
The newly created attack type options.

Example:
>>> tad.attack_type_options.create(
... profile_id='1',
... attack_type_id='1',
... codename='codename',
... value='Some value',
... value_type='string',
... directory_id=None
... )
'''
payload = [self._schema.dump(self._schema.load(kwargs))]

return self._schema.load(
self._api.post(f'profiles/{profile_id}/'
f'attack-types/{attack_type_id}/'
f'attack-type-options', json=payload),
many=True)
30 changes: 30 additions & 0 deletions tenable/ad/attack_type_options/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from marshmallow import fields, validate as v
from tenable.ad.base.schema import CamelCaseSchema, BoolInt


class AttackTypeOptionsSchema(CamelCaseSchema):
id = fields.Int()
codename = fields.Str(required=True)
profile_id = fields.Int()
attack_type_id = fields.Int()
directory_id = fields.Int(allow_none=True)
value = fields.Str(required=True)
value_type = fields.Str(required=True, validate=v.OneOf([
'string',
'regex',
'float',
'integer',
'boolean',
'date',
'object',
'array/string',
'array/regex',
'array/integer',
'array/boolean',
'array/select',
'array/object',
]))
name = fields.Str(allow_none=True)
description = fields.Str(allow_none=True)
translations = fields.List(fields.Str())
staged = BoolInt()
9 changes: 9 additions & 0 deletions tenable/ad/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .about import AboutAPI
from .alert.api import AlertsAPI
from .api_keys import APIKeyAPI
from .attack_type_options.api import AttackTypeOptionsAPI
from .application_settings.api import ApplicationSettingsAPI
from .attack_types.api import AttackTypesAPI
from .attacks.api import AttacksAPI
Expand Down Expand Up @@ -106,6 +107,14 @@ def attack_types(self):
'''
return AttackTypesAPI(self)

@property
def attack_type_options(self):
'''
The interface object for the
:doc:`Tenable.ad Attack Type Options APIs <attack_type_options>`.
'''
return AttackTypeOptionsAPI(self)

@property
def category(self):
'''
Expand Down
87 changes: 87 additions & 0 deletions tests/ad/attack_type_options/test_attack_type_options_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import pytest
import responses
from marshmallow import ValidationError

from tests.ad.conftest import RE_BASE


@responses.activate
def test_attack_type_option_list(api):
responses.add(responses.GET,
f'{RE_BASE}/profiles/1/attack-types/1/attack-type-options'
f'?staged=0',
json=[{
'id': 1,
'codename': 'codename',
'profileId': 1,
'attackTypeId': 1,
'directoryId': None,
'value': '[]',
'valueType': 'array/string',
'name': 'attack type option',
'description': 'description',
'translations': ['some translation'],
'staged': False
}]
)
resp = api.attack_type_options.list(
profile_id='1',
attack_type_id='1',
staged=False
)
assert isinstance(resp, list)
assert len(resp) == 1
assert resp[0]['id'] == 1
assert resp[0]['profile_id'] == 1
assert resp[0]['attack_type_id'] == 1
assert resp[0]['value'] == '[]'
assert resp[0]['value_type'] == 'array/string'


@responses.activate
def test_attack_type_option_create(api):
responses.add(responses.POST,
f'{RE_BASE}/profiles/1/attack-types/1/attack-type-options',
json=[{
'id': 1,
'codename': 'codename',
'profileId': 1,
'attackTypeId': 1,
'directoryId': None,
'value': '[]',
'valueType': 'array/string',
'name': 'attack type option',
'description': 'description',
'translations': ['some translation'],
'staged': False
}]
)
resp = api.attack_type_options.create(
profile_id='1',
attack_type_id='1',
codename='codename',
value='[]',
value_type='array/string',
directory_id=None
)
assert isinstance(resp, list)
assert len(resp) == 1
assert resp[0]['id'] == 1
assert resp[0]['profile_id'] == 1
assert resp[0]['attack_type_id'] == 1
assert resp[0]['value'] == '[]'
assert resp[0]['value_type'] == 'array/string'


def test_attack_type_option_value_type_validationerror(api):
'''
test to raise exception when value_type doesn't match the expected value
'''
with pytest.raises(ValidationError):
api.attack_type_options.create(
profile_id='1',
attack_type_id='1',
codename='attack type option codename',
value='something',
value_type='something',
)
45 changes: 45 additions & 0 deletions tests/ad/attack_type_options/test_attack_type_options_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'''
Testing the attack type option schema
'''
import pytest
from marshmallow import ValidationError
from tenable.ad.attack_type_options.schema import AttackTypeOptionsSchema


@pytest.fixture()
def attacks_type_option_schema():
return {
'codename': 'codename',
'value': '[]',
'value_type': 'array/string',
'directory_id': None
}


def test_attack_type_option_schema(attacks_type_option_schema):
'''
test application setting schema request payload
'''
test_resp = [{
'id': 1,
'codename': 'codename',
'profileId': 1,
'attackTypeId': 1,
'directoryId': None,
'value': '[]',
'valueType': 'array/string',
'name': 'name',
'description': 'description',
'translations': ['some translation'],
'staged': False
}]
schema = AttackTypeOptionsSchema()
req = schema.dump(schema.load(attacks_type_option_schema))
assert req['codename'] == test_resp[0]['codename']
assert req['value'] == test_resp[0]['value']
assert req['valueType'] == test_resp[0]['valueType']
assert req['directoryId'] == test_resp[0]['directoryId']

with pytest.raises(ValidationError):
attacks_type_option_schema['some_val'] = 'something'
schema.load(attacks_type_option_schema)
Loading