def main(): parser = build_cli_parser("Process utility") args = parser.parse_args() # BEGIN Common if args.prefix: output_filename = '{0}-processes.csv'.format(args.prefix) else: output_filename = 'processes.csv' if args.append == True or args.queryfile is not None: file_mode = 'a' else: file_mode = 'w' if args.days: query_base = ' start:-{0}m'.format(args.days * 1440) elif args.minutes: query_base = ' start:-{0}m'.format(args.minutes) else: query_base = '' if args.profile: cb = CbThreatHunterAPI(profile=args.profile) else: cb = CbThreatHunterAPI() queries = [] if args.query: queries.append(args.query) elif args.queryfile: with open(args.queryfile, 'r') as f: for query in f.readlines(): queries.append(query.strip()) f.close() else: queries.append('') # END Common output_file = open(output_filename, file_mode) writer = csv.writer(output_file) writer.writerow([ "proc_timestamp", "proc_hostname", "proc_username", "proc_path", "proc_cmdline", "proc_hashes", "proc_child_count", "proc_filemod_count", "proc_modload_count", "proc_netconn_count", "proc_url", "parent_name" ]) for query in queries: result_set = process_search(cb, query, query_base) for row in result_set: if _python3 == False: row = [ col.encode('utf8') if isinstance(col, unicode) else col for col in row ] writer.writerow(row) output_file.close()
def get_feed_ids(): cb = CbThreatHunterAPI() feeds = cb.select(Feed) if not feeds: log.info("No feeds are available for the org key {}".format(cb.credentials.org_key)) else: for feed in feeds: log.info("Feed name: {:<20} \t Feed ID: {:>20}".format(feed.name, feed.id))
def get_feed_ids(): cb = CbThreatHunterAPI() url = "/threathunter/feedmgr/v2/orgs/{}/feeds".format( cb.credentials.org_key) feeds = cb.get_object(url) if len(feeds['results']) == 0: print("No feeds are available for the org key {}".format( cb.credentials.org_key)) else: for feed in feeds['results']: print("Feed name: {:<20} \t Feed ID: {:>20}".format( feed['name'], feed['id']))
def get_cb_threathunter_object(args): if args.verbose: logging.basicConfig() logging.getLogger("cbapi").setLevel(logging.DEBUG) logging.getLogger("__main__").setLevel(logging.DEBUG) if args.cburl and args.apitoken: cb = CbThreatHunterAPI(url=args.cburl, token=args.apitoken, ssl_verify=(not args.no_ssl_verify)) else: cb = CbThreatHunterAPI(profile=args.profile) return cb
def activate_report(cb: CbThreatHunterAPI, report_id) -> Dict: """Set this report to active status""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/reports/{report_id}/ignore" try: return cb.delete_object(url) except ServerError: LOGGER.error(f"Caught ServerError getting report {report_id}: {e}")
def main(): parser = build_cli_parser() parser.add_argument("-thp", "--threatprofile", help="Threat Hunter profile", default="default") commands = parser.add_subparsers(help="Feed commands", dest="command_name") list_command = commands.add_parser("list", help="List all configured feeds") list_reports_command = commands.add_parser( "list-reports", help="List all configured reports for a feed") list_reports_command.add_argument("-i", "--id", type=str, help="Feed ID") convert_feed_command = commands.add_parser( "convert", help="Convert feed from CB Response to CB Threat Hunter") convert_feed_command.add_argument("-i", "--id", type=str, help="Feed ID") args = parser.parse_args() cb = get_cb_response_object(args) cb_th = CbThreatHunterAPI(profile=args.threatprofile) if args.command_name == "list": return list_feeds(cb, parser, args) if args.command_name == "list-reports": return list_reports(cb, parser, args) if args.command_name == "convert": return convert_feed(cb, cb_th, parser, args)
def get_alert(cb: CbThreatHunterAPI, alert_id) -> Dict: """Get alert by ID.""" url = f"/appservices/v6/orgs/{cb.credentials.org_key}/alerts/{alert_id}" try: return cb.get_object(url) except ServerError: LOGGER.error(f"Caught ServerError getting report {report_id}: {e}")
def get_file_content(cb: CbThreatHunterAPI, session_id: str, file_id: str): """Get file content stored in LR session and write the file locally.""" from cbinterface.helpers import get_os_independent_filepath try: real_session_id, device_id = session_id.split(":", 1) filename = f"{real_session_id}_on_{device_id}" file_metadata = cb.get_object( f"{CBLR_BASE}/session/{session_id}/file/{file_id}") if file_metadata: filepath = get_os_independent_filepath(file_metadata["file_name"]) filename = f"{filename}_{filepath.name}" result = cb.session.get( f"{CBLR_BASE}/session/{session_id}/file/{file_id}/content", stream=True) if result.status_code != 200: LOGGER.error( f"got {result.status_code} from server getting file {file_id} content for session {session_id}" ) return with open(filename, "wb") as fp: for chunk in result.iter_content(io.DEFAULT_BUFFER_SIZE): fp.write(chunk) if os.path.exists(filename): LOGGER.info(f"wrote: {filename}") return os.path.exists(filename) except ObjectNotFoundError: LOGGER.warning(f"no file {file_id} content with session {session_id}") return
def event_search_complete(cb: CbThreatHunterAPI, job_id): """Return true when a search is complete.""" url = f"/api/investigate/v1/orgs/{cb.credentials.org_key}/enriched_events/search_jobs/{job_id}" result = cb.get_object(url) if result["completed"] == result["contacted"]: return True return False
def activate_ioc(cb: CbThreatHunterAPI, report_id, ioc_id): """Activate IOC.""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/reports/{report_id}/iocs/{ioc_id}/ignore" resp = cb.delete_object(url) if resp.status_code == 204: return True return False
def get_session_by_id(cb: CbThreatHunterAPI, session_id): """Get a LR session object by id.""" try: return cb.get_object(f"{CBLR_BASE}/session/{session_id}") except ObjectNotFoundError: LOGGER.warning(f"no live resonse session by ID={session_id}") return None
def make_process_query( cb: CbThreatHunterAPI, query: str, start_time: datetime.datetime = None, last_time: datetime.datetime = None, raise_exceptions=True, validate_query=False, ) -> AsyncProcessQuery: """Query the CbThreatHunterAPI environment and interface results. Args: cb: A CbThreatHunterAPI object to use query: The process query start_time: Set the process start time (UTC). last_time: Set the process last time (UTC). Only processes with a start time that falls before this last_time. raise_exceptions: Let any exceptions raise up (library use) validate_query: If True, validate the query before attempting to use it. Returns: AsyncProcessQuery or empty list. """ LOGGER.debug( f"buiding query: {query} between '{start_time}' and '{last_time}'") processes = [] try: processes = cb.select(Process).where(query) if validate_query and not is_valid_process_query(processes): LOGGER.info( f"For help, refer to {cb.url}/#userGuideLocation=search-guide/investigate-th&fullscreen" ) LOGGER.info( f"Is this a legacy query? ... Attempting to convert to PSC query ..." ) converted_query = convert_from_legacy_query(cb, query) if not converted_query: LOGGER.info( f"failed to convert to PSC query... 🤡 your query is jacked up." ) return [] if is_valid_process_query_string(cb, converted_query): LOGGER.info( "successfully converted and validated the query you supplied to a PSC query 👍, see below." ) LOGGER.info( f"👇👇 try again with the following query 👇👇 - also, hint, single quotes are your friend. " ) LOGGER.info(f"query: '{converted_query}'") return [] if start_time or last_time: start_time = start_time.isoformat() if start_time else "*" end_time = last_time.isoformat() if last_time else "*" processes = processes.where( f"process_start_time:[{start_time} TO {end_time}]") LOGGER.info(f"got {len(processes)} process results.") except Exception as e: if raise_exceptions: raise (e) LOGGER.error(f"unexpected exception: {e}") return processes
def alert_search( cb: CbThreatHunterAPI, search_data: Dict = {}, criteria: Dict = {}, query: str = None, rows=40, sort: List[Dict] = [{"field": "first_event_time", "order": "DESC"}], start: int = 0, workflow_state=["OPEN", "DISMISSED"], ) -> Dict: """Perform an Alert search One request and return the result. """ url = f"/appservices/v6/orgs/{cb.credentials.org_key}/alerts/watchlist/_search" if not search_data: if "workflow" not in criteria: criteria["workflow"] = workflow_state search_data = {"criteria": criteria, "query": query, "rows": rows, "start": start, "sort": sort} try: result = cb.post_object(url, search_data) return result.json() except ServerError as e: LOGGER.error(f"Caught ServerError searching alerts: {e}") return False except ClientError as e: LOGGER.warning(f"got ClientError searching alerts: {e}") return False except ValueError: LOGGER.warning(f"got unexpected {result}") return False
def delete_watchlist(cb: CbThreatHunterAPI, watchlist_id) -> Dict: """Set this report to ignore status""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/watchlists/{watchlist_id}" try: return cb.delete_object(url) except ServerError: LOGGER.error(f"Caught ServerError deleting watchlist {watchlist_id}: {e}")
def get_session_commands(cb: CbThreatHunterAPI, session_id: str): """List commands for this session.""" try: return cb.get_object(f"{CBLR_BASE}/session/{session_id}/command") except ObjectNotFoundError: LOGGER.warning(f"no live resonse session by ID={session_id}") return None
def is_ioc_ignored(cb: CbThreatHunterAPI, report_id, ioc_id, check_existence=False): """Return status of IOC.""" if check_existence: if not ioc_does_exist(cb, report_id, ioc_id): LOGGER.warning("IOC does not exist.") return None url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/reports/{report_id}/iocs/{ioc_id}/ignore" return cb.get_object(url)["ignored"]
def get_report_with_IOC_status(cb: CbThreatHunterAPI, report_id) -> Dict: """Get report and include status of every report IOC.""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/reports/{report_id}/iocs" report = get_report(cb, report_id) if not report: return None for ioc in report["iocs_v2"]: ioc["ignored"] = cb.get_object(f"{url}/{ioc['id']}/ignore")["ignored"] return report
def get_feed_report(cb: CbThreatHunterAPI, feed_id: str, report_id: str) -> Dict: """Get a specific report from a specific feed.""" url = f"/threathunter/feedmgr/v2/orgs/{cb.credentials.org_key}/feeds/{feed_id}/reports/{report_id}" try: return cb.get_object(url) except ServerError: LOGGER.error(f"Caught ServerError getting feed report {feed_id}: {e}") except ObjectNotFoundError: LOGGER.warning(f"No feed {feed_id} or report {report_id} in the feed")
def get_feed(cb: CbThreatHunterAPI, feed_id: str) -> Dict: """Get a specific feed by ID.""" url = f"/threathunter/feedmgr/v2/orgs/{cb.credentials.org_key}/feeds" try: return cb.get_object(f"{url}/{feed_id}") except ServerError: LOGGER.error(f"Caught ServerError getting feed {feed_id}: {e}") except ObjectNotFoundError: LOGGER.warning(f"No feed by feed id {feed_id}")
def get_watchlist(cb: CbThreatHunterAPI, watchlist_id): """Get a watchlist by ID.""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/watchlists" try: return cb.get_object(f"{url}/{watchlist_id}") except ServerError: LOGGER.error(f"Caught ServerError getting watchlist {watchlist_id}: {e}") except ObjectNotFoundError: LOGGER.warning(f"No watchlist with ID {watchlist_id}")
def get_all_feeds(cb: CbThreatHunterAPI, include_public=True) -> Dict: """Retrieve all feeds owned by the caller. Provide include_public=true parameter to also include public community feeds. """ url = f"/threathunter/feedmgr/v2/orgs/{cb.credentials.org_key}/feeds" params = {"include_public": include_public} result = cb.get_object(url, query_parameters=params) return result.get("results", [])
def get_report(cb: CbThreatHunterAPI, report_id) -> Dict: """Get report by report id.""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/reports/{report_id}" try: report = cb.get_object(url) report["ignored"] = get_report_status(cb, report["id"])["ignored"] return report except ServerError: LOGGER.error(f"Caught ServerError getting report {report_id}: {e}") except ObjectNotFoundError: LOGGER.warning(f"report {report_id} does not exist")
def get_command_result(cb: CbThreatHunterAPI, session_id: str, command_id: str): """Get results of a LR session command.""" try: return cb.get_object( f"{CBLR_BASE}/session/{session_id}/command/{command_id}") except ObjectNotFoundError: LOGGER.warning( f"no live resonse session and/or command combination for {session_id}:{command_id}" ) return None
def create_watchlist(cb: CbThreatHunterAPI, watchlist_data: Dict): url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/watchlists" try: result = cb.post_object(url, watchlist_data) except ServerError as e: LOGGER.error(f"Caught ServerError creating watchlist: {e}") return False except ClientError as e: LOGGER.warning(f"got ClientError creating watchlist: {e}") return False return result.json()
def create_report(cb: CbThreatHunterAPI, report_data) -> Dict: """Create an intel Report.""" url = f"/threathunter/watchlistmgr/v3/orgs/{cb.credentials.org_key}/reports" try: result = cb.post_object(url, report_data) except ServerError as e: LOGGER.error(f"Caught ServerError creating report: {e}") return False try: return result.json() except ValueError: return False
def make_device_query(cb: CbThreatHunterAPI, device_query: str) -> DeviceSearchQuery: """Construct a DeviceSearchQuery object.""" try: if ":" not in device_query: LOGGER.info( "No field specification passed. Use 'FIELDS' for help.") devices = cb.select(Device).where(device_query) except ValueError as e: LOGGER.error(f"{e}") return False LOGGER.info(f"got {len(devices)} device results.") return devices
def toggle_device_quarantine(cb: CbThreatHunterAPI, devices: Union[DeviceSearchQuery, List[Device]], quarantine: bool) -> bool: """Toggle device quarantine state. Args: devices: DeviceSearchQuery quarantine: set quarantine if True, else set quarantine to off state. """ if len(devices) > 0: if len(devices) > 10 and quarantine: LOGGER.error( f"For now, not going to quarnantine {len(devices)} devices as a safe gaurd " f"to prevent mass device impact... use the GUI if you must.") return False verbiage = "quarantine" if quarantine else "NOT quarantine" emotion = "ЪЉђ" if quarantine else "ЪЉЈ" LOGGER.info( f"setting {verbiage} on {len(devices)} devices... {emotion}") device_ids = [] for d in devices: if d.quarantined == quarantine: LOGGER.warning( f"device {d.id}:{d.name} is already set to {verbiage}.") continue if not is_device_online(d): LOGGER.info( f"device {d.id}:{d.name} hasn't checked in for: {time_since_checkin(d, refresh=False)}" ) LOGGER.warning(f"device {d.id}:{d.name} appears offline Ъњц") LOGGER.info( f"device {d.id}:{d.name} will change quarantine state when it comes online ЪЉї" ) device_ids.append(d.id) cb.device_quarantine(device_ids, quarantine) return True
def get_event_search_results(cb: CbThreatHunterAPI, job_id) -> Dict: """Return any results of an event search.""" url = f"/api/investigate/v2/orgs/{cb.credentials.org_key}/enriched_events/search_jobs/{job_id}/results" try: while not event_search_complete(cb, job_id): time.sleep(0.1) except Exception as e: LOGGER.error( f"got exception waiting for event search to complete: {e}") return None try: return cb.get_object(url) except Exception as e: LOGGER.error("could not get results: {e}") return None
def is_valid_process_query_string(cb: CbThreatHunterAPI, query: str) -> bool: """ Validates a process query string is valid for PSC. Args: cb: Cb PSC connection object query (str): The query. Returns: True or False """ args = {"q": query} url = f"/api/investigate/v1/orgs/{cb.credentials.org_key}/processes/search_validation" validated = cb.get_object(url, query_parameters=args) if not validated.get("valid"): return False return True
def __init__(self, cb, timeout=30, custom_session_keepalive=False): # First, get a CB object with the LR API permissions cblr = CbThreatHunterAPI(url=cb.credentials.url, token=cb.credentials.lr_token, org_key=cb.credentials.org_key) super().__init__(cblr, timeout=timeout, keepalive_sessions=False) # so now self._cb == cblr -- store a reference to the regular cb self.psc_cb = cb if custom_session_keepalive: self._cleanup_thread = threading.Thread( target=self._keep_active_sessions_alive_thread) self._cleanup_thread.daemon = True self._cleanup_thread.start() # for storing initiatied commands self.commands = []