def call_api(self, path: str, params: dict = None) -> dict: if params is None: params = {} api_url = self.base_url + path headers = {"Authorization": f"basic {self.token}"} try: response = request("GET", self.base_url + path, params=komand.helper.clean(params), headers=headers) response.raise_for_status() return komand.helper.clean(response.json()) except HTTPError as httpError: raise PluginException( cause= f"Failed to get a valid response from AttackerKB at endpoint {api_url}", assistance=f"Response was {httpError.response.text}", data=httpError, )
def run(self, params={}): try: issue = { 'title': params.get(Input.TITLE), 'priority': ('major' if params.get(Input.PRIORITY) == 'None' else params.get(Input.PRIORITY)), 'kind': ('bug' if params.get(Input.KIND) == 'None' else params.get(Input.KIND)), 'state': ('new' if params.get(Input.STATE) == 'None' else params.get(Input.STATE)) } issue = dict((keys, v.lower()) for keys, v in issue.items()) if params.get(Input.CONTENT): issue['content'] = {'raw': params.get(Input.CONTENT)} if params.get(Input.COMPONENT): issue['components'] = {'name': params.get(Input.COMPONENT)} if params.get(Input.ASSIGNEE): issue['assignee'] = {'username': params.get(Input.ASSIGNEE)} if params.get(Input.VERSION): issue['versions'] = {'name': params.get(Input.VERSION)} if params.get(Input.MILESTONE): issue['milestones'] = {'name': params.get(Input.MILESTONE)} self.connection.bucket_session.headers.update( {'Content-Type': 'application/json'}) api_call = f'{self.connection.base_api}/repositories/{self.connection.username}/{params.get(Input.REPOSITORY).lower()}/issues' response = self.connection.bucket_session.post( api_call, data=json.dumps(issue)) if response.status_code == 201: self.logger.info('Issue Successfully created') return {'status': 'Issue Successfully created'} else: resp_obj = response.json() return {'error': resp_obj['error']['message']} except requests.exceptions.RequestException as e: raise PluginException(cause='User repository error', data=e)
def run(self, params={}): endpoint = f"https://{self.connection.host}/api/v2/cmdb/firewall/address" filter_ = params.get(Input.NAME_FILTER, "") params = None if filter_: params = {"filter": f"name=@{filter_}"} result = self.connection.session.get(endpoint, verify=self.connection.ssl_verify, params=params) try: result.raise_for_status() except Exception as e: raise PluginException( cause=f"Get address objects failed for {endpoint}\n", assistance=result.text, data=e) results = result.json().get("results") return {Output.ADDRESS_OBJECTS: komand.helper.clean(results)}
def run(self, params={}): formatter = ADUtils() conn = self.connection.conn dn = params.get("distinguished_name") dn = formatter.format_dn(dn)[0] dn = formatter.unescape_asterisk(dn) self.logger.info(f"Escaped DN {dn}") password_expire = {"pwdLastSet": ("MODIFY_REPLACE", [0])} try: conn.raise_exceptions = True conn.modify(dn=dn, changes=password_expire) except LDAPException as e: raise PluginException( cause="LDAP returned an error.", assistance= "Error was returned when trying to force password reset for this user.", data=e, ) return {"success": True}
def _check_and_compile_query(self, description_query: str) -> Optional[object]: """ This takes a regex string and compiles it to regex. If no string is given it will return None :param description_query: regex as string :return: re.Pattern (re.compile()) OR None if no string was given """ compiled_query = None if description_query: try: compiled_query = re.compile(description_query) except re.error as e: raise PluginException( cause= f"Invalid regex used for Description Query: {description_query}", assistance= "Please check your input for Description Query for errors" ) from e return compiled_query
def run(self, params={}): short_url = params.get(Input.URL) try: r = requests.get('https://unshorten.me/json/' + short_url) r.raise_for_status() out = r.json() except Exception as e: self.logger.error(e) raise PluginException(cause='Internal server error', assistance='Unshorten.me is unable to resolve the URL', data=e) try: if out[Output.ERROR]: if out[Output.ERROR] == "Connection Error": out[Output.ERROR] = "Unshorten.me is unable to resolve the URL" self.logger.error(out.get(Output.ERROR)) except KeyError: # All good, no error key is present self.logger.info('No errors') return out
def maybe_get_log_entries(self, log_id: str, query: str, time_from: int, time_to: int) -> (str, [object]): """ Make a call to the API and ask politely for log results. If the query runs exceptionally fast the API will return results immediately. In this case, we will return the results as the second return entry in the return tuple. The first element of the tuple will be None Usually, the API will return a 202 with callback URL to poll for results. If this is the case, we return the URL as the first entry in the tuple return. The second element in the return tuple will be None @param log_id: str @param query: str @param time_from: int @param time_to: int @return: (callback url, list of log entries) """ endpoint = f"{self.connection.url}log_search/query/logs/{log_id}" params = {"query": query, "from": time_from, "to": time_to} self.logger.info(f"Getting logs from: {endpoint}") self.logger.info(f"Using parameters: {params}") response = self.connection.session.get(endpoint, params=params) try: response.raise_for_status() except Exception: raise PluginException( cause="Failed to get logs from InsightIDR\n", assistance=f"Could not get logs from: {endpoint}\n", data=response.text) results_object = response.json() potential_results = results_object.get("events") if potential_results: self.logger.info("Got results immediately, returning.") return None, potential_results else: self.logger.info("Got a callback url. Polling results...") return results_object.get("links")[0].get("href"), None
def make_request(action, logger, *args, **kwargs): try: response = action(*args, **kwargs) return response.data() except BadRequestException as e: cause = "DomainToolsAPI: Bad Request:" assistance = f"code {e.code}, reason {e.reason}" except ServiceUnavailableException as e: cause = "DomainToolsAPI: Service Unavailable:" assistance = f"code {e.code}, reason {e.reason}" except NotAuthorizedException as e: cause = "DomainToolsAPI: Authorization Failed:" assistance = f"code {e.code}, reason {e.reason}" except NotFoundException as e: cause = "DomainToolsAPI: Action Not Found:" assistance = f"code {e.code}, reason {e.reason}" except InternalServerErrorException as e: cause = "DomainToolsAPI: Internal Server Error:" assistance = f"code {e.code}, reason {e.reason}" logger.error(f"DomainToolsAPI: {cause} {assistance}") raise PluginException(cause=cause, assistance=assistance)
def run(self, params={}): # # Note: ID is not a required payload parameter despite the API docs saying it is # Providing it actually causes the request to fail # resource_helper = ResourceRequests(self.connection.session, self.logger) endpoint = endpoints.ScanEnginePool.scan_engine_pools( self.connection.console_url) if ("engines" not in params) or (("engines" in params) and (len(params["engines"]) == 0)): error = "At least 1 scan engine must be assigned to the scan engine pool for creation." raise PluginException(preset=PluginException.Preset.UNKNOWN, data=error) self.logger.info("Creating scan engine pool...") response = resource_helper.resource_request(endpoint=endpoint, method="post", payload=params) return response
def get_token(self): self.logger.info("Updating Auth Token...") token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/token" body = { "resource": "https://graph.microsoft.com", "grant_type": "password", "client_id": self.app_id, "client_secret": self.app_secret, "username": self.username, "password": self.password } if self.refresh_token: body["refresh_token"] = self.refresh_token self.logger.info(f"Getting token from: {token_url}") result = requests.post(token_url, data=body) try: result.raise_for_status() except Exception as e: raise PluginException( cause="Authentication to Microsoft Graph failed.", assistance= f"Some common causes for this error include an invalid username, password, or connection settings." f"Verify you are using the correct domain name for your user, and verify that user has access to " f"the target tenant. Verify you can log into Office365 with the user account as well.\n" f"The result returned was:\n{result.text}", data=e) from e result_json = result.json() self.api_token = result_json.get("access_token") self.refresh_token = result_json.get("refresh_token") self.logger.info( f"Authentication was successful, token is: ******************{self.api_token[-5:]}" ) self.logger.info(f"Detected Permissions: {result_json.get('scope')}")
def commit(self, action: str, cmd: str) -> dict: querystring = { "type": "commit", "action": action, "key": self.key, "cmd": cmd } response = requests.get(self.url, params=querystring, verify=self.verify_cert) try: output = xmltodict.parse(response.text) except TypeError: raise ServerException( cause='The response from PAN-OS was not the correct data type.', assistance='Contact support for help.', data=response.text) except SyntaxError: raise ServerException( cause='The response from PAN-OS was malformed.', assistance='Contact support for help.', data=response.text) except BaseException as e: raise PluginException( cause= 'An unknown error occurred when parsing the PAN-OS response.', assistance='Contact support for help.', data=f'{response.text}, error {e}') if output['response']['@status'] == 'error': error = output['response']['msg'] error = json.dumps(error) raise ServerException( cause='PAN-OS returned an error in response to the request.', assistance= 'Double that check inputs are valid. Contact support if this issue persists.', data=error) return output
def run(self, params={}): endpoint = f"https://{self.connection.host}/api/v2/cmdb/firewall/policy" filter_ = params.get(Input.NAME_FILTER, "") get_params = {} if filter_: get_params = {"filter": f"name=@{filter_}"} result = self.connection.session.get(endpoint, params=get_params, verify=self.connection.ssl_verify) try: result.raise_for_status() except Exception as e: raise PluginException(cause=f"Get policy failed for {endpoint}\n", assistance=result.text, data=e) policies = result.json().get("results") return {Output.POLICIES: komand.helper.clean(policies)}
def run(self, params={}): saved_search_name = params.get(Input.SAVED_SEARCH_NAME) properties = params.get(Input.PROPERTIES) try: param_dict = json.loads( json.dumps(properties, default=lambda o: o.__dict__, indent=4, sort_keys=True)) saved_search_to_update = self.connection.client.saved_searches[ saved_search_name] saved_search_to_update.update(**param_dict).refresh() except JSONDecodeError as e: raise PluginException( preset=PluginException.Preset.INVALID_JSON) from e except KeyError as error: self.logger.error(error) return {Output.SUCCESS: False} return {Output.SUCCESS: True}
def run(self, params={}): """Add label to issue""" issue = self.connection.client.issue(id=params['id']) if not issue: raise PluginException( cause=f"No issue found with ID: {params['id']}.", assistance='Please provide a valid issue ID.') labels = params['label'].split(',') for label in labels: if label not in issue.fields.labels: issue.fields.labels.append(label) self.logger.info('Adding labels to issue %s: %s', params['id'], issue.fields.labels) issue.update(fields={'labels': issue.fields.labels}) return {'success': True}
def run(self, params={}): query = params.get(Input.QUERY) days_back = params.get(Input.DAYS_BACK, None) if query not in self.connection.terms: raise PluginException( cause="Query term not enabled in PhishEye product.", assistance= f"Add term to PhishEye and try again, or use one of the allowed terms: {self.connection.terms}." ) response = Helper.make_request(self.connection.api.phisheye, self.logger, query, days_back) output = { Output.DOMAINS: response["response"]["domains"], Output.TERM: response["response"]["term"] } if "date" in response["response"]: output["date"] = response["response"]["date"] return output
def _create_client(self, is_enterprise_license: bool) -> Service: """ Creates a Splunk client based on the Splunk license input :param is_enterprise_license: Whether or not the Splunk client should be configured for an Enterprise license :return: Splunk client """ try: if is_enterprise_license: self.logger.info("Connect: Connecting with Enterprise license configuration...") splunk_client = client.connect( host=self.host, port=self.port, username=self.username, password=self.password, scheme=self.scheme, verify=self.verify, ) else: self.logger.info("Connect: Connecting with Free license configuration...") # We need to pass 'admin' as the username for the free license splunk_client = client.connect( host=self.host, port=self.port, username="******", scheme=self.scheme, verify=self.verify, ) except Exception as e: # noinspection PyTypeChecker raise self._EXCEPTIONS.get( type(e), PluginException( cause="An unhandled exception occurred!", assistance="Check the logs for more details.", data=e, ), ) return splunk_client
def get_site_scans(self, params): # Generate unique identifier for report names identifier = uuid.uuid4() # Gather site IDs of sites that match regular expression site_ids = NewScans.get_sites_within_scope(self, params.get(Input.SITE_NAME_FILTER)) # Gather sites and corresponding site IDs in scope report_payload = { 'name': f"Rapid7-InsightConnect-NewScans-{identifier}", 'format': 'sql-query', 'query': NewScans.scans_query(map(lambda x: "'" + x + "'", params.get(Input.STATUS_FILTER)), [str(site_id) for site_id in site_ids]), 'version': '2.3.0', } # Run report to get scans based on sites in scope # This is preferred over API endpoints due to endpoints returning all scans for agent site self.logger.info("Pulling scans") report_contents = util.adhoc_sql_report(self.connection, self.logger, report_payload) site_scans = defaultdict(list) try: csv_report = csv.DictReader(io.StringIO(report_contents['raw'].decode('utf-8'))) except Exception as e: raise PluginException(cause="Error: Failed to process query response while fetching site scans.", assistance=f"Exception returned was {e}") # Identify all scans that match sites from regular expression and status filter for row in csv_report: site_scan = { "scan_id": int(row["scan_id"]), "status": row["status"], "site_id": int(row["site_id"]), "site_name": row["site_name"] } site_scans[row["site_id"]].append(site_scan) return site_scans
def run(self, params={}): try: domain = params.get(Input.DOMAIN) fields = params.get(Input.FIELDS) comment = params.get(Input.COMMENT) if not fields or not len(fields): fields = None if not comment: comment = None domain_report = self.connection.client.lookup_domain( domain, fields=fields, comment=comment) if domain_report.get("warnings", False): self.logger.warning( f"Warning: {domain_report.get('warnings')}") self.logger.info( 'Option for fields are: ["sightings","threatLists","analystNotes","counts","entity","hashAlgorithm","intelCard","metrics", "relatedEntities" ,"risk" ,"timestamps"]' ) return komand.helper.clean(domain_report["data"]) except Exception as e: PluginException(cause=f"Error: {e}", assistance="Review exception")
def run(self, params={}): # Import variables from connection url = self.connection.url access_key = self.connection.access_key secret_key = self.connection.secret_key app_id = self.connection.app_id app_key = self.connection.app_key query = params.get(Input.QUERY) source = params.get(Input.SOURCE) if query: data = {"query": query, "source": source} else: data = {"source": source} # Mimecast request mimecast_request = util.MimecastRequests() response = mimecast_request.mimecast_post( url=url, uri=FindGroups._URI, access_key=access_key, secret_key=secret_key, app_id=app_id, app_key=app_key, data=data, ) try: output = response["data"][0]["folders"] except KeyError: self.logger.error(response) raise PluginException( cause="Unexpected output format.", assistance= "The output from Mimecast was not in the expected format. Please contact support for help.", data=response, ) return {Output.GROUPS: output}
def send_html_message( logger: Logger, connection: komand.connection, message: str, team_id: str, channel_id: str, thread_id: str = None, ) -> dict: """ Send HTML content as a message to Teams :param logger: object (logging.logger) :param connection: object (komand.connection) :param message: String (HTML) :param team_id: String :param channel_id: String :param thread_id: string :return: dict """ send_message_url = f"https://graph.microsoft.com/beta/teams/{team_id}/channels/{channel_id}/messages" if thread_id: send_message_url = send_message_url + f"/{thread_id}/replies" logger.info(f"Sending message to: {send_message_url}") headers = connection.get_headers() body = {"body": {"contentType": "html", "content": message}} result = requests.post(send_message_url, headers=headers, json=body) try: result.raise_for_status() except Exception as e: raise PluginException(cause="Send message failed.", assistance=result.text) from e message = result.json() return message
def run(self, params={}): job_id = params.get(Input.JOB_ID) timeout = params.get(Input.TIMEOUT) timer_step = 0.2 # What we should increment the timeout counter by try: search_job = self.connection.client.jobs[job_id] except KeyError as error: self.logger.error(error) raise PluginException( cause="Unable to find job.", assistance="Ensure the provided job ID input is valid.", data=f"Job ID: {job_id}", ) timer = 0 # Keep track of the timeout self.logger.info("Streaming results") while not search_job.is_done() and timer < timeout: sleep(timer_step) timer += timer_step self.logger.info("Search not complete, sleeping for %s seconds" % timer_step) if timer > timeout: self.logger.info( "Timeout occurred, finalizing and attempting to retrieve results..." ) search_job.finalize() rr = results.ResultsReader(search_job.results()) gathered_results = [] for result in rr: if isinstance(result, dict): gathered_results.append(result) return {Output.SEARCH_RESULTS: gathered_results}
def run(self, params={}): group_name = params[Input.GROUP_NAME] address_name = params[Input.ADDRESS_OBJECT_NAME] group = self.connection.get_address_group(group_name) group_members = group.get("member") group_members.append({"name": address_name}) group["member"] = group_members endpoint = f"https://{self.connection.host}/api/v2/cmdb/firewall/addrgrp/{group.get('name')}" response = self.connection.session.put( endpoint, json=group, verify=self.connection.ssl_verify) try: response.raise_for_status() except Exception as e: json_obj = {} try: json_obj = response.json() except Exception as err: self.throw_unknown_error(err, endpoint, response) if json_obj.get("error", 0) == -3: raise PluginException( cause= f"Add address object to address group failed: {endpoint}\n", assistance= "The error code returned was -3. This usually indicates" "that the address object specified could not be found.", data=e) self.throw_unknown_error(e, endpoint, response) json_response = response.json() success = json_response.get("status", "").lower() == "success" return {Output.SUCCESS: success, Output.RESULT_OBJECT: json_response}
def handle_exceptions(e: pyodbc.Error, query: str): """ This method takes a pyodbc.Error object and raises a specific error type Along with appropriate messaging :param e: A pyodbc.Error object to be raised and give context :param query: The query that the plugin attempted to run """ _ASSISTANCE = 'Double-check the query string and refer to the error code for additional information.' _DATA = f'Error code: {e}\nQuery string: {query}' cause = { pyodbc.OperationalError: 'An operational error occurred. This can be related to the database\'s operation' ' and not necessarily under the control of the programmer, e.g. an unexpected disconnect occurs,' ' the data source name is not found, a transaction could not be processed,' ' a memory allocation error occurred during processing, etc.', pyodbc.ProgrammingError: 'A programing error occurred. This can be caused by \'table not found\' or \'already exists\',' ' syntax error in the SQL statement, wrong number of parameters specified, etc.', pyodbc.DataError: 'A data error occurred. This can be caused by problems with the processed data such as division by zero,' ' numeric value out of range, etc.', pyodbc.IntegrityError: 'An integrity error occurred. This can happen' ' when the relational integrity of the database is affected, e.g. a foreign key check fails.', pyodbc.InternalError: 'An internal error occurred. This can be caused when the database encounters an internal error, e.g. the ' 'cursor is invalid, the transaction is out of sync, etc.', pyodbc.NotSupportedError: 'A \'not supported\' error occurred. This can be caused by a method or database API was used' ' which is not supported by the database, e.g.' ' requesting a .rollback() on a connection that does not support transactions or has transactions turned off.', pyodbc.InterfaceError: 'An interface error occurred. This is an error related to the database interface rather than the database itself.' } raise PluginException(cause=cause[type(e)], assistance=_ASSISTANCE, data=_DATA)
def search(self, pattern, start=None, limit=None, include_category=None): '''Searches for domains that match a given pattern''' params = dict() if start is None: start = datetime.timedelta(days=30) if isinstance(start, datetime.timedelta): params['start'] = int(time.mktime((datetime.datetime.utcnow() - start).timetuple()) * 1000) elif isinstance(start, datetime.datetime): params['start'] = int(time.mktime(start.timetuple()) * 1000) else: raise PluginException(cause='Unable to retrieve domains for search.', assistance=Investigate.SEARCH_ERR) if limit is not None and isinstance(limit, int): params['limit'] = limit if include_category is not None and isinstance(include_category, bool): params['includeCategory'] = str(include_category).lower() uri = self._uris['search'].format(urllib.parse.quote_plus(pattern)) return self.get_parse(uri, params)
def run(self, params={}): endpoint = f"https://{self.connection.host}/api/v2/cmdb/firewall/address" filter_ = params.get(Input.NAME_FILTER, "") helper = Helpers(self.logger) params = None if filter_: params = {"filter": f"name=@{filter_}"} response = self.connection.session.get( endpoint, verify=self.connection.ssl_verify, params=params) try: json_response = response.json() except ValueError: raise PluginException( cause="Data sent by FortiGate was not in JSON format.\n", assistance="Contact support for help.", data=response.text) helper.http_errors(json_response, response.status_code) results = response.json().get("results") return {Output.ADDRESS_OBJECTS: komand.helper.clean(results)}
def run(self, params={}): url = f"https://{self.connection.server_ip}:{self.connection.server_port}/web_api/show-access-rulebase" headers = self.connection.get_headers() payload = { "offset": 0, "limit": params.get(Input.LIMIT, 1), "name": params.get(Input.LAYER_NAME), "details-level": "full", "use-object-dictionary": True, } result = requests.post(url, headers=headers, json=payload, verify=self.connection.ssl_verify) try: result.raise_for_status() except Exception as e: raise PluginException( cause=f"Show Access Rules from {url} failed.\n", assistance=f"{result.text}\n", data=e, ) return {Output.ACCESS_RULES: komand.helper.clean(result.json())}
def run(self, params={}): # Note: ID is not a required payload parameter despite the API docs saying it is # Providing it actually causes the request to fail resource_helper = ResourceRequests(self.connection.session, self.logger) endpoint = endpoints.ScanEngine.scan_engines(self.connection.console_url) payload = params self.logger.info("Creating scan engine...") try: response = resource_helper.resource_request(endpoint=endpoint, method="post", payload=payload) except Exception as e: if "An unexpected error occurred." in str(e): error = "Security console failed to connect to scan engine" elif "errors with the input or parameters supplied" in str(e): error = ( f"{str(e)} - " f"This may be due to an engine with this IP or name already existing in the Security Console." ) else: error = e raise PluginException(preset=PluginException.Preset.UNKNOWN, data=error) return response
def get_address_object(self, address_name): try: response_ipv4_json = self.call_api( path=f"firewall/address/{address_name}").json() if response_ipv4_json["http_status"] == 200: return response_ipv4_json except (PluginException, json.decoder.JSONDecodeError, requests.exceptions.HTTPError): pass response_ipv6 = self.call_api(path=f"firewall/address6/{address_name}") response_ipv6_json = response_ipv6.json() if response_ipv6_json["http_status"] == 200: return response_ipv6_json raise PluginException( cause= f"Get address object failed. Address object '{address_name}' does not exists.\n", assistance="Contact support for assistance.", data=response_ipv6.text, )
def run(self, params={}): group_ids = params.get(Input.GROUP_ID) user_id = params.get(Input.USER_ID) self.logger.info(f"Getting user info: {user_id}") user_response = get_user_info(self.connection, user_id, self.logger) user_object = user_response.json() user = {"@odata.id": f"https://graph.microsoft.com/v1.0/{self.connection.tenant}/users/{user_object.get('id')}"} headers = self.connection.get_headers(self.connection.get_auth_token()) for group_id in group_ids: add_to_group_endpoint = ( f"https://graph.microsoft.com/v1.0/{self.connection.tenant}/groups/{group_id}/members/$ref" ) result = requests.post(add_to_group_endpoint, json=user, headers=headers) if not result.status_code == 204: raise PluginException( cause=f"Add User to Group call returned an unexpected response: {result.status_code}", assistance=f"Check that the group id {group_id} and user id {user_id} are correct.", data=result.text, ) return {Output.SUCCESS: True}
def run(self, params={}): name = params.get(Input.NAME) resource_records = params.get(Input.RESOURCE_RECORDS) record_type = params.get(Input.RECORDTYPE) if record_type: record_type = record_type.replace(" ", "") try: response = self.connection.investigate.get_dns( name, resource_records=resource_records, record_type=record_type ) except Exception as e: raise PluginException( preset=PluginException.Preset.UNKNOWN, data=e ) if resource_records == "Timeline": return { Output.TIMELINE_DATA: response } else: return response