├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── TODO.md ├── cyapi ├── __init__.py ├── cyapi.py ├── exclusions │ ├── carbon_black.json │ ├── cs_falcon.json │ ├── defender.json │ ├── mcafee.json │ ├── ms_scep.json │ ├── sccm.json │ ├── sep.json │ ├── sophos.json │ ├── tanium.json │ └── trend.json ├── mixins │ ├── _Detections.py │ ├── _DeviceCommands.py │ ├── _Devices.py │ ├── _Exceptions.py │ ├── _Focus_View.py │ ├── _Global_List.py │ ├── _InstaQueries.py │ ├── _MTC_HealthCheck.py │ ├── _MTC_PolicyTemplates.py │ ├── _MTC_Reports.py │ ├── _MTC_Tenants.py │ ├── _MTC_Users.py │ ├── _Memory_Protection.py │ ├── _Optics_Policies.py │ ├── _Packages.py │ ├── _Policies.py │ ├── _Rules.py │ ├── _Rulesets.py │ ├── _Threats.py │ ├── _Users.py │ ├── _Zones.py │ └── __init__.py └── reqs │ └── create_policy.json ├── examples ├── MTC_tenants_loop.py ├── find_stale_devices.py ├── safelist_trusted_local.py ├── simple_MTC_setup.py ├── simple_setup.py └── time_getting_all_detection_detail.py ├── poetry.lock ├── pyproject.toml └── tests └── test_cyapi.py /.gitignore: -------------------------------------------------------------------------------- 1 | creds*.json 2 | *.pyc 3 | .vscode/* 4 | build/* 5 | dist/* 6 | jd-* 7 | *.egg-info* 8 | *.swo 9 | *.swp 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # For Contributors 2 | 3 | ## Setup 4 | 5 | ### Requirements 6 | 7 | * Poetry: https://poetry.eustace.io 8 | 9 | ### Installation From Source 10 | 11 | Install project dependencies into a virtual environment: 12 | 13 | ```sh 14 | $ pip install poetry 15 | $ poetry install 16 | ``` 17 | 18 | ## Release Tasks 19 | 20 | Release to PyPI: 21 | 22 | ```sh 23 | $ poetry build 24 | $ poetry publish 25 | ``` 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | This Library provides python bindings to interact with the Cylance API. Examples have been created for you in the Examples/ directory, and provide a majority of the common code you'll need to get setup. In order to utilize this Library, you will need an API token from the API Integrations tab inside of the Cylance Console. 4 | 5 | # Supported Systems 6 | * Python 2.7 & Python 3 Compatible 7 | * Windows 8 | * Mac 9 | * Linux 10 | 11 | # Installation 12 | 13 | ``` 14 | pip install cyapi 15 | ``` 16 | 17 | # Example 18 | 19 | Please note there are a number of example scripts in the examples directory. These are valuable for initial authentication as well as some basic interactions with the library. The example scripts include: 20 | 21 | Single Tenant 22 | * simple_setup.py 23 | * find_stale_devices.py 24 | * safelist_trusted_local.py 25 | * time_getting_all_detection_detail.py 26 | 27 | Multi-Tenant Console (MTC) 28 | * simple_MTC_setup.py 29 | * MTC_tenants_loop.py 30 | 31 | This example will create a connection to the API and return all devices that have registered. 32 | 33 | ``` 34 | from cyapi.cyapi import CyAPI 35 | from pprint import pprint 36 | API = CyAPI(tid=your_id, app_id=your_app_id, app_secret=your_app_secret) 37 | API.create_conn() 38 | devices = API.get_devices() 39 | print("Successful: {}".format(devices.is_success)) 40 | pprint(devices.data[0]) # Print info about a single device. 41 | ``` 42 | 43 | If you have lots of devices/threats/zones/etc, and you'd like to see a progress bar, pass the `disable_progress` parameter: 44 | 45 | ``` 46 | devices = API.get_devices(disable_progress=False) 47 | pprint(devices.data[0]) 48 | ``` 49 | 50 | Additionally you can copy examples/simple_setup.py to your_new_file.py and begin hacking away from there. 51 | 52 | # Creds File 53 | 54 | You can create a file that will store your api credentials instead of passing them in via the command line. The creds file should look like the following: 55 | 56 | For a standard tenant: 57 | creds.json: 58 | ``` 59 | { 60 | "tid": "123456-55555-66666-888888888", 61 | "app_id": "11111111-222222-33333-44444444", 62 | "app_secret": "555555-666666-222222-444444", 63 | "region": "NA" 64 | } 65 | ``` 66 | 67 | For a Multi-Tenant Console (MTC) 68 | ``` 69 | { 70 | "tid": "Not Used for MTC Auth", 71 | "app_id": "11111111-222222-33333-44444444", 72 | "app_secret": "555555-666666-222222-444444", 73 | "region": "NA", 74 | "mtc": "True" 75 | } 76 | ``` 77 | The creds json file can then be passed in by passing -c path/to/creds.json to any of the examples 78 | 79 | # API End Point Documentation 80 | 81 | * Tenant User API Guide - https://docs.blackberry.com/content/dam/docs-blackberry-com/release-pdfs/en/cylance-products/api-and-developer-guides/Cylance%20User%20API%20Guide%20v2.0%20rev24.pdf 82 | * Tenant User API Release Notes - https://docs.blackberry.com/en/unified-endpoint-security/cylance--products/cylance-api-release-notes/BlackBerry-Cylance-API-release-notes 83 | * Multi-Tenant API - https://dev-admin.cylance.com/documentation/api.html 84 | 85 | # Contributing 86 | 87 | See [CONTRIBUTING.md](CONTRIBUTING.md) 88 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODOs 2 | * Add nicety functions for updating policies 3 | * Add logging 4 | * fix debug_level oddness in CyAPI 5 | * Fully Document 6 | * Expose the disable_progress param to all threaded callers 7 | * Expose **kwargs to all callers that have pages, limits, etc... 8 | -------------------------------------------------------------------------------- /cyapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cylance/python-cyapi/d85e67c56bd17593d0fa523685b455bc1b6fed97/cyapi/__init__.py -------------------------------------------------------------------------------- /cyapi/cyapi.py: -------------------------------------------------------------------------------- 1 | # cyAPI.py is designed to have reusable methods and classes for the Cylance v2 API 2 | # python 2.x & 3.x tested 3 | 4 | ################################################################################## 5 | # USAGE 6 | ################################################################################## 7 | 8 | import glob 9 | import json 10 | import os 11 | import re 12 | import time 13 | import uuid 14 | from datetime import datetime, timedelta 15 | from pprint import pprint 16 | from random import shuffle 17 | 18 | import jwt # PyJWT version 1.5.3 as of the time of authoring. 19 | import pkg_resources 20 | import requests # requests version 2.18.4 as of the time of authoring. 21 | from requests.adapters import HTTPAdapter 22 | from requests.exceptions import ConnectionError 23 | from requests.packages.urllib3.util.retry import Retry 24 | from tqdm import tqdm 25 | import concurrent.futures as cf 26 | 27 | from .mixins._Detections import Mixin as DetectionsMixin 28 | from .mixins._DeviceCommands import Mixin as DeviceCommandsMixin 29 | from .mixins._Devices import Mixin as DevicesMixin 30 | from .mixins._Exceptions import Mixin as ExceptionsMixin 31 | from .mixins._Focus_View import Mixin as FocusViewMixin 32 | from .mixins._Global_List import Mixin as GlobalListMixin 33 | from .mixins._InstaQueries import Mixin as InstaQueriesMixin 34 | from .mixins._Memory_Protection import Mixin as MemoryProtectionMixin 35 | from .mixins._Optics_Policies import Mixin as OpticsPoliciesMixin 36 | from .mixins._Packages import Mixin as PackagesMixin 37 | from .mixins._Policies import Mixin as PoliciesMixin 38 | from .mixins._Rules import Mixin as RulesMixin 39 | from .mixins._Rulesets import Mixin as RulesetMixin 40 | from .mixins._Threats import Mixin as ThreatsMixin 41 | from .mixins._Users import Mixin as UsersMixin 42 | from .mixins._Zones import Mixin as ZonesMixin 43 | from .mixins._MTC_HealthCheck import Mixin as MTCHealthCheckMixin 44 | from .mixins._MTC_PolicyTemplates import Mixin as MTCPolicyTemplatesMixin 45 | from .mixins._MTC_Reports import Mixin as MTCReportsMixin 46 | from .mixins._MTC_Tenants import Mixin as MTCTenantsMixin 47 | from .mixins._MTC_Users import Mixin as MTCUsersMixin 48 | 49 | try: 50 | from urllib import urlencode, unquote 51 | from urlparse import urlparse, parse_qsl, ParseResult 52 | except ImportError: 53 | # Python 3 fallback 54 | from urllib.parse import ( 55 | urlencode, unquote, urlparse, parse_qsl, ParseResult 56 | ) 57 | 58 | 59 | class CyAPI(DetectionsMixin,DevicesMixin,DeviceCommandsMixin,ExceptionsMixin, 60 | FocusViewMixin, GlobalListMixin,InstaQueriesMixin,MemoryProtectionMixin,OpticsPoliciesMixin, 61 | PackagesMixin,PoliciesMixin,RulesMixin,RulesetMixin, 62 | ThreatsMixin,UsersMixin,ZonesMixin,MTCHealthCheckMixin,MTCPolicyTemplatesMixin, 63 | MTCReportsMixin,MTCTenantsMixin,MTCUsersMixin): 64 | """The main class that should be used. Each of the Mixins above provides the 65 | functionality for that specific API. Example: DetectionsMixin implements 66 | all relevant functions to getting / working with detections. 67 | 68 | Example Usage: 69 | API = CyAPI(tid="your_tid", app_id="your_app_id", app_secret="your_secret") 70 | API.create_conn() 71 | At this point you're ready to begin interacting with the API. 72 | """ 73 | regions = { 74 | 'NA': {'fullname': 'North America', 'url':''}, 75 | 'US': {'fullname': 'United States', 'mtc_url':'us'}, 76 | 'APN': {'fullname': 'Asia Pacific-North', 'url': '-apne1'}, 77 | 'JP': {'fullname': 'Asia Pacific NE/Japan', 'mtc_url':'jp'}, 78 | 'APS': {'fullname': 'Asia Pacific-South', 'url': '-au'}, 79 | 'AU': {'fullname': 'Asia Pacific SE/Australia', 'mtc_url':'au'}, 80 | 'EU': {'fullname': 'Europe', 'url': '-euc1', 'mtc_url':'eu'}, 81 | 'GOV': {'fullname': 'US-Government', 'url': '-us'}, 82 | 'SA': {'fullname': 'South America', 'url': '-sae1'}, 83 | 'SP': {'fullname': 'South America/Sao Paulo', 'mtc_url': 'sp'} 84 | } 85 | 86 | valid_detection_statuses = ["New", "In Progress", "Follow Up", "Reviewed", "Done", "False Positive"] 87 | valid_artifact_types = ["Protect", "Process", "File", "NetworkConnection", "RegistryKey"] 88 | 89 | root_path = os.path.dirname(os.path.abspath(__file__)) 90 | 91 | exclusions = pkg_resources.resource_listdir(__name__, "exclusions") 92 | 93 | exc_choices = list(map(lambda x: os.path.basename(x).replace('.json',''), exclusions)) 94 | 95 | WORKERS = 20 96 | 97 | def __init__(self, tid=None, app_id=None, app_secret=None, region="NA", mtc=False, tenant_app=False, tenant_jwt=None): 98 | self.tid_val = tid 99 | self.app_id = app_id 100 | self.app_secret = app_secret 101 | self.jwt = None 102 | self.region = region 103 | self.mtc = mtc 104 | self.tenant_app = tenant_app 105 | self.tenant_jwt = tenant_jwt 106 | 107 | if self.mtc: 108 | self.baseURL = "https://api-admin.cylance.com/public/{}/".format(self.regions[region]['mtc_url']) 109 | else: 110 | self.baseURL = "https://protectapi{}.cylance.com/".format(self.regions[region]['url']) 111 | self.debug_level = debug_level 112 | self.s = None 113 | self.req_cnt = 0 114 | 115 | def create_conn(self): 116 | """ 117 | Setup and authenticate the connection to the API 118 | """ 119 | 120 | self.s = self._setup_session(session=self.s) 121 | 122 | if self.mtc: 123 | self.auth = self._get_auth_token() 124 | self.headers = { 125 | "Content-Type": "application/json; charset=utf-8", 126 | "Accept": "*/*", 127 | 'Accept-Encoding': "gzip,deflate,br", 128 | 'Authorization': "Bearer {}".format(self.auth) 129 | } 130 | else: 131 | if self.tenant_app: 132 | self.jwt = self.tenant_jwt 133 | else: 134 | self.jwt = self._get_jwt() 135 | self.headers = { 136 | 'Accept': "application/json", 137 | 'Accept-Encoding': "gzip,compress", 138 | 'Authorization': "Bearer {}".format(self.jwt), 139 | 'Cache-Control': "no-cache" 140 | } 141 | 142 | # # 30 minutes from now 143 | timeout = 1800 144 | now = datetime.utcnow() 145 | timeout_datetime = now + timedelta(seconds=timeout) 146 | self.access_token_expiration = timeout_datetime - timedelta(seconds=30) 147 | 148 | self.s.headers.update(self.headers) 149 | 150 | def _setup_session(self, retries=250, backoff_factor=0.8, 151 | backoff_max=180, status_forcelist=(500, 502, 503, 504), 152 | session=None): 153 | """Creates a session with a Retry handler. This will automatically retry 154 | up to 250 times... which might be overkill 155 | """ 156 | 157 | session = session or requests.Session() 158 | retry = Retry( 159 | total=retries, 160 | read=retries, 161 | connect=retries, 162 | backoff_factor=backoff_factor, 163 | status_forcelist=status_forcelist, 164 | respect_retry_after_header=True, 165 | ) 166 | adapter = HTTPAdapter(max_retries=retry) 167 | session.mount(self.baseURL, adapter) 168 | return session 169 | 170 | def _get_jwt(self): 171 | '''Create a JWT that expires in 30min''' 172 | # 30 minutes from now 173 | timeout = 1800 174 | now = datetime.utcnow() 175 | timeout_datetime = now + timedelta(seconds=timeout) 176 | epoch_time = int((now - datetime(1970, 1, 1)).total_seconds()) 177 | epoch_timeout = int((timeout_datetime - datetime(1970, 1, 1)).total_seconds()) 178 | jti_val = str(uuid.uuid4()) 179 | 180 | AUTH_URL = self.baseURL + "auth/v2/token" 181 | claims = { 182 | "exp": epoch_timeout, 183 | "iat": epoch_time, 184 | "iss": "http://cylance.com", 185 | "sub": self.app_id, 186 | "tid": self.tid_val, 187 | "jti": jti_val 188 | } 189 | 190 | try: 191 | # This is left for compatibility with PyJWT prior to 2.0.0 192 | encoded = jwt.encode(claims, self.app_secret, algorithm='HS256').decode('utf-8') 193 | except: 194 | encoded = jwt.encode(claims, self.app_secret, algorithm='HS256') 195 | 196 | print(type(encoded)) 197 | print(encoded) 198 | if debug_level > 2: 199 | print( "auth_token:\n" + encoded + "\n" ) 200 | payload = {"auth_token": encoded} 201 | headers = {"Content-Type": "application/json; charset=utf-8"} 202 | resp = requests.post(AUTH_URL, headers=headers, data=json.dumps(payload)) 203 | 204 | # Can't do anything without a successful authentication 205 | try: 206 | assert resp.status_code == 200 207 | except AssertionError: 208 | error_message = [] 209 | try: 210 | errors = resp.json() 211 | except json.decoder.JSONDecodeError: 212 | errors = None 213 | error_message.append("Failed request for JWT Token.") 214 | error_message.append(" Response Status Code: {}".format(resp.status_code)) 215 | if not errors == None: 216 | error_message.append(" Error(s):") 217 | for k in errors: 218 | error_message.append(" {}: {}".format(k,errors[k])) 219 | raise RuntimeError('\n'.join(error_message)) 220 | 221 | data = resp.json() 222 | token = data.get('access_token',None) 223 | if debug_level > 1: 224 | print( "http_status_code: {}".format(resp.status_code) ) 225 | print( "access_token:\n" + token + "\n" ) 226 | 227 | return token 228 | 229 | def _get_auth_token(self): 230 | """Get auth token for MTC""" 231 | AUTH_URL = self.baseURL + "auth" 232 | claims = { 233 | "scope" : "api", 234 | "grant_type" : "client_credentials" 235 | } 236 | payload = claims 237 | 238 | headers = {"Content-Type": "application/json; charset=utf-8"} 239 | resp = requests.post(AUTH_URL, data=payload, auth=(self.app_id,self.app_secret)) 240 | 241 | # Can't do anything without a successful authentication 242 | try: 243 | assert resp.status_code == 200 244 | except AssertionError: 245 | error_message = [] 246 | try: 247 | errors = resp.json() 248 | except json.decoder.JSONDecodeError: 249 | errors = None 250 | error_message.append(" Failed request for MTC Auth Token") 251 | error_message.append(" Response Status Code: {}".format(resp.status_code)) 252 | if not errors == None: 253 | error_message.append(" Error(s):") 254 | for k in errors: 255 | error_message.append(" {}: {}".format(k,errors[k])) 256 | raise RuntimeError('\n'.join(error_message)) 257 | 258 | data = resp.json() 259 | token = data.get('access_token',None) 260 | if debug_level > 1: 261 | print( "http_status_code: {}".format(resp.status_code) ) 262 | print( "access_token:\n" + token + "\n" ) 263 | 264 | return token 265 | 266 | def _make_request(self, method, url, data=None): 267 | """Request Handler which also checks for token expiration""" 268 | self.req_cnt += 1 269 | 270 | if datetime.utcnow() > self.access_token_expiration or self.req_cnt >= 9500: 271 | self.req_cnt = 0 272 | # Refresh the token if needed 273 | self.create_conn() 274 | if method == "get": 275 | resp = self.s.get(url) 276 | # loop if rate limited 277 | # TODO: Improve method when headers are uniformally supported 278 | while resp.status_code == 429: 279 | time.sleep(1) 280 | resp = self.s.get(url) 281 | return ApiResponse(resp) 282 | elif method == "post": 283 | if data: 284 | return ApiResponse(self.s.post(url, json=data)) 285 | return ApiResponse(self.s.post(url)) 286 | elif method == "delete": 287 | if data: 288 | return ApiResponse(self.s.delete(url, json=data)) 289 | return ApiResponse(self.s.delete(url)) 290 | elif method == "put": 291 | if data: 292 | return ApiResponse(self.s.put(url, json=data)) 293 | return ApiResponse(self.s.put(url)) 294 | 295 | raise ValueError("Invalid Method: {}".format(method)) 296 | 297 | def _validate_parameters(self, param, valid_params): 298 | 299 | if param not in valid_params: 300 | raise ValueError("{} not valid. Valid values are: {}".format(param, valid_params)) 301 | return True 302 | 303 | def _is_valid_detection_status(self, status): 304 | 305 | if status and status not in self.valid_detection_statuses: 306 | raise ValueError("Status not valid. Valid values: {}".format(self.valid_detection_statuses)) 307 | return True 308 | 309 | def _is_valid_artifact_type(self, artifact_type): 310 | 311 | if artifact_type and artifact_type not in self.valid_artifact_types: 312 | raise ValueError("Artifact Type not valid. Valid values: {}".format(self.valid_artifact_types)) 313 | return True 314 | 315 | def _convert_id(self,pid): 316 | # Convert ID to Uppercase & no '-'s 317 | return pid.replace('-','').upper() 318 | 319 | def _add_url_params(self, url, params): 320 | """ Add GET params to provided URL being aware of existing. 321 | 322 | :param url: string of target URL 323 | :param params: dict containing requested params to be added 324 | :return: string with updated URL 325 | 326 | >> url = 'http://stackoverflow.com/test?answers=true' 327 | >> new_params = {'answers': False, 'data': ['some','values']} 328 | >> add_url_params(url, new_params) 329 | 'http://stackoverflow.com/test?data=some&data=values&answers=false' 330 | """ 331 | # Unquoting URL first so we don't loose existing args 332 | url = unquote(url) 333 | # Extracting url info 334 | parsed_url = urlparse(url) 335 | # Extracting URL arguments from parsed URL 336 | get_args = parsed_url.query 337 | # Converting URL arguments to dict 338 | parsed_get_args = dict(parse_qsl(get_args)) 339 | # Merging URL arguments dict with new params 340 | parsed_get_args.update(params) 341 | 342 | parsed_get_args = {k: v for k, v in parsed_get_args.items() if v is not None} 343 | # Bool and Dict values should be converted to json-friendly values 344 | # you may throw this part away if you don't like it :) 345 | parsed_get_args.update( 346 | {k: json.dumps(v) for k, v in parsed_get_args.items() 347 | if isinstance(v, (bool, dict))} 348 | ) 349 | 350 | # Converting URL argument to proper query string 351 | encoded_get_args = urlencode(parsed_get_args, doseq=True) 352 | # Creating new parsed result object based on provided with new 353 | # URL arguments. Same thing happens inside of urlparse. 354 | new_url = ParseResult( 355 | parsed_url.scheme, parsed_url.netloc, parsed_url.path, 356 | parsed_url.params, encoded_get_args, parsed_url.fragment 357 | ).geturl() 358 | 359 | return new_url 360 | 361 | # TODO: Remove this method 362 | def create_item(self, ptype, item): 363 | '''Type options: zones, rulesets, policies''' 364 | baseURL = self.baseURL + "{}/v2".format(ptype) 365 | 366 | if debug_level > 1: 367 | if debug_level > 2: 368 | pprint(item) 369 | print( "Create Item URL: " + baseURL ) 370 | 371 | return self._make_request("post",baseURL, data=item) 372 | 373 | # Method to get a page of items and return the response object 374 | def _get_list_page(self, page_type, page=1, page_size=200, detail="",params={}): 375 | q_params = {"page": page, "page_size": page_size} 376 | 377 | if params: 378 | q_params.update(params) 379 | 380 | baseURL = self.baseURL + "{}/v2{}".format(page_type, detail) 381 | baseURL = self._add_url_params(baseURL, q_params) 382 | 383 | return self._make_request("get",baseURL) 384 | 385 | def _generate_urls(self, page_type, page=1, page_size=200, detail="",params={}, total_pages=0): 386 | start_page = page 387 | q_params = {"page": start_page, "page_size": page_size} 388 | 389 | if params: 390 | q_params.update(params) 391 | 392 | if self.mtc: 393 | baseURL = self.baseURL + "{}/{}".format(page_type,detail) 394 | else: 395 | baseURL = self.baseURL + "{}/v2{}".format(page_type, detail) 396 | baseURL = self._add_url_params(baseURL, q_params) 397 | 398 | response = self._make_request("get",baseURL) 399 | try: 400 | assert response.status_code == 200 401 | except AssertionError: 402 | error_message = [] 403 | error_message.append("Failed initial request for {}.".format(page_type)) 404 | error_message.append(" get URL:\n {}".format(baseURL)) 405 | error_message.append(" Response Status Code: {}".format(response.status_code)) 406 | if response.errors: 407 | error_message.append("Error(s)") 408 | for k in response.errors: 409 | error_message.append(" {}: {}".format(k,response.errors.get(k))) 410 | raise RuntimeError('\n'.join(error_message)) 411 | data = response.data 412 | 413 | page_size = data['page_size'] 414 | if total_pages == 0 or total_pages > data['total_pages']: 415 | total_pages = data['total_pages'] 416 | 417 | all_urls = [baseURL,] 418 | for page in range(start_page+1, total_pages+1): 419 | updated_param = {"page":page} 420 | baseURL = self._add_url_params(baseURL, updated_param) 421 | all_urls.append(baseURL) 422 | # there seems to be a bug which leads to increased 50x errors the higher the page count is (caching?) 423 | # we want to shuffle the urls so they're not sequentially downloaded anymore and hit the 50x early 424 | # this way, we can retry again and again, until scalar returns our content 425 | shuffle(all_urls) 426 | return all_urls 427 | 428 | def _bulk_get(self, urls, disable_progress=True, paginated=True): 429 | 430 | tqdmargs = {'total': len(urls), 431 | 'unit': 'Page', 432 | 'leave': False, 433 | 'desc': 'Download Progress', 434 | 'disable': disable_progress} 435 | 436 | collector = [] 437 | with cf.ThreadPoolExecutor(max_workers=self.WORKERS) as executor: 438 | res = {executor.submit(self._make_request, "get", url): url for url in urls} 439 | for future in tqdm(cf.as_completed(res), **tqdmargs): 440 | url = res[future] 441 | try: 442 | response = future.result() 443 | 444 | except Exception as exc: 445 | print('[-] {} generated an exception: {}'.format(url,exc)) 446 | continue 447 | else: 448 | try: 449 | if not response.is_success: 450 | raise json.decoder.JSONDecodeError 451 | data = response.data 452 | if paginated: 453 | collector.extend(data["page_items"]) 454 | else: 455 | collector.append(data) 456 | except json.decoder.JSONDecodeError: 457 | print("Likely got a Server Error here, trying to quit. Please try again later.") 458 | executor.shutdown(wait=False) 459 | raise json.decoder.JSONDecodeError 460 | except KeyError: 461 | print("Error: no data returned.") 462 | print("if you see this message, 250 retries per url have been exceeded") 463 | print("get a new token and retry later") 464 | print("this is the url {} and returned data: {}".format(url, data)) 465 | 466 | response.data = collector # This is a hacky way of returning an APIResponse for all of the data 467 | 468 | return response 469 | 470 | 471 | # Method to retrieve list of items 472 | def get_list_items(self, type_name, detail="", limit=200, params={}, disable_progress=True, total_pages=0, 473 | start_page=1): 474 | 475 | urls = self._generate_urls(type_name, detail=detail, page_size=limit, params=params, total_pages=total_pages, 476 | page=start_page) 477 | 478 | return self._bulk_get(urls, disable_progress) 479 | 480 | # Method to retrieve an Item 481 | # This may not be anything as a result of edit error.. 482 | # TODO: Make this return a response instead of a JSON object 483 | def get_item(self, ptype, item): 484 | #Type options: rulesets, policies 485 | 486 | if self.mtc: 487 | baseURL = self.baseURL + "{}/{}".format(ptype, item) 488 | else: 489 | baseURL = self.baseURL + "{}/v2/{}".format(ptype, item) 490 | 491 | response = self._make_request("get",baseURL) 492 | return response 493 | 494 | class ApiResponse: 495 | def __init__(self, response): 496 | self.status_code = response.status_code 497 | self.is_success = response.status_code < 300 498 | self.headers = response.headers 499 | self.data = None 500 | self.errors = None 501 | 502 | if self.is_success: 503 | # 02/29/2021 504 | # Replacing the commented variables and 'if' in favor 505 | # of the try/except and re.sub lines to accept 506 | # non-json values/messages when a successful response 507 | # is sent. 508 | # This would omit a 'None' data response. 509 | """ 510 | try: 511 | headers = response.headers 512 | content = headers.get('content-type') 513 | if content and "json" in content: 514 | self.data = response.json() 515 | else: 516 | self.data = response.text 517 | except json.decoder.JSONDecodeError: 518 | self.data = None 519 | """ 520 | try: 521 | self.data = response.json() 522 | ####print("Good Data: {}".format(self.data)) 523 | except json.decoder.JSONDecodeError: 524 | # Removing the wrapper characters of a string response 525 | self.data = re.sub('{|"|}', "",response.text) 526 | 527 | else: 528 | try: 529 | self.errors = response.json() 530 | except json.decoder.JSONDecodeError: 531 | self.errors = None 532 | 533 | def to_json(self): 534 | return {'is_success': self.is_success, 'data': self.data, 'errors': self.errors} 535 | 536 | # --------------------------- 537 | # END OF HELPER FUNCTIONS 538 | 539 | # Utility Variables 540 | debug_level = 0 541 | 542 | if debug_level >= 1: 543 | print( "Debug level is set at: {}".format(debug_level)) 544 | if debug_level >= 5: 545 | exit("Debug Level too high!") 546 | -------------------------------------------------------------------------------- /cyapi/exclusions/carbon_black.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | ], 4 | "protection": [ 5 | "C:\\Windows\\CarbonBlack\\Store", 6 | "/private/var/lib/cb/store/" 7 | ], 8 | "script": [] 9 | 10 | } 11 | -------------------------------------------------------------------------------- /cyapi/exclusions/cs_falcon.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | ], 4 | "protection": [ 5 | "/Library/CS/Downloads/" 6 | ], 7 | "script": [] 8 | } 9 | -------------------------------------------------------------------------------- /cyapi/exclusions/defender.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | "\\program files\\windows defender advanced threat protection\\mssense.exe", 4 | "\\program files\\windows defender\\nissrv.exe", 5 | "\\program files\\windows defender\\msmpeng.exe" 6 | ], 7 | "protection": [ 8 | "C:\\Program Files\\Windows Defender\\", 9 | "C:\\Program Files\\Windows Defender Advanced Threat Protection\\", 10 | "C:\\ProgramData\\Microsoft\\Windows Defender\\", 11 | "C:\\ProgramData\\Microsoft\\Windows Defender Advanced Threat Protection\\" 12 | ], 13 | "script": [] 14 | } 15 | -------------------------------------------------------------------------------- /cyapi/exclusions/mcafee.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | "\\Program Files\\Common Files\\McAfee\\Engine\\scanners\\x86_64\\datrep\\1.50.1291.1\\mcdatrep.exe", 4 | "\\Program Files\\Common Files\\McAfee\\SystemCore\\mfemms.exe", 5 | "\\Program Files\\McAfee\\Agent\\masvc.exe", 6 | "\\Program Files\\McAfee\\Agent\\x86\\macmnsvc.exe", 7 | "\\Program Files\\McAfee\\Agent\\x86\\macompatsvc.exe", 8 | "\\Program Files\\McAfee\\Agent\\x86\\mctray.exe", 9 | "\\Program Files\\McAfee\\Agent\\x86\\mfemactl.exe", 10 | "\\Program Files\\McAfee\\Endpoint Security\\Endpoint Security Platform\\mfeesp.exe", 11 | "\\Program files\\mcafee\\endpoint security\\threat prevention\\mfetp.exe", 12 | "\\Windows\\System32\\mfevtps.exe", 13 | "\\Program Files\\McAfee\\Endpoint Security\\Firewall\\mfefw.exe", 14 | "\\Program Files\\McAfee\\Endpoint Security\\Adaptive threat Protection\\mfeatp.exe", 15 | "\\Program Files\\Common Files\\McAfee\\SystemCore\\MFEFIRE.exe", 16 | "\\Program Files\\Common Files\\McAfee\\SystemCore\\mfecanary.exe", 17 | "\\Program Files (x86)\\McAfee\\Endpoint Security\\Endpoint Security Platform\\mfeconsole.exe", 18 | "\\Program Files (x86)\\McAfee\\Common Framework\\updaterui.exe", 19 | "\\Program Files\\Common Files\\McAfee\\AVSolution\\mcshield.exe" 20 | ], 21 | "protection": [ 22 | "/opt/NAI/LinuxShield/", 23 | "/opt/McAfee/", 24 | "/opt/McAfee/cma/", 25 | "/opt/isec/ens/threatprevention/bin/", 26 | "/opt/isec/ens/esp/bin/" 27 | ], 28 | "script": [] 29 | 30 | } 31 | -------------------------------------------------------------------------------- /cyapi/exclusions/ms_scep.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | "\\Program Files\\Microsoft Security Client\\MsMpEng.exe", 4 | "\\Program Files\\Microsoft Security Client\\msseces.exe" 5 | ], 6 | "protection": [ 7 | "C:\\ProgramData\\Microsoft\\Microsoft Antimalware\\Quarantine\\", 8 | "C:\\ProgramData\\Microsoft\\Microsoft Antimalware\\scans\\filesstash\\", 9 | "C:\\ProgramData\\Microsoft\\Microsoft Antimalware\\localcopy\\" 10 | ], 11 | "script": [] 12 | } 13 | -------------------------------------------------------------------------------- /cyapi/exclusions/sccm.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [], 3 | "protection": [ 4 | "C:\\Windows\\ccmcache\\" 5 | ], 6 | "script": [ 7 | "/windows/ccm/systemtemp/*.vbs", 8 | "/windows/ccm/systemtemp/*.ps1" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /cyapi/exclusions/sep.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | ], 4 | "protection": [ 5 | "C:\\ProgramData\\Symantec\\Symantec Endpoint Protection", 6 | "C:\\Program Files (X86)\\Symantec\\Symantec Endpoint Protection", 7 | "C:\\Program Files (x86)\\Common Files\\Symantec Shared", 8 | "C:\\Program Files\\Symantec\\Symantec Endpoint Protection" 9 | ], 10 | "script": [] 11 | 12 | } 13 | -------------------------------------------------------------------------------- /cyapi/exclusions/sophos.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | "\\Sophos\\AutoUpdate\\ALsvc.exe", 4 | "\\Sophos\\Sophos Anti-Virus\\SAVAdminService.exe", 5 | "\\Sophos\\Sophos Anti-Virus\\SavService.exe", 6 | "\\Sophos\\Sophos Anti-Virus\\sdcservice.exe", 7 | "\\Sophos\\Sophos Network Threat Protection\\bin\\SntpService.exe", 8 | "\\Sophos\\Sophos System Protection\\ssp.exe", 9 | "\\Sophos\\Sophos Anti-Virus\\Web Control\\swc_service.exe", 10 | "\\Sophos\\Sophos Anti-Virus\\Web Intelligence\\swi_service.exe", 11 | "\\Sophos\\Sophos Anti-Virus\\Web Intelligence\\swi_filter.exe", 12 | "\\Common Files\\Sophos\\Web Intelligence\\swi_fc.exe", 13 | "\\Sophos\\Sophos Patch Agent\\spa.exe", 14 | "\\health.exe", 15 | "\\heartbeat.exe", 16 | "\\sdrservice.exe", 17 | "\\sophosclean.exe" 18 | ], 19 | "protection": [ 20 | "C:\\Program Files\\Sophos\\", 21 | "C:\\Program Files (x86)\\Sophos\\", 22 | "C:\\ProgramData\\Sophos\\" 23 | ], 24 | "script": [] 25 | } 26 | -------------------------------------------------------------------------------- /cyapi/exclusions/tanium.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | "\\Program Files (x86)\\Tanium\\Tanium Client\\TaniumClient.exe", 4 | "\\Program Files\\Tanium\\Tanium Client\\TaniumClient.exe", 5 | "/Library/Tanium/TaniumClient/TaniumClient", 6 | "/opt/Tanium/TaniumClient/taniumclient" 7 | ], 8 | "protection": [ 9 | "C:\\Program Files (x86)\\Tanium", 10 | "C:\\Program Files\\Tanium", 11 | "/Library/Tanium/TaniumClient", 12 | "/opt/Tanium/TaniumClient" 13 | ], 14 | "script": [ 15 | "/Program Files (x86)/Tanium", 16 | "/Program Files/Tanium" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /cyapi/exclusions/trend.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": [ 3 | "\\Program Files (x86)\\Trend Micro\\BM\\TMBMSRV.exe", 4 | "\\Program Files (x86)\\Trend Micro\\OfficeScan Client\\CCSF\\TmCCSF.exe", 5 | "\\Program Files (x86)\\Trend Micro\\OfficeScan Client\\Ntrtscan.exe", 6 | "\\Program Files (x86)\\Trend Micro\\OfficeScan Client\\PccNTMon.exe", 7 | "\\Program Files (x86)\\Trend Micro\\OfficeScan Client\\TmListen.exe", 8 | "\\Program Files (x86)\\Trend Micro\\OfficeScan Client\\TmPfw.exe" 9 | 10 | ], 11 | "protection": [ 12 | "C:\\Program Files (x86)\\Trend Micro\\", 13 | "C:\\ProgramData\\Trend Micro\\" 14 | ], 15 | "script": [] 16 | } 17 | -------------------------------------------------------------------------------- /cyapi/mixins/_Detections.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_detections_by_severity(self, start=None, end=None, interval=None, 5 | detection_type=None, detected_on=None, 6 | event_number=None, device_name=None, status=None): 7 | """ 8 | Returns the detections by severity 9 | :param start: Start Time in Zulu Format: 2019-09-13T00:00:00Z 10 | :param end: End Time in Zulu Format: 2019-09-15T23:59:59Z 11 | :param interval: Timer for grouping detections: Format: [0-9] + Time: 1y, 1M, 1d, 1h, 1m 12 | :param detection_type: DetectionDescription or DetectionName 13 | :param detected_on: Detected On time 14 | :param event_number: Phonetic ID 15 | :param device_name: Device Name Filter 16 | :param status: Filter on statuses: Values are New, In Progress, Follow Up, Reviewed, Done, 17 | False Positive 18 | """ 19 | # /detections/v2/severity?start={detection_start_timestamp}&end{detection_end_ 20 | # timestamp}&interval={detection_interval} 21 | baseURL = self.baseURL + "detections/v2/severity" 22 | if not self._is_valid_detection_status(status): 23 | raise ValueError("Status Value not valid. Valid values: {}".format( 24 | self.valid_detection_statuses)) 25 | 26 | params = { 27 | "start": start, 28 | "end": end, 29 | "interval": interval, 30 | "detection_type": detection_type, 31 | "detected_on": detected_on, 32 | "event_number": event_number, 33 | "device": device_name, 34 | "status": status 35 | } 36 | baseURL = self._add_url_params(baseURL, params) 37 | return self._make_request("get",baseURL) 38 | 39 | def delete_detection(self, detection_id): 40 | """Deletes a detection 41 | :param detection_id: detection ID 42 | endpoint: /detections/v2/{detection_id} 43 | """ 44 | 45 | baseURL = self.baseURL + "detections/v2/{}".format(detection_id) 46 | return self._make_request("delete",baseURL) 47 | 48 | def delete_detections(self, detection_ids): 49 | """Deletes a list of detections 50 | :param detection_ids: List of Detection IDs 51 | endpoint: /detections/v2/ 52 | """ 53 | baseURL = self.baseURL + "detections/v2/" 54 | data = { "ids": detection_ids } 55 | 56 | return self._make_request("delete",baseURL, data=data) 57 | 58 | # Get Detections 59 | def get_recent_detections(self, since, **kwargs): 60 | """Get Detections since a certain time 61 | :param since: Time in Zulu - Format: 2018-07-26T01:20:07.596Z 62 | """ 63 | params = {"since": since} 64 | return self.get_list_items("detections", params=params, **kwargs) 65 | 66 | # Get Detections, might need better way to get additional detection pages 67 | def get_detections(self, zulu_start=None, zulu_end=None, severity=None, 68 | detection_type=None, event_number=None, device_name=None, 69 | status=None, sort=None, csv=False, **kwargs): 70 | ''':param start: Start time in Zulu: 2019-05-04T00:00:00.000Z 71 | :param end: End date-time in Zulu: 2019-05-04T23:00:00.000Z 72 | :param severity: Detection severity filter 73 | Values are Informational, Low, Medium, High. 74 | :param detection_type: Detection type filter 75 | Example: &detection_type=Powershell Download 76 | :param event_number: Event number filter - PhoneticId and DetectionID 77 | :param device: Device name filter 78 | :param status: The status for the detection event. 79 | Values are New, In Progress, Follow Up, Reviewed, Done, False Positive 80 | :param sort: Sort by the following fields 81 | (adding "-" in front of the value denotes descending order) 82 | * Severity 83 | * OccurrenceTime 84 | * Status 85 | * Device 86 | * PhoneticId 87 | * Description 88 | * ReceivedTime 89 | ''' 90 | 91 | valid_sort = ["Severity", "OccurrenceTime","Status","Device","PhoneticId","Description","ReceivedTime"] 92 | valid_severity = ["Low", "Medium", "High"] 93 | 94 | if sort and sort not in valid_sort: 95 | raise ValueError("Sort Value not valid. Valid values: {}".format(valid_sort)) 96 | 97 | if severity and severity not in valid_severity: 98 | raise ValueError("Severity Value not valid. Valid values: {}".format(valid_severity)) 99 | 100 | if not self._is_valid_detection_status(status): 101 | raise ValueError("Status Value not valid. Valid values: {}".format(self.valid_detection_statuses)) 102 | 103 | params = {"start": zulu_start, 104 | "end": zulu_end, 105 | "severity": severity, 106 | "detection_type": detection_type, 107 | "event_number": event_number, 108 | "device": device_name, 109 | "status": status, 110 | "sort": sort} 111 | 112 | # If there are no values passed, the query will fail 113 | params = {k: v for k, v in params.items() if v is not None} 114 | 115 | if csv: 116 | baseURL = self.baseURL + "detections/v2/csv" 117 | baseURL = self._add_url_params(baseURL, params) 118 | return self._make_request("get", baseURL) 119 | 120 | return self.get_list_items("detections", params=params, **kwargs) 121 | 122 | 123 | # Get Detection, might need better way to get additional detection pages 124 | def get_detection(self, detection_id): 125 | """Get Detail about a detection 126 | :param detection_id: The detection ID or IDs to search for 127 | :example: detection_id=123 - This will return detail about one detection 128 | """ 129 | baseURL = self.baseURL + "detections/v2/{}/details".format(detection_id) 130 | return self._make_request("get",baseURL) 131 | 132 | def get_bulk_detection(self, detection_ids, disable_progress=True): 133 | """Get detection detail for many IDs 134 | :param detection_ids: list of detection_ids 135 | """ 136 | baseURL = self.baseURL + "detections/v2/{}/details" 137 | urls = [] 138 | if isinstance(detection_ids, list): 139 | for detection in detection_ids: 140 | urls.append(baseURL.format(detection)) 141 | return self._bulk_get(urls, paginated=False, disable_progress=disable_progress) 142 | 143 | 144 | def update_detection(self, detection_id, field, value): 145 | """ 146 | :param detection_id: The Detection ID to update 147 | :param field: Field to update 148 | :param value: The data you'd like to update with 149 | endpoint: /detections/v2""" 150 | baseURL = self.baseURL + "detections/v2" 151 | 152 | valid_fields = ["comment", "status"] 153 | self._validate_parameters(field, valid_fields) 154 | 155 | if field == "status": 156 | self._is_valid_detection_status(value) 157 | 158 | data = [ 159 | { 160 | "detection_id": detection_id, 161 | "field_to_update": { field: value } 162 | } 163 | ] 164 | 165 | return self._make_request("post",baseURL, data=data) 166 | -------------------------------------------------------------------------------- /cyapi/mixins/_DeviceCommands.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_retrieved_file_results(self, **kwargs): 5 | # /devicecommands/v2/retrieved_files?page=m&page_size=n 6 | return self.get_list_items("devicecommands", detail="/retrieved_files", **kwargs) 7 | 8 | # Get Detection, might need better way to get additional detection pages 9 | def lockdown_device(self, device_id, exp): 10 | # exp = expires: Duration of the lockdown. Format: 'd:hh:mm' 11 | device_id = self._convert_id(device_id) 12 | params = {"value":"true", "expires": exp} 13 | baseURL = self.baseURL + "devicecommands/v2/{}/lockdown".format(device_id) 14 | baseURL = self._add_url_params(baseURL, params) 15 | 16 | return self._make_request("get",baseURL) 17 | 18 | # Get Detection, might need better way to get additional detection pages 19 | def get_device_lockdown_history(self, device_id): 20 | 21 | device_id = self._convert_id(device_id) 22 | baseURL = self.baseURL + "devicecommands/v2/{}/lockdown".format(device_id) 23 | 24 | return self._make_request("get",baseURL) 25 | 26 | def request_file_retrieval_from_device(self, device_id, file_path): 27 | # /devicecommands/v2/{{device_id}}/getfile 28 | device_id = self._convert_id(device_id) 29 | baseURL = self.baseURL + "devicecommands/v2/{}/getfile".format(device_id) 30 | data = { "file_path": file_path } 31 | return self._make_request("post",baseURL, data=data) 32 | 33 | def check_file_retrieval_status_from_device(self,device_id, file_path): 34 | # /devicecommands/v2/{{device_id}}/getfile:get 35 | device_id = self._convert_id(device_id) 36 | baseURL = self.baseURL + "devicecommands/v2/{}/getfile:get".format(device_id) 37 | data = { "file_path": file_path } 38 | return self._make_request("post",baseURL, data=data) 39 | -------------------------------------------------------------------------------- /cyapi/mixins/_Devices.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def delete_devices(self, device_ids, callback_url=None): 5 | """Delete device(s) per ID(s) 6 | :param device_ids: must be a list of device_ids 7 | """ 8 | baseURL = self.baseURL + "devices/v2" 9 | if callback_url == None: 10 | data = { 11 | "device_ids": device_ids 12 | } 13 | return self._make_request("delete", baseURL, data=data) 14 | else: 15 | data = { 16 | "device_ids": device_ids, 17 | "callback_url": callback_url 18 | } 19 | return self._make_request("delete", baseURL, data=data) 20 | 21 | def get_devices(self, **kwargs): 22 | '''Get a list of Devices''' 23 | return self.get_list_items('devices', **kwargs) 24 | 25 | def get_devices_extended(self, **kwargs): 26 | '''Get a list of Devices w/ extended information''' 27 | return self.get_list_items('devices', detail='/extended', **kwargs) 28 | 29 | def get_device_count(self, **kwargs): 30 | '''Get a list of list of products, product versions, 31 | and number of devices using a product and product version 32 | ''' 33 | return self.get_item('devices', 'products') 34 | 35 | def get_device(self, device_id): 36 | '''Get Device Detail''' 37 | return self.get_item("devices", device_id) 38 | 39 | def get_bulk_device(self, device_ids, disable_progress=True): 40 | """Get device detail for many IDs 41 | :param device_ids: list of device_ids 42 | """ 43 | baseURL = self.baseURL + "devices/v2/{}" 44 | urls = [] 45 | if isinstance(device_ids, list): 46 | for device in device_ids: 47 | urls.append(baseURL.format(device)) 48 | return self._bulk_get(urls, paginated=False, disable_progress=disable_progress) 49 | 50 | def get_device_by_mac(self, mac): 51 | '''Get Device Detail by MAC Address''' 52 | 53 | return self.get_item("devices", "macaddress/{}".format(mac)) 54 | 55 | def get_device_threats(self, device_id, **kwargs): 56 | # /devices/v2/{unique_device_id}/threats?page=m&page_size=n 57 | detail = "/{}/threats".format(device_id) 58 | 59 | return self.get_list_items("devices", detail=detail, **kwargs) 60 | 61 | def get_zone_devices(self, zone_id, **kwargs): 62 | '''Return list of devices for a given zone''' 63 | #/devices/v2/{unique_zone_id}/devices?page=m&page_size=n 64 | 65 | detail = "/" + zone_id + "/devices" 66 | return self.get_list_items("devices", detail=detail, **kwargs) 67 | 68 | def update_device(self, device_id, device): 69 | """endpoint: /devices/v2/{unique_device_id}""" 70 | baseURL = self.baseURL + "devices/v2/{}".format(device_id) 71 | return self._make_request("put",baseURL, data=device) 72 | 73 | def update_device_threat(self, device_id, event, threat_id): 74 | """endpoint: /devices/v2/{unique_device_id}/threats""" 75 | baseURL = self.baseURL + "devices/v2/{}/threats".format(device_id) 76 | valid_events = ["Quarantine", "Waive"] 77 | self._validate_parameters(event, valid_events) 78 | data = {"threat_id": threat_id, "event":event} 79 | return self._make_request("post",baseURL, data=data) 80 | 81 | def get_agent_installer_link(self, product, os, arch, package, build=None): 82 | """endpoint: /devices/v2/installer?product=p&os=o&package=k&architecture=a&build=v""" 83 | baseURL = self.baseURL + "devices/v2/installer" 84 | valid_products = ["Protect", "Optics", "Protect Optics"] 85 | valid_packages = ["Exe", "Msi", "Dmg", "Pkg", ""] 86 | valid_archs = ["X86", "X64", "AmazonLinux1", "AmazonLinux2", "CentOS6", "CentOS6UI", 87 | "CentOS7", "CentOS7UI", "Ubuntu1404", "Ubuntu1404UI", "Ubuntu1604", 88 | "Ubuntu1604UI", "Ubuntu1804", "Ubuntu1804UI" ] 89 | valid_os = [ "AmazonLinux1", "AmazonLinux2", "CentOS7", "Linux", "Mac", "Ubuntu1404", 90 | "Ubuntu1604", "Ubuntu1804", "Windows" ] 91 | 92 | self._validate_parameters(product, valid_products) 93 | self._validate_parameters(os, valid_os) 94 | self._validate_parameters(package, valid_packages) 95 | self._validate_parameters(arch, valid_archs) 96 | params = {"product": product, "os": os, "architecture": arch, "package": package, "build": build} 97 | baseURL = self._add_url_params(baseURL, params) 98 | 99 | return self._make_request("get",baseURL) 100 | -------------------------------------------------------------------------------- /cyapi/mixins/_Exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_detection_exceptions(self, csv=False, **kwargs): 5 | # /exceptions/v2 6 | #/exceptions/v2/csv 7 | if csv: 8 | baseURL = self.baseURL + "exceptions/v2/csv" 9 | return self._make_request("get",baseURL) 10 | 11 | return self.get_list_items("exceptions",**kwargs) 12 | 13 | def get_detection_exception(self, exc_id): 14 | # /exceptions/v2/{exception_id} 15 | baseURL = self.baseURL + "exceptions/v2/{}".format(exc_id) 16 | return self._make_request("get",baseURL) 17 | 18 | def create_detection_exception(self, exception_data): 19 | # /exceptions/v2 20 | baseURL = self.baseURL + "exceptions/v2" 21 | return self._make_request("post",baseURL, data=exception_data) 22 | 23 | def update_detection_exception(self, exception_id, exception_data): 24 | # /exceptions/v2/{exception_id} 25 | baseURL = self.baseURL + "exceptions/v2/{}".format(exception_id) 26 | return self._make_request("put",baseURL, data=exception_data) 27 | 28 | def deactivate_detection_exception(self, exception_id): 29 | # /exceptions/v2/{exception_id}/deactivate 30 | baseURL = self.baseURL + "exceptions/v2/{}/deactivate".format(exception_id) 31 | return self._make_request("post",baseURL) 32 | -------------------------------------------------------------------------------- /cyapi/mixins/_Focus_View.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_focus_view_summary(self, focus_id): 5 | # /foci/v2/{focus_id} 6 | baseURL = self.baseURL + "foci/v2/{}".format(focus_id) 7 | return self._make_request("get",baseURL) 8 | 9 | def get_focus_view_results(self, focus_id): 10 | # /foci/v2/{{focus_id}}/results 11 | baseURL = self.baseURL + "foci/v2/{}/results".format(focus_id) 12 | return self._make_request("get",baseURL) 13 | 14 | def get_focus_views(self, q=None, **kwargs): 15 | """q - case-insensitive search term""" 16 | # https://protectapi.cylance.com/foci/v2?page=1&page_size=100 17 | params = {} 18 | if q: 19 | params['q']= q 20 | return self.get_list_items("foci", params=params, **kwargs) 21 | 22 | def search_for_focus_view_results(self, uid, device_id): 23 | # /foci/v2/search 24 | baseURL = self.baseURL + "foci/v2/search" 25 | uid = self._convert_id(uid) 26 | device_id = self._convert_id(device_id) 27 | data = [ 28 | { 29 | "uid": uid, 30 | "device_id": device_id 31 | } 32 | ] 33 | return self._make_request("post",baseURL, data=data) 34 | 35 | def request_focus_view(self, device_id, artifact_type, artifact_subtype, 36 | value, threat_type, description): 37 | # /foci/v2 38 | self._is_valid_artifact_type(artifact_type) 39 | 40 | baseURL = self.baseURL + "foci/v2" 41 | device_id = self._convert_id(device_id) 42 | 43 | data = { 44 | "device_id": device_id, 45 | "artifact_type": artifact_type, 46 | "artifact_subtype": "Uid", 47 | "value": value, 48 | "threat_type": threat_type, 49 | "description": description 50 | } 51 | 52 | return self._make_request("post",baseURL, data=data) 53 | -------------------------------------------------------------------------------- /cyapi/mixins/_Global_List.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def add_to_global_list(self, global_list, reason, sha256, category="None"): 5 | if global_list.lower() == "safe": 6 | list_type = "GlobalSafe" 7 | elif global_list.lower() == "quarantine": 8 | list_type = "GlobalQuarantine" 9 | else: 10 | raise ValueError("global_list must be 'safe' or 'quarantine'") 11 | 12 | categories = ["AdminTool", "CommercialSoftware", "Drivers", "internalApplication", "OperatingSystem", "SecuritySoftware", "None"] 13 | if category not in categories: 14 | raise ValueError("Category must be one of: {}".format(categories)) 15 | 16 | data = { 17 | "sha256": sha256.lower(), 18 | "list_type": list_type, 19 | "category": category, 20 | "reason": reason 21 | } 22 | 23 | baseURL = self.baseURL + "globallists/v2" 24 | return self._make_request("post",baseURL, data=data) 25 | 26 | def delete_from_global_list(self, global_list, sha256): 27 | if global_list.lower() == "safe": 28 | list_type = "GlobalSafe" 29 | elif global_list.lower() == "quarantine": 30 | list_type = "GlobalQuarantine" 31 | else: 32 | raise ValueError("global_list must be 'safe' or 'quarantine'") 33 | 34 | data = { 35 | "sha256": sha256.lower(), 36 | "list_type": list_type, 37 | } 38 | 39 | baseURL = self.baseURL + "globallists/v2" 40 | return self._make_request("delete",baseURL, data=data) 41 | 42 | def get_global_list(self, global_list, **kwargs): 43 | 44 | if global_list.lower() == "quarantine": 45 | type_id = 0 46 | elif global_list.lower() == "safe": 47 | type_id = 1 48 | else: 49 | raise ValueError("global_list value must be 'safe' or 'quarantine'") 50 | 51 | params = {"listTypeId": type_id} 52 | return self.get_list_items('globallists',limit=20, params=params, **kwargs) 53 | -------------------------------------------------------------------------------- /cyapi/mixins/_InstaQueries.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from pprint import pprint 4 | 5 | class Mixin: 6 | # Method to clone an IQ for fresh results. Takes existing IQ ID and returns Cloned IQ ID. 7 | def clone_iq(self, query_id, new_name, new_description=""): 8 | response = self.get_instaquery(query_id) 9 | old_iq = response.json() 10 | if self.debug_level > 1: 11 | for name in old_iq: 12 | print( name, ": ", old_iq[name] ) 13 | 14 | new_iq = self.create_instaquery(new_name, new_description, old_iq['artifact'], old_iq['match_value_type'], old_iq['match_values'], old_iq['case_sensitive'], old_iq['zones'] ) 15 | return new_iq 16 | 17 | # Generic method to generate an InstaQuery request 18 | def create_instaquery(self, query_name, query_description, artifact_type, facet_type, match_values, case_sensitive, zones): 19 | baseURL = self.baseURL + "instaqueries/v2" 20 | requestBody = { 21 | "name": query_name, 22 | "description": query_description, 23 | "artifact": artifact_type, 24 | "match_value_type": facet_type, 25 | "match_values": match_values, 26 | "case_sensitive": case_sensitive, 27 | "match_type": "Fuzzy", 28 | "zones": zones 29 | } 30 | 31 | if self.debug_level > 2: 32 | pprint( requestBody ) 33 | 34 | return self._make_request("post",baseURL, data=requestBody) 35 | 36 | def get_instaqueries(self, q=None, archived=None, origin_from=None, **kwargs): 37 | params = { 38 | "q": q, 39 | "archived": archived, 40 | "originated-from": origin_from 41 | } 42 | 43 | return self.get_list_items("instaqueries", params=params, **kwargs) 44 | 45 | def get_instaquery_results(self, iq_id): 46 | baseURL = self.baseURL + "instaqueries/v2/{}/results".format(iq_id) 47 | return self._make_request("get",baseURL) 48 | 49 | # Method to retrieve an InstaQuery 50 | def get_instaquery(self, query_id): 51 | baseURL = self.baseURL + "instaqueries/v2/{}".format(query_id) 52 | 53 | return self._make_request("get",baseURL) 54 | 55 | def archive_instaquery(self, query_id): 56 | """endpoint: /instaqueries/v2{queryID}/archive 57 | """ 58 | baseURL = self.baseURL + "instaqueries/v2/{}/archive".format(query_id) 59 | return self._make_request("post",baseURL) 60 | -------------------------------------------------------------------------------- /cyapi/mixins/_MTC_HealthCheck.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import requests # requests version 2.18.4 as of the time of authoring. 4 | 5 | 6 | class Mixin: 7 | 8 | def get_mtc_health_check(self, **kwargs): 9 | """Gets MTC API Operational State 10 | No Authorization is required 11 | Sample Data Returned: {"Version: 0.81.480.0 | Environment: 'Production'"} 12 | """ 13 | baseURL = self.baseURL + "health-check" 14 | 15 | response = requests.get(baseURL) 16 | 17 | 18 | return ApiResponse(response) 19 | 20 | class ApiResponse: 21 | def __init__(self, response): 22 | self.status_code = response.status_code 23 | self.is_success = response.status_code < 300 24 | self.data = None 25 | self.errors = None 26 | 27 | if self.status_code == 429: 28 | print("API Rate Limited") 29 | 30 | if self.is_success: 31 | try: 32 | #The try/except and re.sub lines accept the 33 | # non-json values/messages when a successful response 34 | # is sent. This seems unique to the HealthCheck endpoint. 35 | # This should omit a 'None' data response. 36 | try: 37 | self.data = response.json() 38 | except json.decoder.JSONDecodeError: 39 | self.data = re.sub('{|"|}', "",response.text) 40 | except json.decoder.JSONDecodeError: 41 | self.data = None 42 | 43 | else: 44 | try: 45 | self.errors = response.json() 46 | except json.decoder.JSONDecodeError: 47 | self.errors = None 48 | 49 | def to_json(self): 50 | return {'is_success': self.is_success, 'data': self.data, 'errors': self.errors} 51 | -------------------------------------------------------------------------------- /cyapi/mixins/_MTC_PolicyTemplates.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pkg_resources 4 | 5 | class Mixin: 6 | 7 | def get_policy_templates(self, **kwargs): 8 | """Get a list of all MTC Device Policy Templates 9 | Returns: listData: [PolicyTemplates], totalCount 10 | """ 11 | baseURL = self.baseURL + "policy-templates/list" 12 | return self._make_request("get",baseURL) 13 | 14 | def get_policy_template(self, policy_template_id): 15 | """Get MTC Policy Template Detail 16 | parms: Takes policy_template_id 17 | """ 18 | return self.get_item("policy-templates", policy_template_id) 19 | 20 | def update_policy_template(self, policy): 21 | """ Update MTC Device Policy Template by policy_template_id 22 | """ 23 | baseURL = self.baseURL + "policy-templates/{}".format(policy['policy_id']) 24 | 25 | policy['checksum'] = "" 26 | policy['policy_utctimestamp'] = "" 27 | 28 | data = {} 29 | data['modelDetails'] = policy 30 | data['name'] = policy['name'] 31 | data['partnerId'] = self.app_id 32 | data['policyTemplateId'] = policy['policy_id'] 33 | 34 | return self._make_request("put",baseURL, data=data) 35 | 36 | def delete_policy_template(self, policy_template_id): 37 | """Delete MTC Device Policy Template by policy_template_id""" 38 | baseURL = self.baseURL + "policy-templates/{}".format(policy_template_id) 39 | 40 | return self._make_request("delete",baseURL) 41 | 42 | def create_device_policy_template(self, policy_name, policy_data=None): 43 | """Create a MTC Device Policy Template 44 | """ 45 | baseURL = self.baseURL + "policy-templates" 46 | 47 | if policy_data is None: 48 | 49 | f = pkg_resources.resource_stream('cyapi', "reqs/create_policy.json") 50 | data = json.loads(f.read().decode('utf-8')) 51 | f.close() 52 | 53 | else: 54 | data = {'modelDetails': policy_data} 55 | data['policy_id'] = "" 56 | data['modelDetails']['checksum'] = "" 57 | data['modelDetails']['policy_utctimestamp'] = "" 58 | 59 | 60 | data['modelDetails'] = data 61 | data['name'] = policy_name 62 | data['partnerId'] = self.app_id 63 | response = self._make_request("post",baseURL) 64 | 65 | return response 66 | 67 | -------------------------------------------------------------------------------- /cyapi/mixins/_MTC_Reports.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_mtc_report_runs(self, **kwargs): 5 | """Get a list of all MTC Report Runs 6 | Returns: listData: [ReportRuns], totalCount 7 | """ 8 | baseURL = self.baseURL + "reports/report-runs" 9 | return self._make_request("get",baseURL) 10 | 11 | def get_mtc_report(self, report_run_id): 12 | """Delete MTC Device Policy Template by policy_template_id""" 13 | baseURL = self.baseURL + "reports/report-runs/{}/download".format(report_run_id) 14 | return self._make_request("get",baseURL) 15 | 16 | def get_mtc_reports(self, **kwargs): 17 | """Get a list of all MTC Reports 18 | Returns: listData: [Reports], totalCount 19 | """ 20 | baseURL = self.baseURL + "reports/list" 21 | return self._make_request("get",baseURL) 22 | 23 | def run_mtc_report(self, report_run_id): 24 | """Delete MTC Device Policy Template by policy_template_id""" 25 | baseURL = self.baseURL + "reports/report-runs/{}/download".format(report_run_id) 26 | return self._make_request("post", baseURL) -------------------------------------------------------------------------------- /cyapi/mixins/_MTC_Tenants.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | '''TODO: Test Policy and 4 | create provision, update, get install-token, regen install-token 5 | ''' 6 | 7 | def get_tenant_app(self, venueTenantId): 8 | """Get MTC Tenant's Bearer Token 9 | The Bearer JWT is returned as a string in APIRequest.data 10 | """ 11 | baseURL = self.baseURL + "tenants/tenant-app/{}".format(venueTenantId) 12 | return self._make_request("put", baseURL) 13 | 14 | def apply_policy(self, venueTenantIds, policyTemplateId): 15 | """ 16 | Applies a policy template to a group of tenants. Max of 50 tenants can be in the request. 17 | venueTenantIds is a list object. 18 | """ 19 | print("In apply_policy") 20 | 21 | data = { 22 | "venueTenantIds": venueTenantIds, 23 | "policyTemplateId": policyTemplateId 24 | } 25 | 26 | return self._make_request("post",baseURL, data=data) 27 | 28 | def get_tenants(self, **kwargs): 29 | return self.get_item("tenants", "list", **kwargs) 30 | 31 | -------------------------------------------------------------------------------- /cyapi/mixins/_MTC_Users.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | 5 | def resend_mtc_invite_email(self, user_id): 6 | """Resend MTC user invitation 7 | Params: user_id 8 | """ 9 | baseURL = self.baseURL + "users/{}/resend-invite".format(user_id) 10 | 11 | return self._make_request("post",baseURL) 12 | 13 | def create_mtc_user(self, email, first, last, roleNames=["Partner Administrator"]): 14 | """Create MTC User 15 | Params: email, First, Last, roleNames[] 16 | """ 17 | 18 | data = { 19 | "first_name": first, 20 | "last_name": last, 21 | "roleName": roleNames, 22 | "email": email 23 | } 24 | 25 | baseURL = self.baseURL + "users" 26 | return self._make_request("post",baseURL, data=data) 27 | 28 | def update_mtc_user(self, user_id, user_obj): 29 | """Update a MTC User 30 | Params: user_id, user_obj 31 | """ 32 | baseURL = self.baseURL + "users/{}".format(user_id) 33 | return self._make_request("put",baseURL, data=user_obj) 34 | 35 | def delete_mtc_user(self, uid): 36 | """Delete MTC User""" 37 | baseURL = self.baseURL + "users/{}".format(uid) 38 | return self._make_request("delete",baseURL) 39 | 40 | def get_mtc_users(self, **kwargs): 41 | """Get MTC Users 42 | Returns: listData: [Users], totalCount 43 | """ 44 | baseURL = self.baseURL + "users/list" 45 | return self._make_request("get",baseURL) 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /cyapi/mixins/_Memory_Protection.py: -------------------------------------------------------------------------------- 1 | class Mixin: 2 | 3 | def get_memory_protection_events(self, start_time=None, end_time=None, **kwargs): 4 | '''Get a list of Memoryprotection Events''' 5 | if end_time and not start_time: 6 | raise ValueError("start_time must be set if using end_time parameter") 7 | params = {"start_time": start_time, "end_time": end_time} 8 | 9 | return self.get_list_items('memoryprotection', params=params, **kwargs) 10 | 11 | 12 | def get_memory_protection_event(self, device_image_file_event_id): 13 | '''Get Memoryprotection Detail''' 14 | return self.get_item("memoryprotection", device_image_file_event_id) 15 | 16 | -------------------------------------------------------------------------------- /cyapi/mixins/_Optics_Policies.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | 3 | class Mixin: 4 | 5 | def get_rule_sets_to_policy_mapping(self, **kwargs): 6 | return self.get_list_items("opticsPolicies","/configurations", **kwargs) 7 | 8 | def update_ruleset_in_policy(self, ruleset, policy): 9 | '''Success Code: 200''' 10 | baseURL = self.baseURL + "opticsPolicies/v2/configurations" 11 | 12 | data = { 13 | "configuration_type": "DETECTION", 14 | "configuration_id": ruleset['id'], 15 | "link": [policy['policy_id'],], 16 | "unlink": [] 17 | } 18 | 19 | if self.debug_level > 1: 20 | pprint(data) 21 | 22 | return self._make_request("post",baseURL, data=data) 23 | 24 | def remove_policy_from_ruleset(self, ruleset, policy_id): 25 | baseURL = self.baseURL + "opticsPolicies/v2/configurations" 26 | 27 | data = { 28 | "configuration_type": "DETECTION", 29 | "configuration_id": ruleset['id'], 30 | "link": [], 31 | "unlink": [policy_id,] 32 | } 33 | 34 | return self._make_request("post",baseURL, data=data) 35 | 36 | def get_ruleset_for_policy(self, policy_id): 37 | baseURL = self.baseURL + "opticsPolicies/v2/configurations/{}".format(policy_id) 38 | 39 | return self._make_request("get",baseURL) 40 | -------------------------------------------------------------------------------- /cyapi/mixins/_Packages.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_package_execution(self, exec_id): 5 | # /packages/v2/executions/{unique_execution_id} 6 | baseURL = self.baseURL + "packages/v2/executions/{}".format(exec_id) 7 | return self._make_request("get",baseURL) 8 | 9 | def get_package_executions(self,exec_id): 10 | # /packages/v2/executions/{unique_execution_id} 11 | baseURL = self.baseURL + "packages/v2/executions/{}".format(exec_id) 12 | return self._make_request("get",baseURL) 13 | 14 | def delete_package_execution(self, exec_id): 15 | # /packages/v2/executions/{unique_execution_id} 16 | baseURL = self.baseURL + "packages/v2/executions/{}".format(exec_id) 17 | return self._make_request("delete",baseURL) 18 | 19 | def delete_package(self, package_id): 20 | # /packages/v2/{unique_package_id} 21 | baseURL = self.baseURL + "packages/v2/{}".format(package_id) 22 | return self._make_request("delete",baseURL) 23 | 24 | # Method to retrieve List of Packages 25 | def get_packages(self, **kwargs): 26 | 27 | return self.get_list_items("packages", **kwargs) 28 | 29 | # Method to execute a package by zone 30 | def execute_packages_by_zone(self, execution_name, zones, destination, arguments, package_to_execute, keepResultsLocally): 31 | baseURL = self.baseURL + "packages/v2/executions" 32 | 33 | data = { 34 | "execution": { 35 | "name": execution_name, 36 | "target": { 37 | "zones": zones 38 | }, 39 | "destination": destination, 40 | "packageExecutions": [ 41 | { 42 | "arguments": arguments, 43 | "package": package_to_execute 44 | } 45 | ], 46 | "keepResultsLocally": keepResultsLocally 47 | } 48 | } 49 | 50 | return self._make_request("post",baseURL, data=data) 51 | 52 | def create_package(self, package_data): 53 | # /packages/v2 54 | baseURL = self.baseURL + "packages/v2" 55 | return self._make_request("post",baseURL, data=package_data) 56 | 57 | def create_package_execution(self, package_data): 58 | # /packages/v2/executions 59 | baseURL = self.baseURL + "packages/v2/executions" 60 | return self._make_request("post",baseURL, data=package_data) 61 | -------------------------------------------------------------------------------- /cyapi/mixins/_Policies.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pkg_resources 4 | 5 | class Mixin: 6 | 7 | def delete_policies(self, policy_ids): 8 | """Delete multiple policies 9 | :param policy_ids: List of policy IDs to delete 10 | :return APIResponse 11 | """ 12 | baseURL = self.baseURL + "policies/v2" 13 | 14 | if type(policy_ids) != list: 15 | raise TypeError("policy_ids must be a list type") 16 | 17 | data = { 18 | "tenant_policy_ids": policy_ids 19 | } 20 | 21 | return self._make_request("delete", baseURL, data=data) 22 | 23 | 24 | def get_policies(self, **kwargs): 25 | """Get a list of all Device Policies""" 26 | policies = self.get_list_items("policies", **kwargs) 27 | return policies 28 | 29 | def get_policy(self, policy_id): 30 | '''Get Policy Detail''' 31 | return self.get_item("policies", policy_id) 32 | 33 | def get_bulk_policy(self, policy_ids, disable_progress=True): 34 | """Get policy detail for many IDs 35 | :param policy_ids: list of policy_ids 36 | """ 37 | baseURL = self.baseURL + "policies/v2/{}" 38 | urls = [] 39 | if isinstance(policy_ids, list): 40 | for policy in policy_ids: 41 | urls.append(baseURL.format(policy)) 42 | return self._bulk_get(urls, paginated=False, disable_progress=disable_progress) 43 | 44 | def delete_policy(self, policy_id): 45 | '''Delete policy by policy_id''' 46 | baseURL = self.baseURL + "policies/v2/{}".format(policy_id) 47 | 48 | return self._make_request("delete",baseURL) 49 | 50 | def create_device_policy(self, policy_name, policy_data=None): 51 | '''Success Code: 201''' 52 | baseURL = self.baseURL + "policies/v2" 53 | 54 | if policy_data is None: 55 | 56 | f = pkg_resources.resource_stream('cyapi', "reqs/create_policy.json") 57 | data = json.loads(f.read().decode('utf-8')) 58 | f.close() 59 | 60 | else: 61 | data = {'policy': policy_data} 62 | data['policy_id'] = "" 63 | data['policy']['checksum'] = "" 64 | data['policy']['policy_utctimestamp'] = "" 65 | 66 | data['user_id'] = self.app_id 67 | data['policy']['policy_name'] = policy_name 68 | response = self._make_request("post",baseURL, data=data) 69 | 70 | return response 71 | 72 | def update_policy(self, policy): 73 | '''Success Code: 204''' 74 | baseURL = self.baseURL + "policies/v2" 75 | 76 | policy['checksum'] = "" 77 | policy['policy_utctimestamp'] = "" 78 | 79 | request = {} 80 | request['user_id'] = self.app_id 81 | request['policy'] = policy 82 | 83 | return self._make_request("put",baseURL, data=request) 84 | 85 | def add_scan_exclusions(self, exclusions, policy): 86 | for exclusion in exclusions: 87 | policy = self.append_policy_item('scan_exception_list', exclusion, policy) 88 | return policy 89 | 90 | def add_script_exclusions(self, exclusions, policy): 91 | if type(policy['script_control']['global_settings']['allowed_folders']) == str: 92 | policy['script_control']['global_settings']['allowed_folders'] = [] 93 | if type(exclusions) == list: 94 | policy['script_control']['global_settings']['allowed_folders'] += exclusions 95 | else: 96 | policy['script_control']['global_settings']['allowed_folders'].append(exclusions) 97 | 98 | return policy 99 | 100 | def add_mem_exclusions(self, exclusions, policy): 101 | policy = self.append_mem_exclusion_items(exclusions, policy) 102 | return policy 103 | 104 | def add_template_exclusions(self, tpl_name, policy): 105 | 106 | f = pkg_resources.resource_stream('cyapi', "exclusions/{}.json".format(tpl_name)) 107 | data = json.loads(f.read().decode('utf-8')) 108 | f.close() 109 | 110 | exclusions = data.get('memory',None) 111 | if exclusions is not None: 112 | policy = self.add_mem_exclusions(exclusions, policy) 113 | 114 | exclusions = data.get('protection', None) 115 | if exclusions is not None: 116 | policy = self.add_scan_exclusions(exclusions, policy) 117 | 118 | exclusions = data.get('script', None) 119 | if exclusions is not None: 120 | policy = self.add_script_exclusions(exclusions, policy) 121 | 122 | exclusions = data.get('trust_files_in_scan_exception_list', None) 123 | if exclusions is not None: 124 | if exclusions == True or exclusions == 1: 125 | policy = self.set_policy_item("trust_files_in_scan_exception_list", 1, policy) 126 | 127 | return policy 128 | 129 | def set_memdef(self, enabled, policy, mode="Alert"): 130 | 131 | if enabled == True: 132 | policy = self.set_policy_item('memory_exploit_detection',1,policy) 133 | policy = self.set_memdef_actions(policy, mode) 134 | elif enabled == False: 135 | policy = self.set_policy_item('memory_exploit_detection',0,policy) 136 | 137 | return policy 138 | 139 | def set_memdef_actions(self, policy, mode): 140 | for item in policy['memoryviolation_actions']['memory_violations']: 141 | item['action'] = mode 142 | 143 | for item in policy['memoryviolation_actions']['memory_violations_ext']: 144 | item['action'] = mode 145 | return policy 146 | 147 | def set_script_control(self, enabled, policy, mode="Alert", allowed_folders=None): 148 | 149 | if enabled == True: 150 | policy = self.set_policy_item('script_control',1,policy) 151 | policy = self.set_script_control_action(mode, mode,"Allow",mode, 152 | mode,allowed_folders,policy) 153 | elif enabled == False: 154 | policy = self.set_policy_item('script_control', 0, policy) 155 | 156 | return policy 157 | 158 | # TODO: Break this apart into individual functions 159 | def set_script_control_action(self, 160 | act_scr, 161 | macro, 162 | ps_console, 163 | ps_control, 164 | global_control, 165 | allowed_folders, 166 | policy): 167 | policy['script_control']["activescript_settings"]['control_mode'] = act_scr 168 | policy['script_control']["macro_settings"]['control_mode'] = macro 169 | policy['script_control']["powershell_settings"]['control_mode'] = ps_control 170 | policy['script_control']["powershell_settings"]['console_mode'] = ps_console 171 | 172 | 173 | policy['script_control']["global_settings"]['control_mode'] = global_control 174 | if allowed_folders is not None: 175 | policy['script_control']["global_settings"]['allowed_folders'] = allowed_folders 176 | return policy 177 | 178 | def enable_aqt(self, policy): 179 | policy = self.set_policy_item('auto_blocking', 1, policy) 180 | for item in policy['filetype_actions']['suspicious_files']: 181 | item['actions'] = 3 182 | for item in policy['filetype_actions']['threat_files']: 183 | item['actions'] = 3 184 | 185 | return policy 186 | 187 | def disable_btd(self, policy): 188 | return self.set_btd(0, policy) 189 | 190 | def enable_btd_once(self, policy): 191 | return self.set_btd(2, policy) 192 | 193 | def enable_btd_reocurring(self, policy): 194 | return self.set_btd(1, policy) 195 | 196 | def set_btd(self, val, policy): 197 | policy = self.set_policy_item('full_disc_scan', val, policy) 198 | return policy 199 | 200 | def disable_notifications(self, policy): 201 | policy = self.set_policy_item('show_notifications', "0", policy) 202 | return policy 203 | 204 | def enable_notifications(self, policy): 205 | policy = self.set_policy_item('show_notifications', "1", policy) 206 | return policy 207 | 208 | def enable_optics(self, policy): 209 | policy = self.set_policy_item('optics', 1, policy) 210 | return policy 211 | 212 | def set_policy_item(self, key, val, policy): 213 | for item in policy['policy']: 214 | if item['name'] == key: 215 | item['value'] = val 216 | return policy 217 | 218 | def get_policy_item(self, key, policy): 219 | for item in policy['policy']: 220 | if item['name'] == key: 221 | return item 222 | 223 | def append_policy_item(self, key, val, policy): 224 | for item in policy['policy']: 225 | if item['name'] == key: 226 | item['value'].append(val) 227 | return policy 228 | 229 | def append_mem_exclusion_items(self, items, policy): 230 | if type(items) == list: 231 | policy['memoryviolation_actions']['memory_exclusion_list'] += items 232 | else: 233 | policy['memoryviolation_actions']['memory_exclusion_list'].append(items) 234 | 235 | return policy 236 | -------------------------------------------------------------------------------- /cyapi/mixins/_Rules.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_detection_rule(self, rule_id, natlang=False): 5 | # /rules/v2/{rule_id} 6 | if natlang == True: 7 | baseURL = self.baseURL + "rules/v2/{}/natlang".format(rule_id) 8 | return self._make_request("get",baseURL) 9 | 10 | return self.get_item("rules", rule_id) 11 | 12 | def get_bulk_detection_rule(self, rule_ids, disable_progress=True): 13 | """Get rule detail for many IDs 14 | :param rule_ids: list of rule_ids 15 | """ 16 | baseURL = self.baseURL + "rules/v2/{}" 17 | urls = [] 18 | if isinstance(rule_ids, list): 19 | for rule in rule_ids: 20 | urls.append(baseURL.format(rule)) 21 | return self._bulk_get(urls, paginated=False, disable_progress=disable_progress) 22 | 23 | def get_detection_rules(self, csv=False, **kwargs): 24 | # /rules/v2?page=m&page_size=n 25 | if csv: 26 | baseURL = self.baseURL + "rules/v2/csv" 27 | return self._make_request("get",baseURL) 28 | 29 | return self.get_list_items("rules", **kwargs) 30 | 31 | def get_detection_rule_counts(self, rule_id): 32 | # /rules/v2/{rule_id}/counts 33 | baseURL = self.baseURL + "rules/v2/{}/counts".format(rule_id) 34 | return self._make_request("get",baseURL) 35 | 36 | def validate_detection_rule(self, rule_data): 37 | # /rules/v2/validate 38 | baseURL = self.baseURL + "rules/v2/validate" 39 | return self._make_request("post",baseURL, data=rule_data) 40 | 41 | def create_detection_rule(self, rule_data): 42 | # /rules/v2 43 | baseURL = self.baseURL + "rules/v2" 44 | return self._make_request("post",baseURL, data=rule_data) 45 | 46 | def update_detection_rule(self, rule_id, rule_data): 47 | # /rules/v2/{rule_id} 48 | baseURL = self.baseURL + "rules/v2/{}".format(rule_id) 49 | return self._make_request("put",baseURL, data=rule_data) 50 | 51 | def deactivate_detection_rule(self, rule_id): 52 | # /rules/v2/{rule_id}/deactivate 53 | baseURL = self.baseURL + "rules/v2/{}/deactivate".format(rule_id) 54 | return self._make_request("post",baseURL) 55 | -------------------------------------------------------------------------------- /cyapi/mixins/_Rulesets.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | def create_detection_rule_set(self, ruleset_data): 4 | 5 | return self.create_item('rulesets', ruleset_data) 6 | 7 | def delete_rule_sets(self, ruleset_ids): 8 | # /rulesets/v2 9 | baseURL = self.baseURL + "rulesets/v2" 10 | data = { "ids": ruleset_ids } 11 | 12 | return self._make_request("delete",baseURL, data=data) 13 | 14 | def delete_rule_set(self, ruleset_id): 15 | '''Delete ruleset by ID''' 16 | baseURL = self.baseURL + "rulesets/v2/{}".format(ruleset_id) 17 | 18 | return self._make_request("delete",baseURL) 19 | 20 | def get_ruleset(self, ruleset_id): 21 | '''Return detailed Ruleset information''' 22 | ruleset = self.get_item("rulesets", ruleset_id) 23 | return ruleset 24 | 25 | def get_bulk_ruleset(self, ruleset_ids, disable_progress=True): 26 | """Get ruleset detail for many IDs 27 | :param ruleset_ids: list of ruleset_ids 28 | """ 29 | baseURL = self.baseURL + "rulesets/v2/{}" 30 | urls = [] 31 | if isinstance(ruleset_ids, list): 32 | for ruleset in ruleset_ids: 33 | urls.append(baseURL.format(ruleset)) 34 | return self._bulk_get(urls, paginated=False, disable_progress=disable_progress) 35 | 36 | def get_rulesets(self, csv=False, **kwargs): 37 | if csv: 38 | rulesets = self.get_list_items("rulesets", detail="/csv", **kwargs) 39 | else: 40 | rulesets = self.get_list_items("rulesets", **kwargs) 41 | return rulesets 42 | 43 | def update_detection_rule_set(self, ruleset_id, ruleset): 44 | # TODO: Add niceties for validating a ruleset and stuff that you can update 45 | # /rulesets/v2/{ruleset_id} 46 | baseURL = self.baseURL + "rulesets/v2/{}".format(ruleset_id) 47 | return self._make_request("put",baseURL, data=ruleset) 48 | 49 | def retrieve_default_detection_rule_set(self): 50 | # /rulesets/v2/default 51 | baseURL = self.baseURL + "rulesets/v2/default" 52 | return self._make_request("get",baseURL) 53 | -------------------------------------------------------------------------------- /cyapi/mixins/_Threats.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def get_threats(self, start_time=None, end_time=None, **kwargs): 5 | '''Get a list of Threats''' 6 | if end_time and not start_time: 7 | raise ValueError("start_time must be set if using end_time parameter") 8 | params = {"start_time": start_time, "end_time": end_time} 9 | 10 | return self.get_list_items('threats', params=params, **kwargs) 11 | 12 | def get_threat_devices(self, sha256, **kwargs): 13 | # /threats/v2/{threat_sha256}/devices?page=m&page_size=n 14 | return self.get_list_items("threats", "/" + sha256 + "/devices", **kwargs) 15 | 16 | def get_threat(self, threat_id): 17 | '''Get threat Detail''' 18 | return self.get_item("threats", threat_id) 19 | 20 | def get_threat_download_url(self, sha256): 21 | baseURL = self.baseURL + "threats/v2/download/{}".format(sha256) 22 | 23 | return self._make_request("get",baseURL) 24 | -------------------------------------------------------------------------------- /cyapi/mixins/_Users.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def delete_user(self, uid): 5 | baseURL = self.baseURL + "users/v2/{}".format(uid) 6 | 7 | return self._make_request("delete",baseURL) 8 | 9 | def send_invite_email(self, email): 10 | baseURL = self.baseURL + "users/v2/{}/invite".format(email) 11 | 12 | return self._make_request("post",baseURL) 13 | 14 | #def send_request_password_email(self): 15 | def send_password_reset_email(self, email): 16 | baseURL = self.baseURL + "users/v2/{}/resetpassword".format(email) 17 | 18 | return self._make_request("post",baseURL) 19 | 20 | def get_users(self, **kwargs): 21 | return self.get_list_items('users', **kwargs) 22 | 23 | def get_user(self, user_id): 24 | return self.get_item("users", user_id) 25 | 26 | def create_user(self, email, first, last, user_role="Administrator", zones=[]): 27 | """ 28 | endpoint: /users/v2 29 | zones: parameter Example - 30 | "zones": [ 31 | { 32 | "id": "d27ff5c4-5c0d-4f56-a00d-a1fb297e440e", 33 | "role_type": "00000000-0000-0000-0000-000000000002" 34 | } 35 | ] 36 | """ 37 | 38 | roles = { 39 | "User": "00000000-0000-0000-0000-000000000001", 40 | "Administrator": "00000000-0000-0000-0000-000000000002", 41 | "Zone Manager": "00000000-0000-0000-0000-000000000003" 42 | } 43 | self._validate_parameters(user_role, roles.keys()) 44 | 45 | data = { 46 | "email": email, 47 | "user_role": roles[user_role], 48 | "first_name": first, 49 | "last_name": last, 50 | "zones": zones 51 | } 52 | 53 | if not zones or user_role == "Administrator": 54 | data.pop("zones") 55 | 56 | baseURL = self.baseURL + "users/v2" 57 | return self._make_request("post",baseURL, data=data) 58 | 59 | def update_user(self, user_id, user_obj): 60 | """endpoint: /users/v2/{user_id}""" 61 | 62 | baseURL = self.baseURL + "users/v2/{}".format(user_id) 63 | 64 | return self._make_request("put",baseURL, data=user_obj) 65 | -------------------------------------------------------------------------------- /cyapi/mixins/_Zones.py: -------------------------------------------------------------------------------- 1 | 2 | class Mixin: 3 | 4 | def create_zone(self, name, policy): 5 | '''Create a zone and assign policy''' 6 | data = { 7 | "name": name, 8 | "policy_id": policy['policy_id'], 9 | "criticality": "Normal" 10 | } 11 | 12 | return self.create_item("zones", data) 13 | 14 | def delete_zone(self, zone_id): 15 | '''Delete zone by zone_id''' 16 | baseURL = self.baseURL + "zones/v2/{}".format(zone_id) 17 | 18 | return self._make_request("delete",baseURL) 19 | 20 | def get_zones(self, **kwargs): 21 | zones = self.get_list_items("zones", **kwargs) 22 | return zones 23 | 24 | def get_zone(self, zone_id): 25 | '''Get Policy Detail''' 26 | return self.get_item("zones", zone_id) 27 | 28 | def get_bulk_zone(self, zone_ids, disable_progress=True): 29 | """Get zone detail for many IDs 30 | :param zone_ids: list of zone_ids 31 | """ 32 | baseURL = self.baseURL + "zones/v2/{}" 33 | urls = [] 34 | if isinstance(zone_ids, list): 35 | for zone in zone_ids: 36 | urls.append(baseURL.format(zone)) 37 | return self._bulk_get(urls, paginated=False, disable_progress=disable_progress) 38 | 39 | def get_device_zones(self, device_id, **kwargs): 40 | detail = "/{}/zones".format(device_id) 41 | return self.get_list_items("zones", detail, **kwargs) 42 | 43 | def update_zone(self, zone_id, name, policy_id, criticality): 44 | # /zones/v2/{unique_zone_id} 45 | baseURL = self.baseURL + "zones/v2/{}".format(zone_id) 46 | 47 | data = { 48 | "name": name, 49 | "policy_id": policy_id, 50 | "criticality": criticality 51 | } 52 | return self._make_request("put",baseURL, data=data) 53 | -------------------------------------------------------------------------------- /cyapi/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cylance/python-cyapi/d85e67c56bd17593d0fa523685b455bc1b6fed97/cyapi/mixins/__init__.py -------------------------------------------------------------------------------- /cyapi/reqs/create_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "user_id": "", 3 | "policy": { 4 | "file_exclusions" : [], 5 | "checksum" : "", 6 | "memoryviolation_actions": { 7 | "memory_violations": [ 8 | { 9 | "action": "Block", 10 | "violation_type": "outofprocessallocation" 11 | }, 12 | { 13 | "action": "Block", 14 | "violation_type": "outofprocesswritepe" 15 | }, 16 | { 17 | "action": "Block", 18 | "violation_type": "outofprocessmap" 19 | }, 20 | { 21 | "action": "Block", 22 | "violation_type": "stackpivot" 23 | }, 24 | { 25 | "action": "Block", 26 | "violation_type": "outofprocesswrite" 27 | }, 28 | { 29 | "action": "Block", 30 | "violation_type": "outofprocessunmapmemory" 31 | }, 32 | { 33 | "action": "Block", 34 | "violation_type": "outofprocesscreatethread" 35 | }, 36 | { 37 | "action": "Block", 38 | "violation_type": "lsassread" 39 | }, 40 | { 41 | "action": "Block", 42 | "violation_type": "outofprocessapc" 43 | }, 44 | { 45 | "action": "Block", 46 | "violation_type": "outofprocessoverwritecode" 47 | }, 48 | { 49 | "action": "Block", 50 | "violation_type": "stackprotect" 51 | }, 52 | { 53 | "action": "Block", 54 | "violation_type": "overwritecode" 55 | } 56 | ], 57 | "memory_violations_ext": [ 58 | { 59 | "action": "Block", 60 | "violation_type": "maliciouspayload" 61 | }, 62 | { 63 | "action": "Block", 64 | "violation_type": "trackdataread" 65 | }, 66 | { 67 | "action": "Block", 68 | "violation_type": "zeroallocate" 69 | }, 70 | { 71 | "action": "Block", 72 | "violation_type": "dyldinjection" 73 | } 74 | ], 75 | "memory_exclusion_list": [] 76 | }, 77 | "policy": [ 78 | { 79 | "name": "auto_blocking", 80 | "value": "0" 81 | }, 82 | { 83 | "name": "auto_uploading", 84 | "value": "1" 85 | }, 86 | { 87 | "name": "threat_report_limit", 88 | "value": "500" 89 | }, 90 | { 91 | "name": "low_confidence_threshold", 92 | "value": "-600" 93 | }, 94 | { 95 | "name": "full_disc_scan", 96 | "value": "2" 97 | }, 98 | { 99 | "name": "watch_for_new_files", 100 | "value": "1" 101 | }, 102 | { 103 | "name": "memory_exploit_detection", 104 | "value": "0" 105 | }, 106 | { 107 | "name": "trust_files_in_scan_exception_list", 108 | "value": "0" 109 | }, 110 | { 111 | "name": "logpolicy", 112 | "value": "0" 113 | }, 114 | { 115 | "name": "script_control", 116 | "value": "0" 117 | }, 118 | { 119 | "name": "prevent_service_shutdown", 120 | "value": "1" 121 | }, 122 | { 123 | "name": "scan_max_archive_size", 124 | "value": "125" 125 | }, 126 | { 127 | "name": "sample_copy_path", 128 | "value": "" 129 | }, 130 | { 131 | "name": "kill_running_threats", 132 | "value": "0" 133 | }, 134 | { 135 | "name": "show_notifications", 136 | "value": "0" 137 | }, 138 | { 139 | "name": "optics_set_disk_usage_maximum_fixed", 140 | "value": "1000" 141 | }, 142 | { 143 | "name": "optics_malware_auto_upload", 144 | "value": "0" 145 | }, 146 | { 147 | "name": "optics_memory_defense_auto_upload", 148 | "value": "0" 149 | }, 150 | { 151 | "name": "optics_script_control_auto_upload", 152 | "value": "0" 153 | }, 154 | { 155 | "name": "optics_application_control_auto_upload", 156 | "value": "0" 157 | }, 158 | { 159 | "name": "device_control", 160 | "value": "0" 161 | }, 162 | { 163 | "name": "optics", 164 | "value": "0" 165 | }, 166 | { 167 | "name": "auto_delete", 168 | "value": "0" 169 | }, 170 | { 171 | "name": "days_until_deleted", 172 | "value": "14" 173 | }, 174 | { 175 | "name": "pdf_auto_uploading", 176 | "value": "0" 177 | }, 178 | { 179 | "name": "ole_auto_uploading", 180 | "value": "0" 181 | }, 182 | { 183 | "name": "data_privacy", 184 | "value": "0" 185 | }, 186 | { 187 | "name": "docx_auto_uploading", 188 | "value": "0" 189 | }, 190 | { 191 | "name": "powershell_auto_uploading", 192 | "value": "0" 193 | }, 194 | { 195 | "name": "python_auto_uploading", 196 | "value": "0" 197 | }, 198 | { 199 | "name": "autoit_auto_uploading", 200 | "value": "0" 201 | }, 202 | { 203 | "name": "optics_show_notifications", 204 | "value": "0" 205 | }, 206 | { 207 | "name": "custom_thumbprint", 208 | "value": "null" 209 | }, 210 | { 211 | "name": "scan_exception_list", 212 | "value": [] 213 | } 214 | ], 215 | "policy_name": "CHANGE ME", 216 | "script_control": { 217 | "activescript_settings": { 218 | "control_mode": "Alert" 219 | }, 220 | "global_settings": { 221 | "allowed_folders": [], 222 | "control_mode": "Alert" 223 | }, 224 | "macro_settings": { 225 | "control_mode": "Alert" 226 | }, 227 | "powershell_settings": { 228 | "console_mode": "Allow", 229 | "control_mode": "Alert" 230 | } 231 | }, 232 | "filetype_actions": { 233 | "suspicious_files": [{ 234 | "actions": "2", 235 | "file_type": "executable" 236 | }], 237 | "threat_files": [{ 238 | "actions": "2", 239 | "file_type": "executable" 240 | }] 241 | }, 242 | "logpolicy": { 243 | "log_upload": "0", 244 | "maxlogsize": "100", 245 | "retentiondays": "30" 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /examples/MTC_tenants_loop.py: -------------------------------------------------------------------------------- 1 | # Simple example to read creds file, connect to MTC API, get access to each managed 2 | # tenant and list a count of devices w/o Optics 3 | # A change in comments will list each Venue Tenant ID, Device Name and Device ID 4 | 5 | ################################################################################## 6 | # USAGE 7 | # 8 | ################################################################################## 9 | from __future__ import print_function 10 | import json 11 | from pprint import pprint 12 | from cyapi.cyapi import CyAPI, debug_level 13 | import argparse 14 | import time 15 | 16 | __VERSION__ = '1.0' 17 | 18 | ################################################################################## 19 | # Arguments 20 | # 21 | ################################################################################## 22 | def ParseArgs(): 23 | 24 | regions = [] 25 | regions_help = "Region the tenant is located: " 26 | for (k, v) in CyAPI.regions.items(): 27 | regions.append(k) 28 | regions_help += " {} - {} ".format(k,v['fullname']) 29 | 30 | parser = argparse.ArgumentParser(description='Simple example to build from', add_help=True) 31 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="debug_level", 32 | help='Show process location, comments and api responses') 33 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 34 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 35 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 36 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with API info provided') 37 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 38 | parser.add_argument('-m', '--mtc', dest='mtc', help='Indicates API connection via MTC', default=False, action='store_true') 39 | 40 | return parser 41 | 42 | commandline = ParseArgs() 43 | args = commandline.parse_args() 44 | 45 | if args.debug_level: 46 | debug_level = args.debug_level 47 | 48 | if args.creds: 49 | with open(args.creds, 'rb') as f: 50 | creds = json.loads(f.read()) 51 | 52 | if not creds.get('region'): 53 | creds['region'] = args.region 54 | 55 | if not creds.get('mtc'): 56 | creds['mtc'] = args.mtc 57 | 58 | API = CyAPI(**creds) 59 | 60 | elif args.tid_val and args.app_id and args.app_secret: 61 | tid_val = args.tid_val 62 | app_id = args.app_id 63 | app_secret = args.app_secret 64 | API = CyAPI(tid_val,app_id,app_secret,args.region,args.mtc) 65 | 66 | else: 67 | print("[-] Must provide valid token information") 68 | exit(-1) 69 | 70 | """ Optional Health Check that the server is up and running 71 | This is a non-authenticated health-check, but returns a 72 | CYApi APIResonse Object 73 | """ 74 | 75 | conn_health = API.get_mtc_health_check() 76 | if conn_health.is_success: 77 | print(conn_health.data) 78 | print("The MTC API Connection is ready!\n") 79 | else: 80 | print("MTC API Connection failed health-check.\n\nStatus Code:{}\n{} Exiting..".format(conn_health.status_code, 81 | conn_health.errors)) 82 | exit() 83 | 84 | 85 | 86 | 87 | API.create_conn() 88 | 89 | tenant_list = [] 90 | tenants = API.get_tenants() 91 | 92 | print("Collecting Access to {} tenants.".format(len(tenants.data['listData']))) 93 | # Collect the MTC Tenants, for the venueTenantId to call for tenant jwt bearer token. 94 | for t in tenants.data['listData']: 95 | app = API.get_tenant_app(t['venueTenantId']) 96 | t['jwt'] = app.data 97 | tenant_list.append(t) 98 | 99 | print("Starting Tenant Loops") 100 | # Set the tenant_app switch and send in the jwt to create the tenant CyAPI object for access to tenant API. 101 | # Loop each tenant and output the number of Protect Devices for each tenant. 102 | total_no_optics = 0 103 | total_devices = 0 104 | #header = "VenueTenantID,Device,DeviceID" 105 | header = "Tenant, Devices w/o Optics, Total Devices" 106 | print(header) 107 | for t in tenant_list: 108 | tenant_args = {} 109 | tenant_args['region'] = "NA" 110 | tenant_args['tenant_app'] = True 111 | tenant_args['tenant_jwt'] = t['jwt'] 112 | 113 | APITenant = CyAPI(**tenant_args) 114 | APITenant.create_conn() 115 | 116 | # Get Devices Extended and then parse for Devices w/o Optics 117 | resp = APITenant.get_devices_extended() 118 | look_for = "optics" 119 | d_cnt = 0 # Devices counted 120 | no_optics = 0 # Devices w/o Optics 121 | for r in resp.data: 122 | if len(list(filter(lambda x: x.get('name') == look_for, r['products']))) == 0: 123 | no_optics = no_optics + 1 124 | #print("{},{},{}".format(t.get('venueTenantId'),r.get('name'),r.get('id'))) 125 | d_cnt = d_cnt + 1 126 | 127 | print("{} : {} of {}".format(t.get('name'),no_optics, d_cnt)) 128 | total_no_optics = total_no_optics + no_optics 129 | total_devices = total_devices + d_cnt 130 | print("{} Tenants with {} devices w/ {} missing Optics.".format(len(tenant_list),total_devices,total_no_optics)) 131 | 132 | -------------------------------------------------------------------------------- /examples/find_stale_devices.py: -------------------------------------------------------------------------------- 1 | # Simple example to read creds file, connect to API, and print stale devices. 2 | # Use the --days argument to specify stale day amount, other than the default 30. 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import json 7 | from datetime import datetime, timedelta 8 | from pprint import pprint 9 | 10 | import pytz 11 | 12 | from cyapi.cyapi import CyAPI, debug_level 13 | 14 | __VERSION__ = '1.0' 15 | 16 | ################################################################################## 17 | # Arguments 18 | # 19 | ################################################################################## 20 | def ParseArgs(): 21 | 22 | regions = [] 23 | regions_help = "Region the tenant is located: " 24 | for (k, v) in CyAPI.regions.items(): 25 | regions.append(k) 26 | regions_help += " {} - {} ".format(k,v['fullname']) 27 | 28 | parser = argparse.ArgumentParser(description='Find all devices that have been offline for XX days (default: 30)', add_help=True) 29 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="debug_level", 30 | help='Show process location, comments and api responses') 31 | # Cylance SE Tenant 32 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 33 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 34 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 35 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with API info provided') 36 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 37 | parser.add_argument('-d', '--days', dest='days', default=30, help='Number of days to check') 38 | 39 | return parser 40 | 41 | ################################################################################## 42 | # Tenant Integration 43 | # Modify the keys to align with your tenant API 44 | ################################################################################## 45 | 46 | commandline = ParseArgs() 47 | args = commandline.parse_args() 48 | 49 | if args.debug_level: 50 | debug_level = args.debug_level 51 | 52 | if args.creds: 53 | with open(args.creds, 'rb') as f: 54 | creds = json.loads(f.read()) 55 | 56 | if not creds.get('region'): 57 | creds['region'] = args.region 58 | 59 | API = CyAPI(**creds) 60 | 61 | elif args.tid_val and args.app_id and args.app_secret: 62 | tid_val = args.tid_val 63 | app_id = args.app_id 64 | app_secret = args.app_secret 65 | API = CyAPI(tid_val,app_id,app_secret,args.region) 66 | 67 | else: 68 | print("[-] Must provide valid token information") 69 | exit(-1) 70 | 71 | API.create_conn() 72 | devices = API.get_devices() 73 | detailed_devices = [] 74 | 75 | before = datetime.utcnow() - timedelta(days=int(args.days)) 76 | device_ids = [d['id'] for d in devices.data] 77 | bulk_response = API.get_bulk_device(device_ids) 78 | for device in bulk_response.data: 79 | state = device['state'] 80 | date_offline = device['date_offline'] 81 | if state == "Offline" and date_offline: 82 | # Sometimes date_offline can be None 83 | dt = datetime.strptime(date_offline, "%Y-%m-%dT%H:%M:%S.%f") 84 | if dt < before: 85 | detailed_devices.append(device) 86 | 87 | for device in detailed_devices: 88 | print("{} - {} - {}".format(device['host_name'], device['date_offline'], device['state'])) 89 | 90 | print() 91 | print("Found {} Stale Devices".format(len(detailed_devices))) -------------------------------------------------------------------------------- /examples/safelist_trusted_local.py: -------------------------------------------------------------------------------- 1 | # Simple example to read creds file, connect to API, and print Trusted Local 2 | # threats found in your environment. 3 | # Using the --Force argument will add these threats to your Global Safe List 4 | 5 | from __future__ import print_function 6 | import json 7 | from pprint import pprint 8 | from cyapi.cyapi import CyAPI, debug_level 9 | import argparse 10 | 11 | __VERSION__ = '1.0' 12 | 13 | ################################################################################## 14 | # Arguments 15 | # 16 | ################################################################################## 17 | def ParseArgs(): 18 | 19 | regions = [] 20 | regions_help = "Region the tenant is located: " 21 | for (k, v) in CyAPI.regions.items(): 22 | regions.append(k) 23 | regions_help += " {} - {} ".format(k,v['fullname']) 24 | 25 | parser = argparse.ArgumentParser(description='Safelist all Trusted Local detections. See -F flag', add_help=True) 26 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="debug_level", 27 | help='Show process location, comments and api responses') 28 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 29 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 30 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 31 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with API info provided') 32 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 33 | parser.add_argument('-F', '--Force', dest='force', help='Perform the Global Safelisting action, otherwise it will just list', action="store_true") 34 | 35 | return parser 36 | 37 | ################################################################################## 38 | # Tenant Integration 39 | # Modify the keys to align with your tenant API 40 | ################################################################################## 41 | 42 | commandline = ParseArgs() 43 | args = commandline.parse_args() 44 | 45 | if args.debug_level: 46 | debug_level = args.debug_level 47 | 48 | if args.creds: 49 | with open(args.creds, 'rb') as f: 50 | creds = json.loads(f.read()) 51 | 52 | if not creds.get('region'): 53 | creds['region'] = args.region 54 | 55 | API = CyAPI(**creds) 56 | 57 | elif args.tid_val and args.app_id and args.app_secret: 58 | tid_val = args.tid_val 59 | app_id = args.app_id 60 | app_secret = args.app_secret 61 | API = CyAPI(tid_val,app_id,app_secret,args.region) 62 | 63 | else: 64 | print("[-] Must provide valid token information") 65 | exit(-1) 66 | 67 | if not args.force: 68 | print("[+] Listing all Trusted Local threats in your environment") 69 | else: 70 | print("[+] Globally Safelisting all Trusted Local threats in your environment") 71 | 72 | API.create_conn() 73 | threats = API.get_threats() 74 | tl_threats = [] 75 | for threat in threats.data: 76 | if "Trusted" == threat.get('classification') and "Local" == threat.get('sub_classification'): 77 | tl_threats.append(threat) 78 | 79 | for threat in tl_threats: 80 | if args.force: 81 | API.add_to_global_list("safe", "Trusted Local", threat.get('sha256')) 82 | else: 83 | pprint(threat) -------------------------------------------------------------------------------- /examples/simple_MTC_setup.py: -------------------------------------------------------------------------------- 1 | # Simple example to read creds file, connect to the MTC (Multi Tenant Console) API, 2 | # and print tenant list. 3 | 4 | ################################################################################## 5 | # USAGE 6 | # 7 | ################################################################################## 8 | from __future__ import print_function 9 | import json 10 | from pprint import pprint 11 | from cyapi.cyapi import CyAPI, debug_level 12 | import argparse 13 | 14 | __VERSION__ = '1.0' 15 | 16 | ################################################################################## 17 | # Arguments 18 | # 19 | ################################################################################## 20 | def ParseArgs(): 21 | 22 | regions = [] 23 | regions_help = "Region the tenant is located: " 24 | for (k, v) in CyAPI.regions.items(): 25 | regions.append(k) 26 | regions_help += " {} - {} ".format(k,v['fullname']) 27 | 28 | parser = argparse.ArgumentParser(description='Simple example to build from', add_help=True) 29 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="debug_level", 30 | help='Show process location, comments and api responses') 31 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 32 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 33 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 34 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with API info provided') 35 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 36 | parser.add_argument('-m', '--mtc', dest='mtc', help='Indicates API connection via MTC', default=False, action='store_true') 37 | 38 | return parser 39 | 40 | commandline = ParseArgs() 41 | args = commandline.parse_args() 42 | 43 | if args.debug_level: 44 | debug_level = args.debug_level 45 | 46 | if args.creds: 47 | with open(args.creds, 'rb') as f: 48 | creds = json.loads(f.read()) 49 | 50 | if not creds.get('region'): 51 | creds['region'] = args.region 52 | 53 | if not creds.get('mtc'): 54 | creds['mtc'] = args.mtc 55 | 56 | API = CyAPI(**creds) 57 | 58 | elif args.tid_val and args.app_id and args.app_secret: 59 | tid_val = args.tid_val 60 | app_id = args.app_id 61 | app_secret = args.app_secret 62 | API = CyAPI(tid_val,app_id,app_secret,args.region,args.mtc) 63 | 64 | else: 65 | print("[-] Must provide valid token information") 66 | exit(-1) 67 | 68 | API.create_conn() 69 | 70 | cnt = 0 71 | header = "Count,ID,Name,Created\n" 72 | 73 | tenants = API.get_tenants() 74 | 75 | print(header) 76 | for t in tenants.data['listData']: 77 | cnt = cnt+1 78 | print("{},{},{},{}".format(cnt,t['id'],t['name'],t['createdDateTime'])) 79 | #print(t) 80 | -------------------------------------------------------------------------------- /examples/simple_setup.py: -------------------------------------------------------------------------------- 1 | # Simple example to read creds file, connect to API, and devices w/0 zones. 2 | 3 | ################################################################################## 4 | # USAGE 5 | # 6 | ################################################################################## 7 | from __future__ import print_function 8 | import json 9 | from pprint import pprint 10 | from cyapi.cyapi import CyAPI, debug_level 11 | import argparse 12 | 13 | __VERSION__ = '1.0' 14 | 15 | ################################################################################## 16 | # Arguments 17 | # 18 | ################################################################################## 19 | def ParseArgs(): 20 | 21 | regions = [] 22 | regions_help = "Region the tenant is located: " 23 | for (k, v) in CyAPI.regions.items(): 24 | regions.append(k) 25 | regions_help += " {} - {} ".format(k,v['fullname']) 26 | 27 | parser = argparse.ArgumentParser(description='Simple example to build from', add_help=True) 28 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="debug_level", 29 | help='Show process location, comments and api responses') 30 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 31 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 32 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 33 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with API info provided') 34 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 35 | 36 | return parser 37 | 38 | commandline = ParseArgs() 39 | args = commandline.parse_args() 40 | 41 | if args.debug_level: 42 | debug_level = args.debug_level 43 | 44 | if args.creds: 45 | with open(args.creds, 'rb') as f: 46 | creds = json.loads(f.read()) 47 | 48 | if not creds.get('region'): 49 | creds['region'] = args.region 50 | 51 | API = CyAPI(**creds) 52 | 53 | elif args.tid_val and args.app_id and args.app_secret: 54 | tid_val = args.tid_val 55 | app_id = args.app_id 56 | app_secret = args.app_secret 57 | API = CyAPI(tid_val,app_id,app_secret,args.region) 58 | 59 | else: 60 | print("[-] Must provide valid token information") 61 | exit(-1) 62 | 63 | API.create_conn() 64 | 65 | cnt = 0 66 | devices = API.get_devices() 67 | for d in devices.data: 68 | zones = API.get_device_zones(d.get("id")) 69 | if not zones.data: 70 | cnt = cnt + 1 71 | print(f"DEVICE: {d.get('name')}") 72 | print("No Zone Attached") 73 | 74 | if cnt == 0: 75 | print("All devices have zones attached. Terrific!") -------------------------------------------------------------------------------- /examples/time_getting_all_detection_detail.py: -------------------------------------------------------------------------------- 1 | # Simple example to read creds file, connect to API, and print detections. 2 | 3 | ################################################################################## 4 | # USAGE 5 | # 6 | ################################################################################## 7 | from __future__ import print_function 8 | import json 9 | from pprint import pprint 10 | from cyapi.cyapi import CyAPI, debug_level 11 | import argparse 12 | 13 | __VERSION__ = '1.0' 14 | 15 | ################################################################################## 16 | # Arguments 17 | # 18 | ################################################################################## 19 | def ParseArgs(): 20 | 21 | regions = [] 22 | regions_help = "Region the tenant is located: " 23 | for (k, v) in CyAPI.regions.items(): 24 | regions.append(k) 25 | regions_help += " {} - {} ".format(k,v['fullname']) 26 | 27 | parser = argparse.ArgumentParser(description='Get Detection Detail for all Detections', add_help=True) 28 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="debug_level", 29 | help='Show process location, comments and api responses') 30 | # Cylance SE Tenant 31 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 32 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 33 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 34 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with API info provided') 35 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 36 | 37 | return parser 38 | 39 | ################################################################################## 40 | # Tenant Integration 41 | # Modify the keys to align with your tenant API 42 | ################################################################################## 43 | 44 | commandline = ParseArgs() 45 | args = commandline.parse_args() 46 | 47 | if args.debug_level: 48 | debug_level = args.debug_level 49 | 50 | if args.creds: 51 | with open(args.creds, 'rb') as f: 52 | creds = json.loads(f.read()) 53 | 54 | if not creds.get('region'): 55 | creds['region'] = args.region 56 | 57 | API = CyAPI(**creds) 58 | 59 | elif args.tid_val and args.app_id and args.app_secret: 60 | tid_val = args.tid_val 61 | app_id = args.app_id 62 | app_secret = args.app_secret 63 | API = CyAPI(tid_val,app_id,app_secret,args.region) 64 | 65 | else: 66 | print("[-] Must provide valid token information") 67 | exit(-1) 68 | 69 | print("Getting Detections") 70 | API.create_conn() 71 | detections = API.get_detections() 72 | 73 | ids = [] 74 | print("You have {} IDs. This might take ~{} minutes to collect the details.".format(len(detections.data),int(len(detections.data)/(5000/12)))) 75 | for d in detections.data: 76 | try: 77 | ids.append(d['Id']) 78 | except: 79 | pprint(d) 80 | 81 | from datetime import datetime 82 | startTime = datetime.now() 83 | 84 | # This is a non-paralellized way of doing it 85 | # detection_detail = [] 86 | # for d in detections.data: 87 | # data = API.get_detection(d['Id']).data 88 | # detection_detail.append(data) 89 | detection_detail = API.get_bulk_detection(ids) 90 | 91 | print("Number of detections retrieved: {}".format(len(detection_detail.data))) 92 | print("Time to execute: {}".format(datetime.now() - startTime)) -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Python package for providing Mozilla's CA Bundle." 4 | name = "certifi" 5 | optional = false 6 | python-versions = "*" 7 | version = "2019.9.11" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "Universal encoding detector for Python 2 and 3" 12 | name = "chardet" 13 | optional = false 14 | python-versions = "*" 15 | version = "3.0.4" 16 | 17 | [[package]] 18 | category = "main" 19 | description = "Backport of the concurrent.futures package from Python 3" 20 | marker = "python_version == \"2.7\"" 21 | name = "futures" 22 | optional = false 23 | python-versions = ">=2.6, <3" 24 | version = "3.3.0" 25 | 26 | [[package]] 27 | category = "main" 28 | description = "Internationalized Domain Names in Applications (IDNA)" 29 | name = "idna" 30 | optional = false 31 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 32 | version = "2.8" 33 | 34 | [[package]] 35 | category = "main" 36 | description = "JSON Web Token implementation in Python" 37 | name = "pyjwt" 38 | optional = false 39 | python-versions = "*" 40 | version = "1.7.1" 41 | 42 | [[package]] 43 | category = "main" 44 | description = "Extensions to the standard Python datetime module" 45 | name = "python-dateutil" 46 | optional = false 47 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 48 | version = "2.8.1" 49 | 50 | [package.dependencies] 51 | six = ">=1.5" 52 | 53 | [[package]] 54 | category = "main" 55 | description = "World timezone definitions, modern and historical" 56 | name = "pytz" 57 | optional = false 58 | python-versions = "*" 59 | version = "2019.3" 60 | 61 | [[package]] 62 | category = "main" 63 | description = "Python HTTP for Humans." 64 | name = "requests" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 67 | version = "2.21.0" 68 | 69 | [package.dependencies] 70 | certifi = ">=2017.4.17" 71 | chardet = ">=3.0.2,<3.1.0" 72 | idna = ">=2.5,<2.9" 73 | urllib3 = ">=1.21.1,<1.25" 74 | 75 | [[package]] 76 | category = "main" 77 | description = "Python 2 and 3 compatibility utilities" 78 | name = "six" 79 | optional = false 80 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 81 | version = "1.13.0" 82 | 83 | [[package]] 84 | category = "main" 85 | description = "Fast, Extensible Progress Meter" 86 | name = "tqdm" 87 | optional = false 88 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 89 | version = "4.39.0" 90 | 91 | [[package]] 92 | category = "main" 93 | description = "HTTP library with thread-safe connection pooling, file post, and more." 94 | name = "urllib3" 95 | optional = false 96 | python-versions = "*" 97 | version = "1.22" 98 | 99 | [metadata] 100 | content-hash = "21352542819a4f84dbc6ea913e567dbe3035380d208bb43e02f0f04519a74f56" 101 | python-versions = "*" 102 | 103 | [metadata.hashes] 104 | certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"] 105 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 106 | futures = ["49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16", "7e033af76a5e35f58e56da7a91e687706faf4e7bdfb2cbc3f2cca6b9bcda9794"] 107 | idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] 108 | pyjwt = ["5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", "8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"] 109 | python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] 110 | pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"] 111 | requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"] 112 | six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] 113 | tqdm = ["5a1f3d58f3eb53264387394387fe23df469d2a3fab98c9e7f99d5c146c119873", "f1a1613fee07cc30a253051617f2a219a785c58877f9f6bfa129446cbaf8b4c1"] 114 | urllib3 = ["06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", "cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"] 115 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "cyapi" 3 | version = "1.0.20" 4 | description = "Python bindings for Cylance Console and MTC" 5 | 6 | license = "MIT" 7 | readme = "README.md" 8 | authors = ["Shane Shellenbarger ", "Joe Delsol "] 9 | 10 | repository = "https://github.com/cylance/python-cyapi" 11 | 12 | [tool.poetry.dependencies] 13 | python = "*" 14 | futures = {python = "==2.7", version = "*"} 15 | pyjwt = "*" 16 | python-dateutil = "*" 17 | pytz = "*" 18 | requests = "*" 19 | tqdm = "*" 20 | 21 | 22 | [build-system] 23 | requires = ["poetry>=0.12"] 24 | build-backend = "poetry.masonry.api" 25 | -------------------------------------------------------------------------------- /tests/test_cyapi.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import argparse 3 | from pprint import pprint 4 | import json 5 | import sys 6 | import logging 7 | from cyapi.cyapi import CyAPI 8 | 9 | class CyAPITest(unittest.TestCase): 10 | tid = None 11 | app_id = None 12 | app_secret = None 13 | region = "NA" 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | cls.API = CyAPI(cls.tid, cls.app_id, cls.app_secret, cls.region) 18 | cls.log = logging.getLogger("TestLog") 19 | 20 | def test_conn(self): 21 | self.API.create_conn() 22 | assert(self.API.jwt is not None) 23 | 24 | ### unittest Utility Functions ### 25 | def does_policy_name_exist(self, name): 26 | policies = self.API.get_policies() 27 | for p in policies.data: 28 | if name == p['name']: 29 | return True 30 | return False 31 | 32 | def get_policy_by_name(self, name): 33 | policies = self.API.get_policies() 34 | for p in policies.data: 35 | if name == p['name']: 36 | return p 37 | return False 38 | 39 | def get_user_by_email(self, email): 40 | users = self.API.get_users() 41 | for p in users.data: 42 | if email == p['email']: 43 | self.log.debug(" [+] Found existing User") 44 | return p 45 | return False 46 | 47 | def clean_test_user(self, email): 48 | user = self.get_user_by_email(email) 49 | if not user: 50 | return True 51 | else: 52 | response = self.API.delete_user(user['id']) 53 | if response.is_success: 54 | return True 55 | else: 56 | return False 57 | 58 | ### User API ### 59 | 60 | # TODO: Fix this test 61 | # def test_create_user(self): 62 | # self.log.debug(' [+] test_create_user') 63 | # email = 'testuser@email.com' 64 | # if not self.clean_test_user(email): 65 | # self.log.debug(' [+] create_user clean existing user failed') 66 | # else: 67 | # self.log.debug(' [+] create_user clean existing user success') 68 | # response = self.API.create_user(email, 'Test', 'User') 69 | # self.log.debug(response.errors) 70 | # self.log.debug(response.data) 71 | # assert(response.is_success) 72 | # self.clean_test_user(email) 73 | 74 | def test_get_users(self): 75 | response = self.API.get_users() 76 | assert( isinstance(response.data, list) ) 77 | 78 | def test_get_user(self): 79 | response = self.API.get_users() 80 | user = self.API.get_user(response.data[0]["id"]) 81 | assert(isinstance(user.data, dict)) 82 | 83 | # TODO: Fix this test 84 | # def test_update_user(self): 85 | # #TODO: No idea why this is failing validation. 86 | # email = 'testuser@email.com' 87 | # response = self.API.create_user(email, 'Test', 'User') 88 | # user = self.get_user_by_email(email) 89 | # user_id = user['id'] 90 | # updated_user = { 91 | # 'email' : 'updateduser@email.com', 92 | # 'user_role' : '00000000-0000-0000-0000-000000000002', 93 | # 'first_name' : 'Updated', 94 | # 'last_name' : 'User', 95 | # 'zones' : [] 96 | # } 97 | # pprint(user) 98 | # print() 99 | # pprint(updated_user) 100 | # response = self.API.update_user(user_id, user) 101 | # print(response.status_code) 102 | # pprint(response.errors) 103 | # pprint(response.data) 104 | # assert(response.is_success) 105 | # #self.clean_test_user(email) 106 | 107 | # TODO: Fix this test 108 | # def test_delete_user(self): 109 | # self.log.debug(' [+] test_delete_user') 110 | # email = 'testuser@email.com' 111 | # user = self.get_user_by_email(email) 112 | # if not user: 113 | # response = self.API.create_user(email, 'Test', 'User') 114 | # if response.is_success: 115 | # user = self.get_user_by_email(email) 116 | # else: 117 | # self.log.debug(' [+] Failed to find created user by email') 118 | # response = self.API.delete_user(user['id']) 119 | # assert(response.is_success) 120 | 121 | def test_send_invite_email(self): 122 | email = 'testuser@email.com' 123 | response = self.API.create_user(email, 'Test', 'User') 124 | if response.is_success: 125 | sent = self.API.send_invite_email(response.data["email"]) 126 | assert(sent.is_success) 127 | else: 128 | self.log.debug(response.status_code) 129 | self.clean_test_user(email) 130 | 131 | def test_send_request_password_email(self): 132 | email = 'testuser@email.com' 133 | response = self.API.create_user(email, 'Test', 'User') 134 | if response.is_success: 135 | sent = self.API.send_invite_email(response.data["email"]) 136 | assert(sent.is_success) 137 | else: 138 | self.log.debug(response.status_code) 139 | self.clean_test_user(email) 140 | 141 | ### Devices API ### 142 | def test_get_devices(self): 143 | response = self.API.get_devices() 144 | assert( response.is_success ) 145 | 146 | def test_get_device(self): 147 | response = self.API.get_devices() 148 | dev = response.data[0] 149 | dev_detail = self.API.get_device(dev["id"]) 150 | assert( isinstance(dev_detail.data, dict) ) 151 | 152 | def test_get_device_by_mac(self): 153 | # short for test_get_device_by_mac_address 154 | response = self.API.get_devices() 155 | devs = self.API.get_device_by_mac(response.data[0]["mac_addresses"][0]) 156 | assert(len(devs.data) > 0) 157 | 158 | @unittest.skip('Test Not Implemented') 159 | def test_update_device(self): 160 | raise NotImplementedError 161 | 162 | @unittest.skip('Test Not Implemented') 163 | def test_get_device_threat(self): 164 | raise NotImplementedError 165 | 166 | @unittest.skip('Test Not Implemented') 167 | def test_update_device_threat(self): 168 | raise NotImplementedError 169 | 170 | def test_get_zone_devices(self): 171 | pass 172 | # These responses that are only lists need a better test, but when they are empty due to tenant 173 | # then what? This works, just not sure when it is a false possative. 174 | #zoneId = "Create Method to pull valid ZoneId" 175 | """ response = self.API.get_zones() 176 | self.log.debug("Validate devices in zone: {}".format(response[0]["name"])) 177 | #assert(len(self.API.get_zone_devices(response[0]["id"])) > 0) 178 | response = self.API.get_zone_devices(zoneId) 179 | assert(response.status_code == 200)""" 180 | 181 | @unittest.skip('Test Not Implemented') 182 | def test_get_agent_installer_link(self): 183 | raise NotImplementedError 184 | 185 | @unittest.skip('Test Not Implemented') 186 | def test_delete_devices(self): 187 | raise NotImplementedError 188 | 189 | ### Global List API ### 190 | def test_get_global_list(self): 191 | lists = ["quarantine", "safe"] 192 | for l in lists: 193 | response = self.API.get_global_list( l ) 194 | assert( isinstance(response.data, list) ) 195 | #assert(response.status_code == 200) 196 | 197 | @unittest.skip('Test Not Implemented') 198 | def test_add_to_global_list(self): 199 | raise NotImplementedError 200 | 201 | @unittest.skip('Test Not Implemented') 202 | def test_delete_from_global_list(self): 203 | raise NotImplementedError 204 | 205 | ### Zone API ### 206 | def test_get_zones(self): 207 | response = self.API.get_zones() 208 | assert( response.is_success ) 209 | 210 | @unittest.skip('Test Not Implemented') 211 | def test_delete_zones(self): 212 | pass 213 | 214 | ### Policy API ### 215 | # create policy, configure policy, apply policy, validate that it works 216 | def test_create_policy(self, name="PyUnitTest - Create Policy"): 217 | # We need to 'undo' everything we do during testing. This will remove/delete the policy we create 218 | self.addCleanup(self.delete_policy_by_name, name) 219 | policyExists = self.get_policy_by_name(name) 220 | if type(policyExists) == bool: 221 | response = self.API.create_device_policy(name) 222 | self.log.debug(response.status_code) 223 | assert(response.status_code == 201) 224 | else: 225 | self.log.debug("Deleting test policy: {}".format(name)) 226 | response = self.API.delete_policy(policyExists['id']) 227 | self.log.debug(response.status_code) 228 | response = self.API.create_device_policy(name) 229 | self.log.debug(response.status_code) 230 | assert(response.status_code == 201) 231 | 232 | def test_get_policies(self): 233 | # This works because even a new tenant has a policy 234 | response = self.API.get_policies() 235 | assert(len(response.data) > 0) 236 | 237 | def test_delete_policy(self, name="PyUnitTest - Delete Policy"): 238 | policyExists = self.get_policy_by_name(name) 239 | print() 240 | if not policyExists: 241 | print("Creating new Policy") 242 | response = self.API.create_device_policy(name) 243 | response = response.data 244 | else: 245 | print("Got existing policy") 246 | response = policyExists 247 | 248 | if isinstance(response, dict): 249 | name = response.get('name') if response.get('name') else response.get('policy_name') 250 | policy_id = response.get('id') if response.get('id') else response.get('policy_id') 251 | self.log.debug("Deleting test policy: {}".format(name)) 252 | self.log.debug("Test policy ID: {}".format(policy_id)) 253 | 254 | response = self.API.delete_policy(policy_id) 255 | """ {'date_added': '2019-10-31T17:27:23.6470751', 256 | 'date_modified': '2019-10-31T17:27:23.6470751', 257 | 'device_count': 0, 258 | 'id': '6daf8a99-d47e-47f9-b269-8c6431f1aa6d', 259 | 'name': 'PyUnitTest - Delete Policy', 260 | 'zone_count': 0} """ 261 | # pprint(response.data) 262 | assert(response.is_success) 263 | 264 | """ def test_policy_configuration(self): 265 | name = "PyUnitTest - Policy" 266 | self.addCleanup(self.API.delete_policy, name) """ 267 | 268 | def test_policy(self): 269 | name = "PyUnitTest - Test Policy" 270 | 271 | # We need to 'undo' everythign we do during testing. This will remove/delete the policy we create 272 | self.addCleanup(self.delete_policy_by_name, name) 273 | policyExists = self.get_policy_by_name(name) 274 | if type(policyExists) == bool: 275 | response = self.API.create_device_policy(name) 276 | else: 277 | response = policyExists 278 | #response = self.API.create_device_policy(name) 279 | self.log.debug(response.status_code) 280 | if isinstance(response.data, list): 281 | assert( isinstance(response.data, list) ) 282 | else: 283 | assert(response.status_code == 201) 284 | walk_policy = response.data 285 | 286 | walk_policy = self.API.enable_aqt(walk_policy) 287 | walk_policy = self.API.disable_btd(walk_policy) 288 | walk_policy = self.API.enable_optics(walk_policy) 289 | walk_policy = self.API.set_memdef(True, walk_policy, "Alert") 290 | walk_policy = self.API.set_script_control(True, walk_policy) 291 | 292 | self.log.debug("[+] Updating Policy: {}".format(name)) 293 | response = self.API.update_policy(walk_policy) 294 | # Test to make sure the update succeeded 295 | assert(response.status_code == 204) 296 | policy = self.API.get_policy(walk_policy['policy_id']) 297 | 298 | item = self.API.get_policy_item('auto_blocking',policy.data) 299 | self.assertEqual(item['value'], '1') 300 | 301 | # Utility Functions 302 | 303 | @unittest.skip('Test Not Implemented') 304 | def exc_choices(self): 305 | raise NotImplementedError 306 | 307 | @unittest.skip('Test Not Implemented') 308 | def regions(self): 309 | raise NotImplementedError 310 | 311 | 312 | def delete_policy_by_name(self, name): 313 | policyExists = self.get_policy_by_name(name) 314 | if type(policyExists) == bool: 315 | response = self.API.create_device_policy(name) 316 | response = response.data 317 | else: 318 | response = policyExists 319 | #item = response.data 320 | #pprint(response) 321 | self.log.debug("Deleting policy by name: {}".format(response['name'])) 322 | self.log.debug("Test policy ID: {}".format(response['id'])) 323 | response = self.API.delete_policy(response['id']) 324 | if response.status_code == 204: 325 | return True 326 | else: 327 | return False 328 | 329 | 330 | ### Missing Tests by API ### 331 | 332 | ### User API ### 333 | 334 | ### Device API ### 335 | 336 | ### Global List API ### 337 | 338 | ### Policy API ### 339 | 340 | @unittest.skip('Test Not Implemented') 341 | def test_delete_policies(self): 342 | pass 343 | 344 | 345 | ### Zone API ### 346 | @unittest.skip('Test Not Implemented') 347 | def test_get_device_zones(self): 348 | pass 349 | 350 | @unittest.skip('Test Not Implemented') 351 | def test_update_zone(self): 352 | pass 353 | 354 | 355 | ### Threat API ### 356 | def test_get_threats(self): 357 | 358 | threats = self.API.get_threats() 359 | assert(isinstance(threats.data, list)) 360 | 361 | def test_get_threat(self): 362 | threats = self.API.get_threats() 363 | threat = self.API.get_threat(threats.data[0]['sha256']) 364 | assert(threats.data[0]['sha256'] == threat.data['sha256']) 365 | 366 | def test_get_threat_devices(self): 367 | threats = self.API.get_threats() 368 | threat_devices = self.API.get_threat_devices(threats.data[0]['sha256']) 369 | assert( len(threat_devices.data) > 0 ) 370 | 371 | @unittest.skip('Test Not Implemented') 372 | def test_get_threat_download_url(self): 373 | pass 374 | 375 | 376 | ### Detection API ### 377 | def test_get_detections(self): 378 | ## Add tests to test optional query string parameters 379 | response = self.API.get_detections() 380 | assert( isinstance(response.data, list) ) 381 | 382 | response = self.API.get_detections(csv=True) 383 | assert( isinstance(response.data, str) ) 384 | 385 | 386 | @unittest.skip('Test Not Implemented') 387 | def test_get_recent_detections(self): 388 | # Need recent date function that outputs UTC 389 | response = self.API.get_recent_detections(since='00000Z') 390 | assert( isinstance(response, list) ) 391 | #assert(response.status_code == 200) 392 | 393 | @unittest.skip('Test Not Implemented') 394 | def test_get_detections_by_severity(self): 395 | #/severity?start={detection_start_timestamp}&end{detection_end_ timestamp}&interval={detection_interval} 396 | # Needs UTC Dates and maybe an interval List 397 | pass 398 | 399 | @unittest.skip('Test Not Implemented') 400 | def test_update_detection(self): 401 | pass 402 | 403 | @unittest.skip('Test Not Implemented') 404 | def test_delete_detection(self): 405 | pass 406 | 407 | @unittest.skip('Test Not Implemented') 408 | def test_delete_detections(self): 409 | pass 410 | 411 | 412 | ### Package Deployment API ### 413 | @unittest.skip('Test Not Implemented') 414 | def test_create_package(self): 415 | pass 416 | 417 | def test_get_packages(self): 418 | response = self.API.get_packages() 419 | assert( isinstance(response.data, list) ) 420 | #assert(response.status_code == 200) 421 | 422 | @unittest.skip('Test Not Implemented') 423 | def test_delete_package(self): 424 | pass 425 | 426 | @unittest.skip('Test Not Implemented') 427 | def test_create_package_execution(self): 428 | pass 429 | 430 | @unittest.skip('Test Not Implemented') 431 | def test_get_package_executions(self): 432 | pass 433 | 434 | @unittest.skip('Test Not Implemented') 435 | def test_delete_package_execution(self): 436 | pass 437 | 438 | 439 | ### Detection Rule API ### 440 | def test_get_detection_rules(self): 441 | response = self.API.get_detection_rules() 442 | assert( isinstance(response.data, list) ) 443 | #assert(response.status_code == 200) 444 | response = self.API.get_detection_rules(True) 445 | #assert( isinstance(response.data, dict) ) 446 | assert(response.status_code == 200) 447 | 448 | def test_get_detection_rule(self): 449 | ruleId = 'MAKE PROCESS TO GET ONE' 450 | response = self.API.get_detection_rule(ruleId) 451 | 452 | # assert( isinstance(response.data, dict) ) 453 | # #assert(response.status_code == 200) 454 | # response = self.API.get_detection_rule(ruleId, natlang=True) 455 | # assert( isinstance(response.data, list) ) 456 | # #assert(response.status_code == 200) 457 | 458 | """ #Sent as Argument to get_detection_rule 459 | def test_get_detection_rule_csv(self): 460 | response = self.API.get_detection_rule_csv() 461 | assert(response.status_code == 200) """ 462 | 463 | @unittest.skip('Test Not Implemented') 464 | def test_validate_detection_rule(self): 465 | pass 466 | 467 | @unittest.skip('Test Not Implemented') 468 | def test_create_detection_rule(self): 469 | pass 470 | 471 | @unittest.skip('Test Not Implemented') 472 | def test_update_detection_rule(self): 473 | pass 474 | 475 | @unittest.skip('Test Not Implemented') 476 | def test_deactivate_or_delete_detection_rule(self): 477 | pass 478 | 479 | @unittest.skip('Test Not Implemented') 480 | def test_get_detection_rule_counts(self): 481 | pass 482 | 483 | ### Detection Rule Sets ### 484 | @unittest.skip('Test Not Implemented') 485 | def test_get_detection_rule_set(self): 486 | pass 487 | 488 | @unittest.skip('Test Not Implemented') 489 | def test_create_detection_rule_set(self): 490 | pass 491 | 492 | @unittest.skip('Test Not Implemented') 493 | def test_retrieve_default_detection_rule_set_update_detection_rule_set(self): 494 | pass 495 | 496 | @unittest.skip('Test Not Implemented') 497 | def test_delete_detection_rule_set(self): 498 | pass 499 | 500 | @unittest.skip('Test Not Implemented') 501 | def test_delete_multiple_detection_rule_sets(self): 502 | pass 503 | 504 | ### Detection Exceptions ### 505 | def test_get_detection_exceptions(self): 506 | response = self.API.get_detection_exceptions() 507 | # pprint(response.data) 508 | assert( isinstance(response.data, list) ) 509 | #assert(response.status_code == 200) 510 | response = self.API.get_detection_exceptions(True) 511 | #assert( isinstance(response.data, list) ) 512 | assert(response.status_code == 200) 513 | 514 | @unittest.skip('Test Not Implemented') 515 | def test_get_detection_exception_content(self): 516 | pass 517 | 518 | @unittest.skip('Test Not Implemented') 519 | def test_create_detection_exception(self): 520 | pass 521 | 522 | @unittest.skip('Test Not Implemented') 523 | def test_update_detection_exception(self): 524 | pass 525 | 526 | @unittest.skip('Test Not Implemented') 527 | def test_deactivate_delete_detection_exception(self): 528 | pass 529 | 530 | ### Device Commands ### 531 | @unittest.skip('Test Not Implemented') 532 | def test_lockdown_device_command(self): 533 | pass 534 | 535 | @unittest.skip('Test Not Implemented') 536 | def test_get_device_lockdown_history(self): 537 | pass 538 | 539 | def test_get_retrieved_file_results(self): 540 | response = self.API.get_retrieved_file_results() 541 | assert( isinstance(response.data, list) ) 542 | #assert(response.status_code == 200) 543 | 544 | @unittest.skip('Test Not Implemented') 545 | def test_request_file_retrieval_from_device(self): 546 | pass 547 | 548 | @unittest.skip('Test Not Implemented') 549 | def test_check_file_retrieval_status_from_device(self): 550 | pass 551 | 552 | ### Focus View ### 553 | def test_get_focus_views(self): 554 | response = self.API.get_focus_views() 555 | assert( isinstance(response.data, list) ) 556 | #assert(response.status_code == 200) 557 | 558 | @unittest.skip('Test Not Implemented') 559 | def test_search_for_focus_view_results(self): 560 | pass 561 | 562 | @unittest.skip('Test Not Implemented') 563 | def test_request_a_focus_view(self): 564 | pass 565 | 566 | @unittest.skip('Test Not Implemented') 567 | def test_get_a_focus_view_summary(self): 568 | pass 569 | 570 | @unittest.skip('Test Not Implemented') 571 | def test_get_focus_view_results(self): 572 | pass 573 | 574 | ### InstaQueries ### 575 | def test_get_instaqueries(self): 576 | response = self.API.get_instaqueries() 577 | assert( isinstance(response.data, list) ) 578 | #assert(response.status_code == 200) 579 | 580 | @unittest.skip('Test Not Implemented') 581 | def test_create_instaquery(self): 582 | pass 583 | 584 | @unittest.skip('Test Not Implemented') 585 | def test_get_instaquery(self): 586 | pass 587 | 588 | @unittest.skip('Test Not Implemented') 589 | def test_get_instaquery_results(self): 590 | pass 591 | 592 | @unittest.skip('Test Not Implemented') 593 | def test_archive_instaquery(self): 594 | pass 595 | 596 | ### cylanceOPTICS Policy ### 597 | def test_get_rule_sets_to_policy_mapping(self): 598 | response = self.API.get_rule_sets_to_policy_mapping() 599 | assert( isinstance(response.data, list) ) 600 | #assert(response.status_code == 200) 601 | 602 | @unittest.skip('Test Not Implemented') 603 | def test_get_detection_rule_set_for_a_policy(self): 604 | pass 605 | 606 | @unittest.skip('Test Not Implemented') 607 | def test_update_a_detection_rule_set_in_a_policy(self): 608 | pass 609 | 610 | 611 | 612 | 613 | def ParseArgs(): 614 | 615 | regions = [] 616 | regions_help = "Region the tenant is located: " 617 | for (k, v) in CyAPI.regions.items(): 618 | regions.append(k) 619 | regions_help += " {} - {} ".format(k,v['fullname']) 620 | 621 | parser = argparse.ArgumentParser(description='Create a new OPTICS Rule Set based on an existing on and best practice phases.', add_help=True) 622 | parser.add_argument('-v', '--verbose', action="count", default=0, dest="verbose", 623 | help='Show process location, comments and api responses') 624 | parser.add_argument('-f', '--failfast', dest='failfast', help="Tests should fail immediately", action='store_true', default=False) 625 | # Cylance SE Tenant 626 | parser.add_argument('-tid', '--tid_val', help='Tenant Unique Identifier') 627 | parser.add_argument('-aid', '--app_id', help='Application Unique Identifier') 628 | parser.add_argument('-ase', '--app_secret', help='Application Secret') 629 | parser.add_argument('-c', '--creds_file', dest='creds', help='Path to JSON File with self.API info provided') 630 | parser.add_argument('-r', '--region', dest='region', help=regions_help, choices=regions, default='NA') 631 | 632 | return parser 633 | 634 | if __name__ == '__main__': 635 | 636 | commandline = ParseArgs() 637 | args = commandline.parse_args() 638 | if args.verbose >= 1: 639 | logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) 640 | 641 | if args.creds: 642 | with open(args.creds, 'rb') as f: 643 | creds = json.loads(f.read()) 644 | tid_val = creds['tid'] # The tenant's unique identifier. 645 | app_id = creds['app_id'] # The application's unique identifier. 646 | app_secret = creds['app_secret'] # The application's secret to sign the auth token with. 647 | 648 | elif args.tid_val and args.app_id and args.app_secret: 649 | tid_val = args.tid_val 650 | app_id = args.app_id 651 | app_secret = args.app_secret 652 | 653 | else: 654 | print("[-] Must provide valid token information") 655 | exit(-1) 656 | 657 | CyAPITest.tid = tid_val 658 | CyAPITest.app_id = app_id 659 | CyAPITest.app_secret = app_secret 660 | CyAPITest.region = args.region 661 | 662 | sys.argv = sys.argv[:1] 663 | 664 | unittest.main(failfast=args.failfast) 665 | 666 | --------------------------------------------------------------------------------