diff --git a/docs/api/sc/nnmscanner.rst b/docs/api/sc/nnmscanner.rst new file mode 100644 index 000000000..a72b3c51e --- /dev/null +++ b/docs/api/sc/nnmscanner.rst @@ -0,0 +1 @@ +.. automodule:: tenable.sc.nnmscanner diff --git a/tenable/sc/__init__.py b/tenable/sc/__init__.py index f7ab96a4c..79ed5351f 100644 --- a/tenable/sc/__init__.py +++ b/tenable/sc/__init__.py @@ -29,6 +29,7 @@ feeds files groups + nnmscanner organizations plugins policies @@ -60,6 +61,7 @@ from .files import FileAPI from .feeds import FeedAPI from .groups import GroupAPI +from .nnmscanner import NNMAPI from .organizations import OrganizationAPI from .plugins import PluginAPI from .policies import ScanPolicyAPI @@ -389,6 +391,14 @@ def groups(self): :doc:`Tenable Security Center Groups APIs `. ''' return GroupAPI(self) + + @property + def nnm(self): + ''' + The interface object for the + :doc:`Tenable.sc NNM APIs `. + ''' + return NNMAPI(self) @property def organizations(self): diff --git a/tenable/sc/nnmscanner.py b/tenable/sc/nnmscanner.py new file mode 100644 index 000000000..932c7f63f --- /dev/null +++ b/tenable/sc/nnmscanner.py @@ -0,0 +1,248 @@ +''' +NNM +======== + +The following methods allow for interaction into the Tenable.sc +:sc-api:`NNM ` API. These items are typically seen under the +**Passive Scanner** section of Tenable.sc. + +Methods available on ``sc.nnm``: + +.. rst-class:: hide-signature +.. autoclass:: NNMAPI + :members: +''' +from .base import SCEndpoint +from tenable.utils import dict_merge + + +class NNMAPI(SCEndpoint): + def _constructor(self, **kw): + ''' + Handles parsing the keywords and returns an NNM definition document + ''' + if 'name' in kw: + # Validate that the name parameter is a string. + self._check('name', kw['name'], str) + + if 'description' in kw: + # Validate that the description parameter is a string. + self._check('description', kw['description'], str) + + # Make sure that the appropriate authentication type is set. + if 'username' in kw: + kw['authType'] = 'password' + elif 'cert' in kw: + kw['authType'] = 'certificate' + + if 'cert' in kw: + # Validate that the cert parameter is a string. + self._check('cert', kw['cert'], str) + + if 'username' in kw: + # Validate that the username parameter is a string. + self._check('username', kw['username'], str) + + if 'password' in kw: + # Validate that the password parameter is a string. + self._check('password', kw['password'], str) + + if 'address' in kw: + # Validate that the address parameter is a string and store it + # within the ip parameter + kw['ip'] = self._check('address', kw['address'], str) + del(kw['address']) + + if 'port' in kw: + # Validate that the port parameter is a integer. + self._check('port', kw['port'], int) + + if 'proxy' in kw: + # Validate that the proxy parameter is a boolean flag and store it + # as a lowercased string in useProxy. + kw['useProxy'] = str(self._check( + 'proxy', kw['proxy'], bool)).lower() + del(kw['proxy']) + + if 'verify' in kw: + # Validate that the verify parameter is a boolean flag and store it + # as a lowercased string in verifyHost. + kw['verifyHost'] = str(self._check( + 'verify', kw['verify'], bool)).lower() + del(kw['verify']) + + if 'enabled' in kw: + # Validate that the enabled parameter is a boolean flag and store it + # as a lowercased string. + kw['enabled'] = str(self._check( + 'enabled', kw['enabled'], bool)).lower() + + return kw + + def create(self, name, address, **kw): + ''' + Creates a NNM in SC. + + :sc-api:`NNM: create ` + + Args: + address (str): The address of the nnm + name (str): The name of the nnm + description (str, optional): + The description of the nnm. + enabled (bool, optional): + Is this NNM enabled? If left unspecified, the default is + ``True``. + port (int, optional): + What is the port that the NNM service is running on. If left + unspecified, then the default is ``8835``. + proxy (bool, optional): + Is this NNM behind a proxy? If left unspecified then the + default is ``False``. + repository_ids (list, optional): + Lists repository ID. + username (str) + Define username of NNM user. + password (str) + Define password of NNM user. + + Returns: + :obj:`dict`: + The newly created NNM. + + Examples: + >>> nnm = sc.nnm.create('Example NNM', '192.168.0.1', username='admin', password='userpasswordhere') + ''' + payload = { + 'port': 8835, + 'proxy': False, + 'verify': False, + 'name': name, + 'address': address, + } + payload = self._constructor(**dict_merge(payload, kw)) + return self._api.post('passivescanner', json=payload).json()['response'] + + def details(self, id, fields=None): + ''' + Returns the details for a specific NNM. + + :sc-api:`NNM: details ` + + Args: + id (int): The identifier for the NNM. + fields (list, optional): A list of attributes to return. + + Returns: + :obj:`dict`: + The NNM resource record. + + Examples: + >>> nnm = sc.nnm.details(1) + >>> pprint(nnm) + ''' + params = dict() + if fields: + params['fields'] = ','.join([self._check('field', f, str) for f in fields]) + + return self._api.get('passivescanner/{}'.format(self._check('id', id, int)), + params=params).json()['response'] + + def edit(self, id, **kw): + ''' + Edits a NNM. + + :sc-api:`NNM: edit ` + + Args: + id (int): The numeric identifier for the NNM. + address (str, optional): The address of the NNM + description (str, optional): + The description of the NNM. + enabled (bool, optional): + Is this NNM enabled? If left unspecified, the default is + ``True``. + name (str, optional): The name of the NNM. + port (int, optional): + What is the port that the NNM service is running on. If left + unspecified, then the default is ``8835``. + proxy (bool, optional): + Is this scanner behind a proxy? If left unspecified then the + default is ``False``. + repository_ids (list, optional): + Lists repository ID. + + Returns: + :obj:`dict`: + The newly updated NNM. + + Examples: + >>> nnm = sc.nnm.edit(1, enabled=True) + ''' + payload = self._constructor(**kw) + return self._api.patch('passivescanner/{}'.format(id), + json=payload).json()['response'] + + def delete(self, id): + ''' + Removes the specified NNM. + + :sc-api:`NNM: delete ` + + Args: + id (int): The numeric identifier for the NNM to remove. + + Returns: + :obj:`str`: + An empty response. + + Examples: + >>> sc.nnm.delete(1) + ''' + return self._api.delete('passivescanner/{}'.format( + self._check('id', id, int))).json()['response'] + + def list(self, fields=None): + ''' + Retrieves the list of NNM definitions. + + :sc-api:`NNM: list ` + + Args: + fields (list, optional): + A list of attributes to return for each NNM. + + Returns: + :obj:`list`: + A list of NNM resources. + + Examples: + >>> for nnm in sc.nnm.list(): + ... pprint(nnm) + ''' + params = dict() + if fields: + params['fields'] = ','.join([self._check('field', f, str) + for f in fields]) + + return self._api.get('passivescanner', params=params).json()['response'] + ''' + Returns: + :obj:`list`: + The list of scans that match the search criteria. + ''' + def update_status(self): + ''' + Starts an on-demand NNM status update. + + :sc-api:`NNM: update-status ` + + Returns: + :obj:`list`: + The updated NNM status for all NNM instances. + + Examples: + >>> status = sc.nnm.update_status() + ''' + return self._api.post('passivescanner/updateStatus', + json={}).json()['response']['status'] diff --git a/tests/sc/cassettes/test_nnm_create_success.yaml b/tests/sc/cassettes/test_nnm_create_success.yaml new file mode 100644 index 000000000..d9b37a264 --- /dev/null +++ b/tests/sc/cassettes/test_nnm_create_success.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '{"port": 8835, "name": "Example", "username": "admin", "password": "password", + "authType": "password", "ip": "127.0.0.1", "useProxy": "false", "verifyHost": + "false"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '165' + Content-Type: + - application/json + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: POST + uri: https://127.0.0.1/rest/passivescanner + response: + body: + string: '{"type":"regular","response":{"id":"10","name":"Example","description":"","ip":"127.0.0.1","port":"8835","useProxy":"false","enabled":"true","verifyHost":"false","authType":"password","cert":null,"username":"admin","password":"SET","version":null,"webVersion":null,"admin":"false","uptime":-1,"status":"8192","pluginSet":null,"loadedPluginSet":null,"lastReportTime":"1647270051","createdTime":"1647273651","modifiedTime":"1647273651","repositories":[]},"error_code":0,"error_msg":"","warnings":[],"timestamp":1647273651} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '521' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:51 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sc/cassettes/test_nnm_delete_success.yaml b/tests/sc/cassettes/test_nnm_delete_success.yaml new file mode 100644 index 000000000..d05b2967d --- /dev/null +++ b/tests/sc/cassettes/test_nnm_delete_success.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: DELETE + uri: https://127.0.0.1/rest/passivescanner/10 + response: + body: + string: '{"type":"regular","response":"","error_code":0,"error_msg":"","warnings":[],"timestamp":1647273651} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '100' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:51 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sc/cassettes/test_nnm_details_success.yaml b/tests/sc/cassettes/test_nnm_details_success.yaml new file mode 100644 index 000000000..8df6fc254 --- /dev/null +++ b/tests/sc/cassettes/test_nnm_details_success.yaml @@ -0,0 +1,72 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: GET + uri: https://127.0.0.1/rest/passivescanner/10 + response: + body: + string: '{"type":"regular","response":"","error_code":147,"error_msg":"Passive + Scanner get failed.\nPassive Scanner #10 does not exist.\n","warnings":[],"timestamp":1647273652} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '168' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:52 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 403 + message: Forbidden +version: 1 diff --git a/tests/sc/cassettes/test_nnm_details_success_for_fields.yaml b/tests/sc/cassettes/test_nnm_details_success_for_fields.yaml new file mode 100644 index 000000000..2acdfc4fd --- /dev/null +++ b/tests/sc/cassettes/test_nnm_details_success_for_fields.yaml @@ -0,0 +1,72 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: GET + uri: https://127.0.0.1/rest/passivescanner/10?fields=id%2Cname%2Cdescription + response: + body: + string: '{"type":"regular","response":"","error_code":147,"error_msg":"Passive + Scanner get failed.\nPassive Scanner #10 does not exist.\n","warnings":[],"timestamp":1647273653} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '168' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:53 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 403 + message: Forbidden +version: 1 diff --git a/tests/sc/cassettes/test_nnm_edit_success.yaml b/tests/sc/cassettes/test_nnm_edit_success.yaml new file mode 100644 index 000000000..a6c87489a --- /dev/null +++ b/tests/sc/cassettes/test_nnm_edit_success.yaml @@ -0,0 +1,76 @@ +interactions: +- request: + body: '{"name": "Updated nnm Name"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '28' + Content-Type: + - application/json + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: PATCH + uri: https://127.0.0.1/rest/passivescanner/10 + response: + body: + string: '{"type":"regular","response":"","error_code":147,"error_msg":"Passive + Scanner edit failed.\nPassive Scanner #10 does not exist.\n","warnings":[],"timestamp":1647273653} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '169' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:53 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 403 + message: Forbidden +version: 1 diff --git a/tests/sc/cassettes/test_nnm_list_success.yaml b/tests/sc/cassettes/test_nnm_list_success.yaml new file mode 100644 index 000000000..cd4ec23b5 --- /dev/null +++ b/tests/sc/cassettes/test_nnm_list_success.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: GET + uri: https://127.0.0.1/rest/passivescanner + response: + body: + string: '{"type":"regular","response":[],"error_code":0,"error_msg":"","warnings":[],"timestamp":1647273653} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '100' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:53 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sc/cassettes/test_nnm_list_success_for_fields.yaml b/tests/sc/cassettes/test_nnm_list_success_for_fields.yaml new file mode 100644 index 000000000..28d51fafe --- /dev/null +++ b/tests/sc/cassettes/test_nnm_list_success_for_fields.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: GET + uri: https://127.0.0.1/rest/passivescanner?fields=id%2Cname%2Cstatus%2Cdescription + response: + body: + string: '{"type":"regular","response":[],"error_code":0,"error_msg":"","warnings":[],"timestamp":1647273653} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '100' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:53 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sc/cassettes/test_nnm_update_status.yaml b/tests/sc/cassettes/test_nnm_update_status.yaml new file mode 100644 index 000000000..1047b9757 --- /dev/null +++ b/tests/sc/cassettes/test_nnm_update_status.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '{}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + Cookie: + - TNS_SESSIONID=SESSIONID + TNS_SESSIONID: + - cd4d086a6fee6fce62aa510acf845657 + User-Agent: + - Integration/1.0 (pytest; pytenable-automated-testing; Build/unknown) pyTenable/1.4.3 + (Restfly/1.4.5; Python/3.8.2; Darwin/x86_64) + X-SecurityCenter: + - '0000000000' + method: POST + uri: https://127.0.0.1/rest/passivescanner/updateStatus + response: + body: + string: '{"type":"regular","response":{"status":[]},"error_code":0,"error_msg":"","warnings":[],"timestamp":1647273653} + + ' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - Keep-Alive + Content-Length: + - '111' + Content-Security-Policy: + - 'default-src ''self''; script-src ''self'' pendo-io-static.storage.googleapis.com + app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com + *.cloudfront.net; connect-src ''self'' app.pendo.io data.pendo.io pendo-static-6165929460760576.storage.googleapis.com + cdn.metarouter.io e.metarouter.io api.amplitude.com cdn.amplitude.com *.cloudfront.net; + img-src ''self'' data: cdn.pendo.io app.pendo.io pendo-static-6165929460760576.storage.googleapis.com + data.pendo.io; style-src ''self'' app.pendo.io cdn.pendo.io pendo-static-6165929460760576.storage.googleapis.com; + frame-ancestors ''self'' app.pendo.io; form-action ''self''; block-all-mixed-content; + Upgrade-Insecure-Requests; object-src ''none''' + Content-Type: + - application/json + Date: + - Mon, 14 Mar 2022 16:00:53 GMT + Expect-CT: + - max-age=31536000 + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=15, max=100 + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - x-apikey + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sc/test_nnmscanners.py b/tests/sc/test_nnmscanners.py new file mode 100644 index 000000000..092ec47c0 --- /dev/null +++ b/tests/sc/test_nnmscanners.py @@ -0,0 +1,285 @@ +''' +test file for testing various scenarios in securitycenter's nnm functionality +''' +import pytest + +from tenable.errors import APIError +from tests.pytenable_log_handler import log_exception +from ..checker import check + + +@pytest.fixture +def nnm(request, admin, vcr): + ''' + test fixture for nnm + ''' + with vcr.use_cassette('test_nnm_create_success'): + nnm = admin.nnm.create('Example', 'securitycenter.home.cugnet.net/', + username='admin', + password='password') + + def teardown(): + try: + with vcr.use_cassette('test_nnm_delete_success'): + admin.nnm.delete(int(nnm['id'])) + except APIError as error: + log_exception(error) + + request.addfinalizer(teardown) + return nnm + + +def test_nnm_constructor_name_typeerror(security_center): + ''' + test nnm for name type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(name=1) + + +def test_nnm_constructor_description_typeerror(security_center): + ''' + test nnm constructor for description type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(description=1) + + +def test_nnm_constructor_username_typeerror(security_center): + ''' + test nnm constructor for username type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(username=1) + + +def test_nnm_constructor_cert_typeerror(security_center): + ''' + test nnm constructor for 'cert' type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(cert=1) + + +def test_nnm_constructor_password_typeerror(security_center): + ''' + test nnm constructor for password type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(password=1) + + +def test_nnm_constructor_address_typeerror(security_center): + ''' + test nnm constructor for address type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(address=1) + + +def test_nnm_constructor_port_typeerror(security_center): + ''' + test nnm constructor for port type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(port='one') + + +def test_nnm_constructor_proxy_typeerror(security_center): + ''' + test nnm constructor for proxy type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(proxy='one') + + +def test_nnm_constructor_verify_typeerror(security_center): + ''' + test nnm constructor for verify type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(verify='yup') + + +def test_nnm_constructor_enabled_typeerror(security_center): + ''' + test nnm constructor for enabled type error + ''' + with pytest.raises(TypeError): + security_center.nnm._constructor(enabled='nope') + + +def test_nnm_constructor_success(security_center): + ''' + test nnm constructor for success + ''' + resp = security_center.nnm._constructor( + name='Example', + description='Described', + username='admin', + password='password', + address='securitycenter.home.cugnet.net/', + port=443, + proxy=False, + verify=False, + enabled=True, + ) + assert resp == { + 'name': 'Example', + 'description': 'Described', + 'authType': 'password', + 'username': 'admin', + 'password': 'password', + 'ip': 'securitycenter.home.cugnet.net/', + 'port': 443, + 'useProxy': 'false', + 'verifyHost': 'false', + 'enabled': 'true', + } + + +@pytest.mark.vcr() +def test_nnm_create_success(nnm): + ''' + test nnm create for success + ''' + assert isinstance(nnm, dict) + check(nnm, 'id', str) + check(nnm, 'name', str) + check(nnm, 'description', str) + check(nnm, 'ip', str) + check(nnm, 'port', str) + check(nnm, 'useProxy', str) + check(nnm, 'enabled', str) + check(nnm, 'verifyHost', str) + check(nnm, 'authType', str) + check(nnm, 'cert', str, allow_none=True) + check(nnm, 'username', str, allow_none=True) + check(nnm, 'password', str, allow_none=True) + check(nnm, 'version', str, allow_none=True) + check(nnm, 'webVersion', str, allow_none=True) + check(nnm, 'admin', str) + check(nnm, 'uptime', int) + check(nnm, 'status', str) + check(nnm, 'pluginSet', str, allow_none=True) + check(nnm, 'loadedPluginSet', str, allow_none=True) + check(nnm, 'createdTime', str) + check(nnm, 'modifiedTime', str) + +@pytest.mark.vcr() +def test_nnm_details_success(admin, nnm): + ''' + test nnm details for success + ''' + nnm = admin.nnm.details(int(nnm['id'])) + assert isinstance(nnm, dict) + check(nnm, 'id', str) + check(nnm, 'name', str) + check(nnm, 'description', str) + check(nnm, 'ip', str) + check(nnm, 'port', str) + check(nnm, 'useProxy', str) + check(nnm, 'enabled', str) + check(nnm, 'verifyHost', str) + check(nnm, 'authType', str) + check(nnm, 'cert', str, allow_none=True) + check(nnm, 'username', str, allow_none=True) + check(nnm, 'password', str, allow_none=True) + check(nnm, 'version', str, allow_none=True) + check(nnm, 'webVersion', str, allow_none=True) + check(nnm, 'admin', str) + check(nnm, 'uptime', int) + check(nnm, 'status', str) + check(nnm, 'pluginSet', str, allow_none=True) + check(nnm, 'loadedPluginSet', str, allow_none=True) + check(nnm, 'createdTime', str) + check(nnm, 'modifiedTime', str) + +@pytest.mark.vcr() +def test_nnm_details_success_for_fields(admin, nnm): + ''' + test nnm details success for fields + ''' + nnm = admin.nnm.details(int(nnm['id']), fields=['id', 'name', 'description']) + assert isinstance(nnm, dict) + check(nnm, 'id', str) + check(nnm, 'name', str) + check(nnm, 'description', str) + + +@pytest.mark.vcr() +def test_nnm_edit_success(admin, nnm): + ''' + test nnm edit for success + ''' + nnm = admin.nnm.edit(int(nnm['id']), name='Updated NNM Name') + assert isinstance(nnm, dict) + check(nnm, 'id', str) + check(nnm, 'name', str) + assert nnm['name'] == 'Updated NNM Name' + check(nnm, 'description', str) + check(nnm, 'ip', str) + check(nnm, 'port', str) + check(nnm, 'useProxy', str) + check(nnm, 'enabled', str) + check(nnm, 'verifyHost', str) + check(nnm, 'managePlugins', str) + check(nnm, 'authType', str) + check(nnm, 'cert', str, allow_none=True) + check(nnm, 'username', str, allow_none=True) + check(nnm, 'password', str, allow_none=True) + check(nnm, 'version', str, allow_none=True) + check(nnm, 'webVersion', str, allow_none=True) + check(nnm, 'admin', str) + check(nnm, 'uptime', int) + check(nnm, 'status', str) + check(nnm, 'pluginSet', str, allow_none=True) + check(nnm, 'loadedPluginSet', str, allow_none=True) + check(nnm, 'createdTime', str) + check(nnm, 'modifiedTime', str) + +@pytest.mark.vcr() +def test_nnm_delete_success(admin, nnm): + ''' + test nnm delete for success + ''' + admin.nnm.delete(int(nnm['id'])) + + +@pytest.mark.vcr() +def test_nnm_list_success(admin): + ''' + test nnm list for success + ''' + for nnm in admin.nnm.list(): + check(nnm, 'id', str) + check(nnm, 'name', str) + check(nnm, 'description', str) + check(nnm, 'status', str) + + +@pytest.mark.vcr() +def test_nnm_list_success_for_fields(admin, nnm): + ''' + test nnm list success for fields + ''' + for nnm_scanner in admin.nnm.list(fields=['id', 'name', 'status', 'description']): + check(nnm_scanner, 'id', str) + check(nnm_scanner, 'name', str) + check(nnm_scanner, 'status', str) + check(nnm_scanner, 'description', str) + + + +@pytest.mark.vcr() +def test_nnm_update_status(admin, nnm): + ''' + test nnm update status for success + ''' + resp = admin.nnm.update_status() + assert isinstance(resp, list) + for nnm in resp: + check(nnm, 'id', str) + check(nnm, 'name', str) + check(nnm, 'description', str) + check(nnm, 'status', str)