├── setup.cfg ├── pyproject.toml ├── .gitignore ├── setup.py ├── LICENSE ├── README.md └── InsightIDR4Py.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md 3 | license_files = LICENSE -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=43.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *~ 10 | 11 | # due to using tox and pytest 12 | .tox 13 | .cache -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="InsightIDR4Py", 8 | version="0.3.1", 9 | description="A Python client allowing simplified interaction with Rapid7's InsightIDR REST API.", 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | license="MIT", 13 | author="Micah Babinski", 14 | author_email="m.babinski.88@gmail.com", 15 | py_modules=["InsightIDR4Py"], 16 | url="https://github.com/mbabinski/InsightIDR4Py", 17 | keywords="Rapid7, InsightIDR, SIEM, Logsearch, Investigations, Threats, Alerts", 18 | install_requires=[ 19 | "requests", 20 | ], 21 | 22 | ) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Micah Babinski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InsightIDR4Py 2 | A Python client allowing simplified interaction with Rapid7's InsightIDR REST API. 3 | 4 | InsightIDR4Py allows users to perform numerous actions within Rapid7 [InsightIDR](https://docs.rapid7.com/insightidr/). This tool handles some of the challenges and complexities of using the InsightIDR REST API, including polling queries in progress, paginated responses, handling the JSON output, and time range queries. 5 | 6 | These capabilities can be particularly useful for automating processes, integrating log data with other APIs (like VirusTotal), managing content in the InsightIDR platform, and performing multi-tenant workflows (for instance, updating content across tenants for consistency, or copying content from one InsightIDR tenant to another). For some ideas on how InsightIDR4Py can be used, check out this [blog post](https://micahbabinski.medium.com/button-pusher-to-masterbuilder-automating-siem-workflows-3f51874a80e) where I cover some use cases. 7 | 8 | The API capabilities provided by InsightIDR4Py include: 9 | ## Logsearch 10 | * Query Events 11 | * Query Groups 12 | 13 | ## Saved Queries 14 | * List Saved Queries 15 | * Get a Saved Query 16 | * Create Saved Query 17 | * Replace a Saved Query 18 | * Update a Saved Query 19 | * Delete a Saved Query 20 | 21 | ## Custom Alerts* 22 | * List Custom Alerts 23 | * Get a Custom Alert 24 | * Create Custom Alert 25 | * Replace a Custom Alert 26 | * Update a Custom Alert 27 | * Delete a Custom Alert 28 | 29 | *Only pattern detection alerts are supported currently. 30 | 31 | ## Investigations 32 | * List Investigations 33 | * Get an Investigation 34 | * Create Investigation 35 | * Close Investigations in Bulk 36 | * List Alerts by Investigation 37 | * List Rapid7 Product Alerts by Investigation 38 | * Update Investigation 39 | * List Comments on an Investigation 40 | * Create Comment 41 | * Delete Comment 42 | 43 | ## Threats 44 | * Create Threat 45 | * Add Indicators to Threat 46 | * Replace Threat Indicators 47 | * Delete Threat 48 | 49 | Happy analyzing :monocle_face: and happy administering! :hammer: 50 | 51 | # Installation 52 | InsightIDR4Py is available on [PyPI](https://pypi.org/project/InsightIDR4Py/) and can be installed using: 53 | ``` 54 | pip install InsightIDR4Py 55 | ``` 56 | 57 | # Prerequisites 58 | You will need obtain an API key from the InsightIDR system. The documentation for this can be found [here](https://docs.rapid7.com/insight/managing-platform-api-keys/). From there, you'll use this API key value to create the InsightIDR API object as shown below: 59 | ```python 60 | import InsightIDR4Py as idr 61 | 62 | # define API key (store this value securely) 63 | api_key = "API_Key_Here" 64 | 65 | # create the InsightIDR object 66 | api = idr.InsightIDR(api_key) 67 | ``` 68 | Remember to store the API key securely! There are several ways to do this, and you should make sure that the way you choose aligns with your organization's security policy. Python's [keyring](https://pypi.org/project/keyring/) library is one possibility. 69 | 70 | # Examples 71 | ## Example 1: Query DNS Logs for Suspicious TLDs 72 | ```python 73 | import InsightIDR4Py as idr 74 | 75 | # create the InsightIDR object 76 | api = idr.InsightIDR(api_key) 77 | 78 | # define the query parameters 79 | logset_name = "DNS Query" 80 | query = "where(public_suffix IN [buzz, top, club, work, surf, tw, gq, ml, cf, biz, tk, cam, xyz, bond])" 81 | time_range = "Last 36 Hours" 82 | 83 | # query the logs 84 | events = api.QueryEvents(logset_name, query, time_range) 85 | 86 | # print out an event 87 | print(event[0]) 88 | ``` 89 | Result: 90 | ```python 91 | {'timestamp': '2021-09-28T15:11:45.000Z', 'asset': 'windesk05.organization.com', 'source_address': '192.168.4.10', 'query': 'regulationprivilegescan.top', 'public_suffix': 'top', 'top_private_domain': 'regulationprivilegescan.top', 'query_type': 'A', 'source_data': '09/28/2021 8:11:45 AM 1480 PACKET 00000076ED1A0140 UDP Rcv 192.168.4.121 c3b3 Q [0001 D NOERROR] A (3)regulationprivilegescan(3)top(0)'} 92 | ``` 93 | 94 | ## Example 2: Query Authentication Logs for top Five Failed Logins, Grouped by Count 95 | ```python 96 | import InsightIDR4Py as idr 97 | 98 | # create the InsightIDR object 99 | api = idr.InsightIDR(api_key) 100 | 101 | # define the query parameters 102 | logset_name = "Asset Authentication" 103 | query = "where(source_json.eventCode = 4625) groupby(destination_account) limit(5)" 104 | time_range = "Last 24 Hours" 105 | 106 | # query the logs 107 | groups = api.QueryGroups(logset_name, query, time_range) 108 | 109 | # print out the groups 110 | for group in groups.items(): 111 | print(group) 112 | ``` 113 | Result: 114 | ``` 115 | ('Mark.Corrigan', 132) 116 | ('Jeremy.Usborne', 102) 117 | ('Sophie.Chapman', 88) 118 | ('Alan.Johnson', 64) 119 | ('Super.Hans', 24) 120 | ``` 121 | 122 | ## Example 3: Query VPN Logins from a Certain IP Range and Check the Results Using [AbuseIPDB](https://www.abuseipdb.com/) 123 | This example uses [python-abuseipdb](https://github.com/meatyite/python-abuseipdb), a Python object oriented wrapper for AbuseIPDB v2 API. 124 | 125 | It requires an API key, which you can get by creating a free account. From there, go to User Account > API, choose Create Key, and enter this string into the abuse_ip_db_api_key variable in the example below. 126 | 127 | The same API key security principles mentioned above apply here. Guard your API keys to prevent rogue usage! 128 | 129 | ```python 130 | import InsightIDR4Py as idr 131 | import abuseipdb import * 132 | 133 | # create the InsightIDR object 134 | api = idr.InsightIDR(api_key) 135 | 136 | # define the AbuseIPDB API key 137 | abuse_ip_db_api_key = "YOUR_KEY_HERE" 138 | 139 | # define the query parameters 140 | logset_name = "Ingress Authentication" 141 | query = "where(service = vpn AND source_ip = IP(64.62.128.0/17))" 142 | time_range = "Last 24 Hours" 143 | 144 | # query the logs 145 | events = api.QueryEvents(logset_name, query, time_range) 146 | 147 | # check the source IP addresses in AbuseIPDB and display results 148 | if len(events) > 0: 149 | ipdb = AbuseIPDB(abuse_ip_db_api_key) 150 | for event in events: 151 | check = ipdb.check(event["source_ip"]) 152 | print("----------") 153 | print("IP Address: " + ip_check.ipAddress) 154 | print("Last reported at: " + ip_check.lastReportedAt) 155 | print("Abuse confidence score: " + str(ip_check.abuseConfidenceScore)) 156 | print("Abuser country: " + ip_check.countryName) 157 | print("Abuser ISP: " + ip_check.isp) 158 | print("Total reports of abuser: " + str(ip_check.totalReports)) 159 | print("----------") 160 | ``` 161 | 162 | # License 163 | This repository is licensed under an [MIT license](https://github.com/mbabinski/InsightIDR4Py/blob/main/LICENSE), which grants extensive permission to use this material however you wish. 164 | 165 | # Contributing 166 | You are welcome to contribute however you wish! I appreciate feedback in any format. -------------------------------------------------------------------------------- /InsightIDR4Py.py: -------------------------------------------------------------------------------- 1 | import requests, json, time 2 | from datetime import datetime, timedelta, timezone 3 | 4 | def GetDefaultStartTime(): 5 | """ 6 | Get default start time for time-based queries. 7 | """ 8 | default_start_time = (datetime.now(timezone.utc) - timedelta(28)).strftime("%Y-%m-%dT%H:%M:%SZ") 9 | 10 | return default_start_time 11 | 12 | def GetDefaultEndTime(): 13 | """ 14 | Get default end time (now) for time-based queries. 15 | """ 16 | default_end_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") 17 | 18 | return default_end_time 19 | 20 | class InsightIDR(object): 21 | def __init__(self, api_key, region=None): 22 | self.session = requests.Session() 23 | self.session.headers = {"X-Api-Key": api_key} 24 | if not region: 25 | self.region = self._get_region() 26 | else: 27 | self.region = region 28 | self.logs_url = "https://{}.rest.logs.insight.rapid7.com/query/logs/".format(self.region) 29 | self.query_url = "https://{}.rest.logs.insight.rapid7.com/query/".format(self.region) 30 | self.log_mgmt_url = "https://{}.api.insight.rapid7.com/log_search/management/logs/".format(self.region) 31 | self.investigations_url = "https://{}.api.insight.rapid7.com/idr/v2/investigations/".format(self.region) 32 | self.comments_url = "https://{}.api.insight.rapid7.com/idr/v1/comments/".format(self.region) 33 | self.threat_url = "https://{}.api.insight.rapid7.com/idr/v1/customthreats/".format(self.region) 34 | self.mgmt_url = "https://{}.rest.logs.insight.rapid7.com/management/".format(self.region) 35 | 36 | def _get_region(self): 37 | """ 38 | This method cycles through available API regions, making a call to the log management URL with each 39 | region until a successful call indicates the correct region. If you already know your region, simply 40 | pass that in when creating the InsightIDR object. 41 | """ 42 | self.regions = ["us", "us2", "us3", "eu", "ca", "au", "ap"] 43 | for region in self.regions: 44 | self.response = self.session.get("https://{}.rest.logs.insight.rapid7.com/management/logs".format(region)) 45 | if self.response.status_code == 200: 46 | return region 47 | 48 | def GetLogInfo(self): 49 | """Returns metadata about the available log sources.""" 50 | response = self.session.get(self.log_mgmt_url).json()["logs"] 51 | 52 | return response 53 | 54 | def ListLogSetNames(self): 55 | """Returns a list of logset names as they appear in the InsightIDR console.""" 56 | log_info = self.GetLogInfo() 57 | logset_names = list(set([log["logsets_info"][0]["name"] for log in log_info])) 58 | 59 | return sorted(logset_names) 60 | 61 | def ListLogIdsByLogSetName(self, logset_name): 62 | """Returns a list of log ID values for a given logset name.""" 63 | log_info = self.GetLogInfo() 64 | log_ids = [log["id"] for log in log_info if log["logsets_info"][0]["name"].upper() == logset_name.upper()] 65 | 66 | return log_ids 67 | 68 | def QueryEvents(self, logset_name, query, time_range="Last 20 Minutes", from_time=None, to_time=None, suppress_msgs=True): 69 | """ 70 | Returns an ordered list of events matching a given timeframe, logset name, and query. Must supply 71 | either a relative time range or from time and to time in the format MM/DD/YYYY Hr:Min:Sec. 72 | """ 73 | # convert from/to times as necessary (string to timestamp with milliseconds) 74 | if not time_range: 75 | from_time = int(datetime.strptime(from_time, "%m/%d/%Y %H:%M:%S").timestamp()) * 1000 76 | to_time = int(datetime.strptime(to_time, "%m/%d/%Y %H:%M:%S").timestamp()) * 1000 77 | 78 | # get the relevant Log IDs 79 | log_ids = self.ListLogIdsByLogSetName(logset_name) 80 | # get the time range 81 | if time_range: 82 | during = {"time_range": time_range} 83 | else: 84 | during = {"from": from_time, "to": to_time} 85 | body = {"logs": log_ids, 86 | "leql": {"during": during, 87 | "statement": query}} 88 | 89 | # build the first full URL 90 | url = self.logs_url + "?per_page=500" 91 | 92 | # retrieve the data 93 | run = True 94 | events = [] 95 | cntr = 1 96 | r = self.session.post(url, json=body) 97 | while run: 98 | if r.status_code == 202: 99 | cont = True 100 | while cont: 101 | continue_url = r.json()["links"][0]["href"] 102 | r = self.session.get(continue_url, headers=headers) 103 | if r.status_code != 202: 104 | cont = False 105 | break 106 | elif r.status_code == 200: 107 | events.extend(r.json()["events"]) 108 | if "links" in r.json(): 109 | continue_url = r.json()["links"][0]["href"] 110 | r = self.session.get(continue_url, headers=headers) 111 | else: 112 | run = False 113 | else: 114 | raise ValueError("Query failed without a normal status code. Status code returned was: " + str(r.status_code)) 115 | return 116 | cntr += 1 117 | if not suppress_msgs: 118 | if cntr % 30 == 0: 119 | print("-Gathered {} events.".format(str(len(events)))) 120 | 121 | # filter the event objects to get just the dictionary representation of the event data 122 | events = [json.loads(event["message"]) for event in events] 123 | 124 | return events 125 | 126 | def QueryGroups(self, logset_name, query, time_range="Last 20 Minutes", from_time=None, to_time=None, suppress_msgs=True): 127 | """ 128 | Retrieves group values and associated stats. Query must contain a groupby() clause 129 | """ 130 | # validate input query 131 | if not "groupby(" in query.lower(): 132 | raise ValueError("Query must contain the groupby() clause!") 133 | 134 | # convert from/to times as necessary (string to timestamp with milliseconds) 135 | if not time_range: 136 | from_time = int(datetime.strptime(from_time, "%m/%d/%Y %H:%M:%S").timestamp()) * 1000 137 | to_time = int(datetime.strptime(to_time, "%m/%d/%Y %H:%M:%S").timestamp()) * 1000 138 | 139 | # get the relevant Log IDs 140 | log_ids = self.ListLogIdsByLogSetName(logset_name) 141 | 142 | # get the time range 143 | if time_range: 144 | during = {"time_range": time_range} 145 | else: 146 | during = {"from": from_time, "to": to_time} 147 | body = {"logs": log_ids, 148 | "leql": {"during": during, 149 | "statement": query}} 150 | 151 | # build the first full URL 152 | url = self.logs_url 153 | 154 | # retrieve the data 155 | run = True 156 | results = [] 157 | cntr = 1 158 | r = self.session.post(url, json=body) 159 | while run: 160 | if r.status_code == 202: 161 | cont = True 162 | while cont: 163 | continue_url = r.json()["links"][0]["href"] 164 | r = self.session.get(continue_url) 165 | if r.status_code != 202: 166 | cont = False 167 | break 168 | elif r.status_code == 200: 169 | if "links" in r.json(): 170 | continue_url = r.json()["links"][0]["href"] 171 | r = self.session.get(continue_url) 172 | else: 173 | results.extend(r.json()["statistics"]["groups"]) 174 | run = False 175 | else: 176 | raise ValueError("Query failed without a normal status code. Status code returned was: " + str(r.status_code)) 177 | return 178 | cntr += 1 179 | if not suppress_msgs: 180 | if cntr % 30 == 0: 181 | print("-Gathered {} groups.".format(str(len(results)))) 182 | 183 | groups = {} 184 | for result in results: 185 | key = list(result.keys())[0] 186 | value = int(result[key]["count"]) 187 | groups[key] = value 188 | 189 | return groups 190 | 191 | def ListInvestigations(self, 192 | assignee_email=None, 193 | start_time=GetDefaultStartTime(), 194 | end_time=GetDefaultEndTime(), 195 | multi_customer=False, 196 | priorities=["LOW", "MEDIUM", "HIGH", "CRITICAL"], 197 | sort="priority,DESC", 198 | sources=None, 199 | statuses=["OPEN", "INVESTIGATING", "CLOSED"], 200 | tags=None): 201 | """ 202 | Queries InsightIDR investigations based on available filter criteria. 203 | """ 204 | # list to hold investigations 205 | investigations = [] 206 | # pre-process parameters 207 | priorities = ", ".join(priorities) 208 | statuses = ",".join(statuses) 209 | if tags: 210 | tags = ", ".join(tags) 211 | params = { 212 | "index": 0, 213 | "size": 100, 214 | "assignee.email": assignee_email, 215 | "start_time": start_time, 216 | "end_time": end_time, 217 | "multi-customer": multi_customer, 218 | "priorities": priorities, 219 | "sort": sort, 220 | "sources": sources, 221 | "statuses": statuses, 222 | "tags": tags 223 | } 224 | # filter the parameters to be only those with a supplied value 225 | params = {key:val for key, val in params.items() if val} 226 | if 'index' not in params: 227 | params["index"] = 0 228 | # get the initial set of investigations 229 | url = self.investigations_url 230 | self.session.headers["Accept-version"] = "investigations-preview" 231 | response = self.session.get(url, params=params) 232 | result = response.json() 233 | # get the total 234 | total = result["metadata"]["total_data"] 235 | # add the results to the output list 236 | investigations.extend(result["data"]) 237 | # iterate through remaining investigations and add them to the output list 238 | while len(investigations) < total: 239 | params["index"] += 1 240 | response = self.session.get(url, params=params) 241 | result = response.json() 242 | investigations.extend(result["data"]) 243 | 244 | # return the result 245 | return investigations 246 | 247 | def GetInvestigation(self, investigation_id, multi_customer=False): 248 | """ 249 | Retrieves a single investigation by Investigation ID/RRN 250 | """ 251 | url = self.investigations_url + "{}".format(investigation_id) 252 | params = {"multi-customer": multi_customer} 253 | self.session.headers["Accept-version"] = "investigations-preview" 254 | response = self.session.get(url, params=params) 255 | result = response.json() 256 | 257 | return result 258 | 259 | def CreateInvestigation(self, title, assignee_email=None, disposition="UNDECIDED", 260 | priority="LOW", status="OPEN"): 261 | """ 262 | Creates an InsightIDR investigation. 263 | """ 264 | if assignee_email: 265 | assignee = {"email": assignee_email} 266 | else: 267 | assignee = None 268 | data = { 269 | "title": title, 270 | "assignee": assignee, 271 | "disposition": disposition, 272 | "priority": priority, 273 | "status": status 274 | } 275 | # filter the parameters to be only those with a supplied value 276 | data = {key:val for key, val in data.items() if val} 277 | 278 | # submit the request 279 | url = self.investigations_url 280 | self.session.headers["Accept-version"] = "investigations-preview" 281 | response = self.session.post(url, json=data) 282 | result = response.json() 283 | 284 | return result 285 | 286 | def CloseInvestigationsInBulk(self, source, from_time=GetDefaultStartTime(), 287 | to_time=GetDefaultEndTime(), alert_type=None, 288 | disposition=None, detection_rule_rrn=None, 289 | max_investigations_to_close=None): 290 | """ 291 | Closes investigations in bulk according to selected criteria. 292 | """ 293 | data = { 294 | "source": source.upper(), 295 | "from": from_time, 296 | "to": to_time, 297 | "alert_type": alert_type, 298 | "disposition": disposition, 299 | "detection_rule_rrn": detection_rule_rrn, 300 | "max_investigations_to_close": max_investigations_to_close 301 | } 302 | 303 | # validate input 304 | if source.upper() not in ("ALERT", "MANUAL", "HUNT"): 305 | raise ValueError("Source must be one of [ALERT, MANUAL, or HUNT]!") 306 | if source.upper() == "ALERT" and not alert_type: 307 | raise ValueError("The alert_type parameter is required when source is ALERT!") 308 | if detection_rule_rrn and alert_type != "Attacker Behavior Detected": 309 | raise ValueError("If a detection rule RRN is specified, the alert type must be 'Attacker Behavior Detected'") 310 | 311 | # filter the parameters to be only those with a supplied value 312 | data = {key:val for key, val in data.items() if val} 313 | 314 | # submit the request 315 | url = self.investigations_url + "bulk_close" 316 | self.session.headers["Accept-version"] = "investigations-preview" 317 | response = self.session.post(url, json=data) 318 | result = response.json() 319 | 320 | return result 321 | 322 | def ListAlertsByInvestigation(self, investigation_id, multi_customer=False): 323 | """ 324 | Retrieves all alerts associated with an investigation. The listed alerts are sorted in descending order by alert create time. 325 | """ 326 | alerts = [] 327 | url = self.investigations_url + "{}/alerts".format(investigation_id) 328 | params = { 329 | "index": 0, 330 | "size": 100, 331 | "multi-customer": multi_customer 332 | } 333 | # make initial request 334 | self.session.headers["Accept-version"] = "investigations-preview" 335 | response = self.session.get(url, params=params) 336 | result = response.json() 337 | # get the total 338 | total = result["metadata"]["total_data"] 339 | # add the results to the output list 340 | alerts.extend(result["data"]) 341 | # iterate through remaining alerts and add them to the output list 342 | while len(alerts) < total: 343 | params["index"] += 100 344 | response = self.session.get(url, params) 345 | result = response.json() 346 | alerts.extend(result["data"]) 347 | 348 | return alerts 349 | 350 | def ListRapid7ProductAlertsByInvestigation(self, investigation_id, multi_customer=False): 351 | """ 352 | Retrieves all Rapid7 product alerts associated with an investigation. These alerts are generated by Rapid7 products other 353 | than InsightIDR that you have an active license for. 354 | """ 355 | product_alerts = [] 356 | url = self.investigations_url + "{}/rapid7-product-alerts".format(investigation_id) 357 | params = {"multi-customer": multi_customer} 358 | self.session.headers["Accept-version"] = "investigations-preview" 359 | response = self.session.get(url, params=params) 360 | result = response.json() 361 | 362 | return result 363 | 364 | def UpdateInvestigation(self, investigation_id, multi_customer=False, assignee_email=None, disposition=None, priority=None, 365 | status=None, threat_command_close_reason=None, threat_command_free_text=None, title=None): 366 | """ 367 | Updates multiple fields in a single operation for an investigation, specified by id or rrn. 368 | The investigation will be returned with its changed fields. Null or omitted fields will not have their values 369 | updated in the investigation. 370 | """ 371 | if assignee_email: 372 | assignee = {"email": assignee_email} 373 | else: 374 | assignee = None 375 | params = {"multi-customer": multi_customer} 376 | data = { 377 | "title": title, 378 | "assignee": assignee, 379 | "disposition": disposition, 380 | "priority": priority, 381 | "status": status, 382 | "threat_command_close_reason": threat_command_close_reason, 383 | "threat_command_free_text": threat_command_free_text 384 | } 385 | # submit the request 386 | url = self.investigations_url + investigation_id 387 | self.session.headers["Accept-version"] = "investigations-preview" 388 | response = self.session.patch(url, json=data, params=params) 389 | result = response.json() 390 | 391 | return result 392 | 393 | def ListCommentsByInvestigation(self, investigation_rrn): 394 | """ 395 | Returns a list of comments filtered by a specific investigation with a given rrn. 396 | """ 397 | comments = [] 398 | url = self.comments_url 399 | params = { 400 | "index": 0, 401 | "size": 100, 402 | "target": investigation_rrn 403 | } 404 | 405 | # make initial request 406 | self.session.headers["Accept-version"] = "comments-preview" 407 | response = self.session.get(url, params=params) 408 | result = response.json() 409 | # get the total 410 | total = result["metadata"]["total_data"] 411 | # add the results to the output list 412 | comments.extend(result["data"]) 413 | # iterate through remaining alerts and add them to the output list 414 | while len(comments) < total: 415 | params["index"] += 100 416 | response = self.session.get(url, params) 417 | result = response.json() 418 | comments.extend(result["data"]) 419 | 420 | return comments 421 | 422 | def CreateComment(self, investigation_rrn, comment_text): 423 | """ 424 | Creates a comment on an investigation. 425 | """ 426 | data = { 427 | "attachments": [], # attachments not yet supported 428 | "body": comment_text, 429 | "target": investigation_rrn 430 | } 431 | url = self.comments_url 432 | self.session.headers["Accept-version"] = "comments-preview" 433 | response = self.session.post(url, json=data) 434 | result = response.json() 435 | 436 | return result 437 | 438 | def DeleteComment(self, comment_rrn): 439 | """ 440 | Deletes a comment identified by its rrn value. 441 | """ 442 | url = self.comments_url + "{}".format(comment_rrn) 443 | response = self.session.delete(url) 444 | 445 | return response 446 | 447 | def CreateThreat(self, threat_name, threat_description, indicators={}): 448 | """ 449 | CreateS a private InsightIDR Community Threat and optionally adds indicators to this Community Threat. 450 | Indicator types can include IP addresses, hashes, domain names, or URLs. 451 | """ 452 | data = { 453 | "threat": threat_name, 454 | "note": threat_description, 455 | "indicators": indicators, 456 | } 457 | url = self.threat_url 458 | response = self.session.post(url, json=data) 459 | result = response.json() 460 | 461 | return result 462 | 463 | def AddIndicatorsToThreat(self, threat_key, ips=None, domains=None, hashes=None, urls=None): 464 | """ 465 | Adds indicators to a threat based off threat key. The threat key can be obtained on the threat page in the InsightIDR GUI. 466 | Only JSON formatted indicators are supported at this time. 467 | """ 468 | params = {"format": "json"} 469 | data = {} 470 | if ips: 471 | data["ips"] = ips 472 | if domains: 473 | data["domain_names"] = domains 474 | if hashes: 475 | data["hashes"] = hashes 476 | if urls: 477 | data["urls"] = urls 478 | url = self.threat_url + "key/{}/indicators/add".format(threat_key) 479 | 480 | response = self.session.post(url, params=params, json=data) 481 | result = response.json() 482 | 483 | return result 484 | 485 | def ReplaceThreatIndicators(self, threat_key, ips=None, domains=None, hashes=None, urls=None): 486 | """ 487 | Replaces indicators in a threat abased off threat key. The threat key can be obtained on the threat page in the InsightIDR GUI. 488 | Only JSON formatted indicators are supported at this time. 489 | """ 490 | params = {"format": "json"} 491 | data = {} 492 | if ips: 493 | data["ips"] = ips 494 | if domains: 495 | data["domain_names"] = domains 496 | if hashes: 497 | data["hashes"] = hashes 498 | if urls: 499 | data["urls"] = urls 500 | url = self.threat_url + "key/{}/indicators/replace".format(threat_key) 501 | 502 | response = self.session.post(url, params=params, json=data) 503 | result = response.json() 504 | 505 | return result 506 | 507 | def DeleteThreat(self, threat_key, reason=""): 508 | """ 509 | Deletes an InsightIDR Community Threat. The threat key can be obtained on the threat page in the InsightIDR GUI. 510 | """ 511 | data = {"reason": reason} 512 | url = self.threat_url + "/key/{}/delete".format(threat_key) 513 | response = self.session.post(url, json=data) 514 | result = response.json() 515 | 516 | return result 517 | 518 | def ListCustomAlerts(self): 519 | """ 520 | Lists all 'Tags' aka alerts, associated with the account. 521 | """ 522 | url = self.mgmt_url + "tags" 523 | response = self.session.get(url) 524 | result = response.json() 525 | alerts = result["tags"] 526 | 527 | return alerts 528 | 529 | def GetCustomAlert(self, custom_alert_id): 530 | """ 531 | Retrieve a single custom alert by its ID value 532 | """ 533 | url = self.mgmt_url + "tags/{}".format(custom_alert_id) 534 | response = self.session.get(url) 535 | result = response.json() 536 | alert = result["tag"] 537 | 538 | return alert 539 | 540 | def CreateCustomAlert(self, name, logsource, query, description="", actions=[], labels=[], 541 | priority="low"): 542 | """ 543 | Create a custom alert. If log source is set to an array of logsource IDs, these will be used. If a logset name is provided, 544 | the log IDs for this log set will be retrieved and used. 545 | """ 546 | # validate input 547 | if not priority.lower() in ("low", "medium", "high", "critical"): 548 | raise ValueError("Priority must be one of [low, medium, high, critical]!") 549 | 550 | # map alert priority to numeric value 551 | priority_mapping = { 552 | "low": 1, 553 | "medium": 2, 554 | "high": 3, 555 | "critical": 4 556 | } 557 | priority = priority_mapping[priority.lower()] 558 | 559 | # list logsource IDs 560 | if type(logsource) == list: 561 | logsource_ids = [{"id": item} for item in logsource] 562 | else: 563 | logsource_ids = [{"id": item} for item in self.ListLogIdsByLogSetName(logsource)] 564 | 565 | # format parameters 566 | data = { 567 | "tag": { 568 | "name": name, 569 | "sources": logsource_ids, 570 | "type": "Alert", 571 | "leql": {"statement": query}, 572 | "description": description, 573 | "actions": actions, 574 | "labels": labels, 575 | "priority": priority 576 | } 577 | } 578 | 579 | # send request 580 | self.session.headers["Content-type"] = "application/json" 581 | url = self.mgmt_url + "tags" 582 | response = self.session.post(url, json=data) 583 | result = response.json() 584 | 585 | return result 586 | 587 | def ReplaceCustomAlert(self, alert_id, name, logsource, query, description="", actions=[], labels=[], 588 | priority="low"): 589 | """ 590 | Replaces a custom alert identified by the alert_id value. If log source is set to an array of logsource IDs, these will be used. 591 | If a logset name is provided, the log IDs for this log set will be retrieved and used. 592 | """ 593 | # validate input 594 | if not priority.lower() in ("low", "medium", "high", "critical"): 595 | raise ValueError("Priority must be one of [low, medium, high, critical]!") 596 | 597 | # map alert priority to numeric value 598 | priority_mapping = { 599 | "low": 1, 600 | "medium": 2, 601 | "high": 3, 602 | "critical": 4 603 | } 604 | priority = priority_mapping[priority.lower()] 605 | 606 | # list logsource IDs 607 | if type(logsource) == list: 608 | logsource_ids = [{"id": item} for item in logsource] 609 | else: 610 | logsource_ids = [{"id": item} for item in self.ListLogIdsByLogSetName(logsource)] 611 | 612 | # format parameters 613 | data = { 614 | "tag": { 615 | "name": name, 616 | "sources": logsource_ids, 617 | "type": "Alert", 618 | "leql": {"statement": query}, 619 | "description": description, 620 | "actions": actions, 621 | "labels": labels, 622 | "priority": priority 623 | } 624 | } 625 | 626 | # send request 627 | self.session.headers["Content-type"] = "application/json" 628 | url = self.mgmt_url + "tags/{}".format(alert_id) 629 | response = self.session.put(url, json=data) 630 | result = response.json() 631 | 632 | return result 633 | 634 | def UpdateCustomAlert(self, alert_id, name=None, logsource=None, query=None, description=None, actions=None, labels=None, 635 | priority=None): 636 | """ 637 | Updates any user-specified element of a custom alert, identified by the alert_id 638 | """ 639 | if priority: 640 | # validate input 641 | if not priority.lower() in ("low", "medium", "high", "critical"): 642 | raise ValueError("Priority must be one of [low, medium, high, critical]!") 643 | 644 | # map alert priority to numeric value 645 | priority_mapping = { 646 | "low": 1, 647 | "medium": 2, 648 | "high": 3, 649 | "critical": 4 650 | } 651 | priority = priority_mapping[priority.lower()] 652 | 653 | # list logsource IDs 654 | if logsource: 655 | if type(logsource) == list: 656 | logsource_ids = [{"id": item} for item in logsource] 657 | else: 658 | logsource_ids = [{"id": item} for item in self.ListLogIdsByLogSetName(logsource)] 659 | else: 660 | logsource_ids = None 661 | 662 | if query: 663 | leql_obj = {"statement": query} 664 | else: 665 | leql_obj = None 666 | 667 | # format parameters 668 | params = { 669 | "name": name, 670 | "sources": logsource_ids, 671 | "type": "Alert", 672 | "leql": leql_obj, 673 | "description": description, 674 | "actions": actions, 675 | "labels": labels, 676 | "priority": priority 677 | } 678 | params = {k:v for k, v in params.items() if v} 679 | data = {"tag": params} 680 | 681 | # send request 682 | self.session.headers["Content-type"] = "application/json" 683 | url = self.mgmt_url + "tags/{}".format(alert_id) 684 | response = self.session.patch(url, json=data) 685 | result = response.json() 686 | 687 | return result 688 | 689 | def DeleteCustomAlert(self, alert_id): 690 | """ 691 | Deletes the custom alert identified by the alert_id 692 | """ 693 | url = self.mgmt_url + "tags/{}".format(alert_id) 694 | response = self.session.delete(url) 695 | 696 | return response 697 | 698 | def ListSavedQueries(self): 699 | """ 700 | Lists saved queries in the InsightIDR platform. 701 | """ 702 | url = self.query_url + "saved_queries" 703 | response = self.session.get(url) 704 | result = response.json() 705 | queries = result["saved_queries"] 706 | 707 | return queries 708 | 709 | def GetSavedQuery(self, saved_query_id): 710 | """ 711 | Retrieve details on a single saved query. 712 | """ 713 | url = self.query_url + "saved_queries/{}".format(saved_query_id) 714 | response = self.session.get(url) 715 | result = response.json()["saved_query"] 716 | 717 | return result 718 | 719 | def CreateSavedQuery(self, name, query, logset_name=None, time_range="Last 20 Minutes", from_time=None, to_time=None): 720 | """ 721 | Creates a Saved Query. 722 | """ 723 | # convert from/to times as necessary (string to timestamp with milliseconds) 724 | if not time_range: 725 | from_time = int(datetime.strptime(from_time, "%m/%d/%Y %H:%M:%S").timestamp()) * 1000 726 | to_time = int(datetime.strptime(to_time, "%m/%d/%Y %H:%M:%S").timestamp()) * 1000 727 | 728 | # get the relevant Log IDs 729 | if logset_name: 730 | log_ids = self.ListLogIdsByLogSetName(logset_name) 731 | else: 732 | log_ids = [] 733 | 734 | # get the time range 735 | if time_range: 736 | during = {"time_range": time_range} 737 | else: 738 | during = {"from": from_time, "to": to_time} 739 | 740 | data = { 741 | "saved_query": { 742 | "name": name, 743 | "leql": { 744 | "statement": query, 745 | "during": during 746 | }, 747 | "logs": log_ids 748 | } 749 | } 750 | 751 | # make the request 752 | url = self.query_url + "saved_queries" 753 | response = self.session.post(url, json=data) 754 | result = response.json() 755 | 756 | return result 757 | 758 | def ReplaceSavedQuery(self, saved_query_id, name, query, logset_name=None, time_range=None, from_time=None, to_time=None): 759 | """ 760 | Replace an existing saved query with the parameters specified in the input. 761 | """ 762 | # get the relevant Log IDs (updated or keep as existing if not set) 763 | if logset_name: 764 | log_ids = self.ListLogIdsByLogSetName(logset_name) 765 | else: 766 | query_obj = self.GetSavedQuery(saved_query_id) 767 | log_ids = query_obj["logs"] 768 | 769 | # get the time range 770 | if time_range: 771 | during = {"time_range": time_range} 772 | elif from_time and to_time: 773 | during = {"from": from_time, "to": to_time} 774 | else: 775 | during = None 776 | 777 | data = { 778 | "saved_query": { 779 | "name": name, 780 | "leql": { 781 | "statement": query, 782 | "during": during 783 | }, 784 | "logs": log_ids 785 | } 786 | } 787 | 788 | # make the reuest 789 | url = self.query_url + "saved_queries/{}".format(saved_query_id) 790 | response = self.session.put(url, json=data) 791 | result = response.json() 792 | 793 | return result 794 | 795 | def UpdateSavedQuery(self, saved_query_id, name=None, query=None, logset_name=None, time_range=None, from_time=None, to_time=None): 796 | """ 797 | Update an existing saved query with the parameters specified in the input. 798 | """ 799 | # get the relevant Log IDs 800 | if logset_name: 801 | log_ids = self.ListLogIdsByLogSetName(logset_name) 802 | else: 803 | query_obj = self.GetSavedQuery(saved_query_id) 804 | log_ids = query_obj["logs"] 805 | 806 | # get the time range 807 | if time_range: 808 | during = {"time_range": time_range} 809 | elif from_time and to_time: 810 | during = {"from": from_time, "to": to_time} 811 | else: 812 | during = None 813 | 814 | data = { 815 | "saved_query": { 816 | "name": name, 817 | "leql": { 818 | "statement": query, 819 | "during": during 820 | }, 821 | "logs": log_ids 822 | } 823 | } 824 | 825 | # make the reuest 826 | url = self.query_url + "saved_queries/{}".format(saved_query_id) 827 | response = self.session.patch(url, json=data) 828 | result = response.json() 829 | 830 | return result 831 | 832 | def DeleteSavedQuery(self, saved_query_id): 833 | """ 834 | Delete a saved query with the specified saved query ID. 835 | """ 836 | url = self.query_url + "saved_queries/{}".format(saved_query_id) 837 | response = self.session.delete(url) 838 | 839 | return response 840 | --------------------------------------------------------------------------------