def import_scan(self, fobj: Optional[BytesIO] = None, file_id: Optional[str] = None, folder_id: Optional[int] = None, password: Optional[str] = None) -> Dict: ''' Import a scan report into the Nessus scanner. Either a file object or a file_id must be specified. Args: fobj (BytesIO, optional): The file object to import. file_id (str, optional): The id of the already uploaded file object to import. folder_id (int, optional): The folder that the imported scan should reside within. password (str, optional): If the file object is encrypted, this password will be used to decrypt. Example: >>> with open('Example.nessus', 'rb') as reportfile: ... nessus.scans.import_scan(reportfile) ''' if not file_id: file_id = self._api.files.upload(fobj) return self._post('import', json=dict_clean({ 'file': file_id, 'folder_id': folder_id, 'password': password }))
def edit(self, rule_id: int, plugin_id: Optional[int] = None, type: Optional[Literal['recast_critical', 'recast_high', 'recast_medium', 'recast_low', 'recast_info', 'exclude']] = None, host: Optional[str] = None, date: Optional[int] = None) -> None: ''' Creates a new plugin rule Args: rule_id (int): The rule to modify plugin_id (int, optional): The plugin id to modify type: (str, optional): The type of modification to perform host (str, optional): The host to apply this rule to date (int, optional): The unix date for this rule to expire Example: >>> nessus.plugin_rules.edit(1, date=1645164000) ''' rule = self.details(1) payload = dict_merge( rule, dict_clean({ 'plugin_id': str(plugin_id), 'type': type, 'host': host, 'date': date })) return self._put(f'{rule_id}', json=payload)
def test_dict_clean(): dirty = { 'a': 1, 'b': { 'c': 2, 'd': None }, 'e': None, 'f': [{ 'g': 1, 'h': None }, { 'i': None }], 'j': [1, None, {}] } assert dict_clean(dirty) == { 'a': 1, 'b': { 'c': 2 }, 'f': [{ 'g': 1 }], 'j': [1, None] }
def settings(self, update: Literal['all', 'plugins', 'disabled'], custom_host: Optional[str] = None, auto_update_delay: Optional[int] = None) -> None: ''' Update the software update settings Args: update (str): What components should be updated? Expected values are ``all``, ``plugins``, and ``disabled``. custom_host (str, optional): URL of the custom plugin feed host auto_update_delay (int, optional): How often should the plugin feed attempt to update (in hours) Example: >>> nessus.software_update.settings(update='all', ... auto_update_delay=24 ... ) ''' self._put(json=dict_clean({ 'update': update, 'custom_host': custom_host, 'auto_update_delay': auto_update_delay }))
def edit(self, user_id: int, permissions: int, name: Optional[str] = None, email: Optional[str] = None) -> Dict: ''' Updates the specified user object Args: user_id (int): The id of the user to update permissions (int): The permissions settings for the user name (str, optional): The user's friendly name email (str, optional): The user's email address Returns: Dict: The updates user object Example: >>> nessus.users.edit(1, 32, name='Updated User') ''' return self._put(f'{user_id}', json=dict_clean({ 'permissions': permissions, 'name': name, 'email': email }))
def export_scan(self, scan_id: int, history_id: Optional[int] = None, fobj: Optional[BytesIO] = None, **kwargs) -> BytesIO: ''' Generate a scan export or report and download it. Args: scan_id (int): The id of the scan to export. history_id (int, optional): The history id of the specific point in time to export. fobj (BytexIO, optional): The file object to write the exported file to. If none is specified then a BytesIO object is written to in memory. filters (list[tuple], optional): The filters to apply to the exported data. format (str, optional): The exported scan format. Supported values are ``nessus``, ``html``, ``csv``, and ``db``. If unspecified, the default is ``nessus``. password (str, optional): The password to apply to the exported data (required for db). template_id (int, optional): When exporting in HTML or PDF, what report definition should the exported data be represented within. chunk_size (int, optional): The chunk sizing for the download itself. stream_hook (callable, optional): Overload the default downloading behavior with a custom stream hook. hook_kwargs (dict, optional): keyword arguments to pass to the stream_hook callable in addition to the default passed params. ''' dlopts = { 'fobj': fobj, 'chunk_size': kwargs.pop('chunk_size', None), 'stream_hook': kwargs.pop('stream_hook', None), 'hook_kwargs': kwargs.pop('hook_kwargs', None) } schema = ScanExportSchema() payload = dict_clean(schema.dump(schema.load(kwargs))) token = self._post(f'{scan_id}/export', params=dict_clean({'history_id': history_id}), json=payload)['token'] return self._api.tokens._fetch(token, **dlopts) # noqa PLW0212
def edit(self, smtp_host: Optional[str] = None, smtp_port: Optional[int] = None, smtp_enc: Optional[Literal['No Encryption', 'Use TLS if available', 'Force SSL' 'Force TLS' ]] = None, smtp_from: Optional[str] = None, smtp_www_host: Optional[str] = None, smtp_user: Optional[str] = None, smtp_pass: Optional[str] = None, smtp_auth: Optional[Literal['NONE', 'PLAIN', 'LOGIN', 'NTLM', 'CRAM-MD5' ]] = None ) -> None: ''' Updates the Nessus daemon's mail settings Args: smtp_host (str, optional): DNS/IP Address of the SMTP server smtp_port (int, optional): Port number for the SMTP service smtp_enc (str, optional): The connection encryption for the SMTP server smtp_from (str, optional): Reply email address for email sent by the Nessus daemon smtp_www_host (str, optional): The host to use in email links smtp_user (str, optional): The username to use when authenticating to the SMTP service smtp_pass (str, optional): The password to use when authenticating to the SMTP service smtp_auth (str, optional): The authentication type for the SMTP server Example: >>> nessus.mail.edit(smtp_user='******', ... smtp_pass='******', ... smtp_auth='LOGIN', ... ) ''' current = self.details() updated = dict_merge(current, dict_clean({ 'smtp_host': smtp_host, 'smtp_port': smtp_port, 'smtp_enc': smtp_enc, 'smtp_from': smtp_from, 'smtp_www_host': smtp_www_host, 'smtp_user': smtp_user, 'smtp_pass': smtp_pass, 'smtp_auth': smtp_auth })) self._put(json=updated)
def list(self, # noqa: PLC0103,PLR0913 name: Optional[str] = None, contains: Optional[str] = None, offset: int = 0, limit: int = 1000, return_json: bool = False ) -> Union[Dict, CSIterator]: ''' Returns the list of images stored within Container Security. :devportal:`API Documentation <container-security-v2-list-repositories>` # noqa: E501 Args: name (str, optional): Image name to filter on. Filter is case-sensitive and enforces an exact match. contains (str, optional): Partial name to filter on. Filter is case-sensitive. offset (int, optional): The number of records to skip before starting to return data. limit (int, optional): The number of records to return for each page of data. return_json (bool, optional): If set, then the response will instead be a Dict object instead of an iterable. Examples: Using the default iterable: >>> for repo in tio.cs.repositories.list(): ... print(repo) Getting the raw JSON response: >>> resp = tio.cs.repositories.list(return_json=True) >>> for item in resp['items']: ... print(item) ''' params = dict_clean({ 'offset': offset, 'limit': limit, 'name': name, 'contains': contains, }) if return_json: return self._get(params=params) return CSIterator(self._api, _path=self._path, _params=params, _limit=limit, _offset=offset )
def reformat_filters(self, data, **kwargs) -> Dict: # noqa PLW0613 PLR0201 ''' Reformats the response to match what the API expects to see ''' filters = data.pop('filters', None) if data.get('search_type'): data['filter.search_type'] = data.pop('search_type') if filters: for f in filters: # noqa PLC0103 idx = filters.index(f) data[f'filter.{idx}.filter'] = f['filter'] data[f'filter.{idx}.quality'] = f['quality'] data[f'filter.{idx}.value'] = f['value'] return dict_clean(data)
def edit(self, name: Optional[str] = None, email: Optional[str] = None) -> None: ''' Updates the current user's settings. Args: name (str, optional): Updated name for the user email (str, optional): Updated email for the user Example: >>> nessus.session.edit(email='*****@*****.**') ''' self._put(json=dict_clean({'name': name, 'email': email}))
def update(self, scanner_id: int, force_plugin_update: Optional[bool] = None, force_ui_update: Optional[bool] = None, finish_update: Optional[bool] = None, registration_code: Optional[str] = None, aws_update_interval: Optional[int] = None) -> None: ''' Update the scanner Args: scanner_id (int): Id of the scanner to update force_plugin_update (bool, optional): Should the scanner plugins be forcibly updated? force_ui_update (bool, optional): Should the scanner UI be forcibly updated? finish_update (bool, optional): Should the scanner service be restarted to run the latest software update? This is only valid if automatic updates on the scanner are disabled. registration_code (str, optional): Sets the registration code for the scanner. aws_update_interval (int, optional): Informs the scanner how often to check into the controlling Nessus service. This is only valid for AWS scanners. Example: >>> nessus.scanners.update(1, ... force_plugin_update=True, ... force_ui_update=True ... ) ''' if force_plugin_update is not None: force_plugin_update = int(force_plugin_update) if force_ui_update is not None: force_ui_update = int(force_ui_update) if finish_update is not None: finish_update = int(finish_update) self._put(f'{scanner_id}', json=dict_clean({ 'force_plugin_update': force_plugin_update, 'force_ui_update': force_ui_update, 'finish_update': finish_update, 'registration_code': registration_code, 'aws_update_interval': aws_update_interval }))
def create(self, username: str, password: str, permissions: int, type: Literal['local', 'ldap'] = 'local', name: Optional[str] = None, email: Optional[str] = None) -> Dict: ''' Creates a new user Args: username (str): The unique username password (str): The user's password permissions (int): The permission level for the user. Basic users are ``16``, regular Users are ``32``, and administrators are ``64``. type (str, optional): The type of user account to create. The default is ``local`` name (str, optional): A friendly name for the user email (str, optional): The user's email address Returns: Dict: The created user object Example: >>> nessus.users.create(username='******', ... password='******', ... permissions=32, ... name='Example User', ... email='*****@*****.**' ... ) ''' return self._post(json=dict_clean({ 'username': username, 'password': password, 'permissions': permissions, 'type': type, 'name': name, 'email': email }))
def export_formats(self, scan_id: int, schedule_id: Optional[int] = None) -> Dict: ''' Returns the available export formats and report options. Args: scan_id (int): The scan to export schedule_id (int, optional): The schedule id associated with the scan Returns: Dict: The available export and report options Example: >>> nessus.scans.export_formats(1) ''' return self._get(f'{scan_id}/export/formats', params=dict_clean({'schedule_id': schedule_id}))
def restart(self, reason: Optional[str] = None, soft: Optional[bool] = None, unlink: Optional[bool] = None, when_idle: Optional[bool] = None) -> None: ''' Initiates a restart of this Nessus service Args: reason (str, optional): What is the reason for the restart to occur? soft (bool, optional): Should we only restart the web service (soft restart) or restart the whole Nessus service? unlink (bool, optional): Should the scanner be unlinked from it's upstream controller before restarting? when_idle (bool, optional): Should the scanner restart once there are no running scans? Example: >>> nessus.server.restart(reason='Time to restart', ... when_idle=True, ... soft=True ... ) ''' if soft is not None: soft = str(soft).lower() if unlink is not None: unlink = str(unlink).lower() if when_idle is not None: when_idle = str(when_idle).lower() return self._get('restart', params=dict_clean({ 'reason': reason, 'soft': soft, 'unlink': unlink, 'when_idle': when_idle }))
def list( self, # noqa: PLC0103,PLR0913 name: Optional[str] = None, repo: Optional[str] = None, tag: Optional[str] = None, has_malware: Optional[bool] = None, score: Optional[int] = None, score_operator: Optional[Literal['EQ', 'GT', 'LT']] = None, os: Optional[str] = None, offset: int = 0, limit: int = 1000, return_json: bool = False) -> Union[Dict, CSIterator]: ''' Returns the list of images stored within Container Security. :devportal:`API Documentation <container-security-v2-list-images>` Args: name (str, optional): Image name to filter on. Filter is case-sensitive and enforces an exact match. repo (str, optional): Repository name to filter on. Filter is case-sensitive and enforces an exact match. tag (str, optional): Tag to filter on. Filter is case-sensitive and enforces an exact match. has_malware (bool, optional): Specifies whether to return only images with malware associated to them. score (int, optional): The score value to filter on. score_operator (str, optional): The score operator to use with the score value. Supported operations are ``EQ`` (equal), ``GT`` (greater-than), and ``LT`` (less-than). os (str, optional): The operating system to filter on. Filter is case-sensitive and enforces an exact match. offset (int, optional): The number of records to skip before starting to return data. limit (int, optional): The number of records to return for each page of data. return_json (bool, optional): If set, then the response will instead be a Dict object instead of an iterable. Examples: Using the default iterable: >>> for image in tio.cs.images.list(): ... print(image) Getting the raw JSON response: >>> resp = tio.cs.images.list(return_json=True) >>> for item in resp['items']: ... print(item) ''' params = dict_clean({ 'offset': offset, 'limit': limit, 'name': name, 'repo': repo, 'tag': tag, 'hasMalware': has_malware, 'score': score, 'scoreOperator': score_operator, 'os': os }) if return_json: return self._get(params=params) return CSIterator(self._api, _path=self._path, _params=params, _limit=limit, _offset=offset)
def edit(self, exclusion_id, scanner_id=1, name=None, start_time=None, end_time=None, timezone=None, description=None, frequency=None, interval=None, weekdays=None, day_of_month=None, enabled=None): ''' Edit an existing agent exclusion. :devportal:`agent-exclusions: edit <agent-exclusions-edit>` The edit function will first gather the details of the exclusion that will be edited and will overlay the changes on top. The result will then be pushed back to the API to modify the exclusion. Args: exclusion_id (int): The id of the exclusion object in Tenable.io scanner_id (int, optional): The scanner id. name (str, optional): The name of the exclusion to create. description (str, optional): Some further detail about the exclusion. start_time (datetime, optional): When the exclusion should start. end_time (datetime, optional): When the exclusion should end. timezone (str, optional): The timezone to use for the exclusion. The default if none is specified is to use UTC. frequency (str, optional): The frequency of the rule. The string inputted will be up-cased. Valid values are: *ONETIME, DAILY, WEEKLY, MONTHLY, YEARLY*. interval (int, optional): The interval of the rule. weekdays (list, optional): List of 2-character representations of the days of the week to repeat the frequency rule on. Valid values are: *SU, MO, TU, WE, TH, FR, SA* Default values: ``['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']`` day_of_month (int, optional): The day of the month to repeat a **MONTHLY** frequency rule on. enabled (bool, optional): enable/disable exclusion. Returns: dict: Dictionary of the newly minted exclusion. Examples: >>> exclusion = tio.agent_exclusions.edit(1, name='New Name') ''' # Lets start constructing the payload to be sent to the API... payload = self.details(exclusion_id, scanner_id=scanner_id) if name: payload['name'] = self._check('name', name, str) if description: payload['description'] = self._check('description', description, str) if enabled is not None: payload['schedule']['enabled'] = self._check( 'enabled', enabled, bool) if payload['schedule']['enabled']: frequency = self._check( 'frequency', frequency, str, choices=['ONETIME', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'], default=payload['schedule']['rrules']['freq'], case='upper') rrules = { 'freq': frequency, 'interval': payload['schedule']['rrules']['interval'], 'byweekday': None, 'bymonthday': None, } # frequency default value is designed for weekly and monthly based on below conditions # - if schedule rrules is not None and not defined in edit params, # and byweekday/bymonthday key already exist, assign old values # - if schedule rrules is not None and not defined in edit params # and byweekday/bymonthday key not already exist, assign default values # - if schedule rrules is not None and defined in edit params, assign new values if frequency == 'WEEKLY': rrules['byweekday'] = ','.join( self._check( 'weekdays', weekdays, list, choices=['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'], default=payload['schedule']['rrules'].get( 'byweekday', '').split() or ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'], case='upper')) # In the same vein as the frequency check, we're accepting # case-insensitive input, comparing it to our known list of # acceptable responses, then joining them all together into a # comma-separated string. if frequency == 'MONTHLY': rrules['bymonthday'] = self._check( 'day_of_month', day_of_month, int, choices=list(range(1, 32)), default=payload['schedule']['rrules'].get( 'bymonthday', datetime.today().day)) # update new rrules in existing payload dict_merge(payload['schedule']['rrules'], rrules) # remove null values from payload payload = dict_clean(payload) if start_time: payload['schedule']['starttime'] = self._check( 'start_time', start_time, datetime).strftime('%Y-%m-%d %H:%M:%S') if end_time: payload['schedule']['endtime'] = self._check( 'end_time', end_time, datetime).strftime('%Y-%m-%d %H:%M:%S') if interval: payload['schedule']['rrules']['interval'] = self._check( 'interval', interval, int) if timezone: payload['schedule']['timezone'] = self._check( 'timezone', timezone, str, choices=self._api._tz) # Lets check to make sure that the scanner_id and exclusion_id are # integers as the API documentation requests and if we don't raise an # error, then lets make the call. return self._api.put('scanners/{}/agents/exclusions/{}'.format( self._check('scanner_id', scanner_id, int), self._check('exclusion_id', exclusion_id, int)), json=payload).json()
def create(self, infrastructure_id: int, name: str, ip: str, dns: str, **kwargs) -> List[Dict]: ''' Creates a new directory instance. Args: infrastructure_id (int): The infrastructure object to bind this directory to. name (str): Name of the directory instance. ip (str): The IP Address of the directory server. dns (str): The DNS domain that this directory is tied to. directory_type (optional, str): The directory's type. ldap_port (optional, str): The port number associated to the LDAP service on the directory server. global_catalog_port (optional, str): The port number associated to the Global Catalog service running on the directory server. smb_port (optional, str): The port number associated to the Server Messaging Block (SMB) service running on the directory server. Returns: dict: The created directory instance. Examples: >>> tad.directories.create( ... infrastructure_id=1, ... name='ExampleServer', ... ip='172.16.0.1', ... directory_type='????', ... dns='company.tld', ... ) ''' schema = DirectorySchema(unknown=INCLUDE) payload = [ schema.dump( schema.load( dict_clean({ 'infrastructureId': infrastructure_id, 'name': name, 'ip': ip, 'type': kwargs.get('directory_type'), 'dns': dns, 'ldapPort': kwargs.get('ldap_port'), 'globalCatalogPort': kwargs.get('global_catalog_port'), 'smbPort': kwargs.get('smb_port') }))) ] return schema.load(self._post(json=payload), many=True)