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

NNM Module Commit #565

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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/sc/nnmscanner.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. automodule:: tenable.sc.nnmscanner
10 changes: 10 additions & 0 deletions tenable/sc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
feeds
files
groups
nnmscanner
organizations
plugins
policies
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -389,6 +391,14 @@ def groups(self):
:doc:`Tenable Security Center Groups APIs <groups>`.
'''
return GroupAPI(self)

@property
def nnm(self):
'''
The interface object for the
:doc:`Tenable.sc NNM APIs <passivescanner>`.
'''
return NNMAPI(self)

@property
def organizations(self):
Expand Down
248 changes: 248 additions & 0 deletions tenable/sc/nnmscanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
'''
NNM
========

The following methods allow for interaction into the Tenable.sc
:sc-api:`NNM <Passive-Scanner.html>` 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 <Passive-Passive-Scanner.html#passivescanner_POST>`

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 <Passive-Scanner.html#passivescanner_POST>`

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 <Passive-Scanner.html#passivescanner_id_PATCH>`

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 <Passive-Scanner.html#passivescanner_id_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 <Passive-Scanner.html#passivescanner_GET>`

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 <Passive-Scanner.html#passiveScannerRESTReference-/passivescanner/updateStatus>`

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']
77 changes: 77 additions & 0 deletions tests/sc/cassettes/test_nnm_create_success.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading