├── CSIntel.py ├── README.md ├── __init__.py └── csintel.ini /CSIntel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | try: # python3 5 | from configparser import SafeConfigParser 6 | except Exception: # python2 7 | from ConfigParser import SafeConfigParser 8 | try: # python3 9 | from urllib.parse import urlencode 10 | except Exception: 11 | from urllib import urlencode 12 | from datetime import datetime, timedelta 13 | import os 14 | try: # python3 15 | import urllib.request as urllib 16 | except Exception: # python2 17 | import urllib 18 | 19 | from collections import Counter 20 | 21 | 22 | # Global 23 | CSconfigSection = "CrowdStrikeIntelAPI" 24 | #host = "https://intelapi.crowdstrike.com/indicator/v2/search/" 25 | host = "https://intelapi.crowdstrike.com/" 26 | indicatorPath = "indicator/v2/search/" 27 | malqueryPath = "malquery/" 28 | reportsPath = "reports/" 29 | defaultConfigFileName = os.path.join(os.path.expanduser("~"), ".csintel.ini") 30 | 31 | # setup 32 | __author__ = "Adam Hogan" 33 | __email__ = "adam.hogan@crowdstrike.com" 34 | __version__ = '0.9beta2' 35 | 36 | # I should do more with this.... 37 | # These specs from the API documentation should be used to do more input validation 38 | validType = ['binary_string', 'compile_time', 'device_name', 'domain', 'email_address', 'email_subject', 'event_name', 'file_mapping', 'file_name', 'file_path', 'hash_ion', 'hash_md5', 'hash_sha1', 'hash_sha256', 'ip_address', 'ip_address_block', 'mutex_name', 'password', 'persona_name', 'phone_number', 'port', 'registry', 'semaphore_name', 'service_name', 'url', 'user_agent', 'username', 'x509_serial', 'x509_subject'] 39 | validParameter = ['sort', 'order', 'last_updated', 'perPage', 'page'] 40 | validSearch = ['indicator', 'actor', 'report', 'actor', 'malicious_confidence', 'published_date', 'last_updated', 'malware_family', 'kill_chain', 'domain_type'] 41 | validDomainType = ['Actor Controlled', 'DGA', 'DynamicDNS', 'DynamicDNS/Afraid', 'DynamicDNS/DYN', 'DynamicDNS/Hostinger', 'DynamicDNS/noIP', 'DynamicDNS/Oray', 'KnownGood', 'LegitimateCompromised', 'PhishingDomain', 'Sinkholed', 'StragegicWebCompromise', 'Unregistered'] 42 | validFilter = ['match', 'equal', 'gt', 'gte', 'lt', 'lte'] 43 | validSort = ['indicator', 'type', 'report', 'actor', 'malicious_confidence', 'published_date', 'last_updated'] 44 | 45 | 46 | # local methods 47 | def readConfig(fileName=None): 48 | """this method is usef for initial creation and can be called 49 | before an API object is created. Just pass it the filename 50 | to read an existing config file.""" 51 | 52 | # check file exists 53 | if (os.path.exists(fileName)) is False: 54 | raise Exception("Config file does not exist: " + fileName) 55 | 56 | # read config file 57 | parser = SafeConfigParser() 58 | parser.read(fileName) 59 | 60 | # get var [custid, custkey] 61 | section = CSconfigSection 62 | custid = parser.get(section, "custid") 63 | custkey = parser.get(section, "custkey") 64 | perpage = parser.get(section, "perpage") 65 | 66 | # TODO might need error checking on what is or isn't being pulled 67 | # from the file down the road. 68 | 69 | return (custid, custkey, perpage) 70 | # end readConfig() 71 | 72 | 73 | ###################################################################### 74 | class CSIntelAPI: 75 | """ 76 | Class for interacting with CrowdStrike Intel API 77 | 78 | This class object is used in this program if called directly or can be imported 79 | into another script to use it's methods to search the threat intel API and process 80 | the data returned. 81 | 82 | Check out functions that start with Search* to see different methods of pulling data. 83 | 84 | Check out functions that like Get*FromResults to see different exampels of processing 85 | the JSON data that is returned from an API search. 86 | 87 | To create this object you need to pass your Customer ID and Cutomer Key. If you're 88 | going to be reusing this at all it is much faster to add your ID & Key to a config 89 | file to be read by this script. The default config file is ~/.csintel.ini. 90 | 91 | If you have a config file then creating an API object is easy. 92 | 93 | import CSIntel 94 | (custid, custkey) = CSIntel.readConfig() 95 | api_obj = CSIntel.CSIntelAPI(custid, cutkey) 96 | 97 | Now you can search for data - for eample, data on an adversary we've been tracking. 98 | 99 | results - api_obj.SearchActorEqual("putterpanda") 100 | 101 | And then manipulate the results. 102 | 103 | data = json.loads(result.text) 104 | 105 | Or use some of the built in methods. 106 | 107 | hashes = api_obj.GetHashesFromResults(data) 108 | """ 109 | 110 | def __init__(self, custid=None, custkey=None, perpage=None, page="1", deleted=False, debug=False): 111 | """ 112 | Intit funciton for the CS Intel API object - pass it the API customer ID and 113 | customer key to create it. 114 | Optional: whether the config should be written to disk as a config ini file. 115 | If the config file is being used check out the readConfig() function to grab 116 | those fields before creating this object. 117 | """ 118 | 119 | # customer id and key should be passed when obj is created 120 | self.custid = custid 121 | self.custkey = custkey 122 | 123 | # pull some global settings for object reference 124 | self.configSection = CSconfigSection # config file section title 125 | self.host = host # hostname of where to query API 126 | self.indicatorPath = indicatorPath 127 | self.malqueryPath = malqueryPath 128 | self.reportsPath = reportsPath 129 | self.perpage = perpage 130 | self.page = page 131 | self.deleted = deleted 132 | 133 | # set API valid terms 134 | # should be used more for syntax validation. 135 | self.validType = validType 136 | self.ValidParameter = validParameter 137 | self.validSearch = validSearch 138 | self.validFilter = validFilter 139 | self.validSort = validSort 140 | self.validDomainType = validDomainType 141 | 142 | # debug? 143 | self.debug = debug 144 | 145 | # end init 146 | 147 | def writeConfig(self, fileName=None): 148 | """ 149 | This script supports reading the API config data from a 150 | configuration file instead of passing it as CLI options. 151 | That file can easily be written manually, but if you use 152 | this script it can also write the config file from the options 153 | you have loaded. 154 | """ 155 | 156 | # setup 157 | section = self.configSection # config section to use 158 | parser = SafeConfigParser() # create object from ConfigParser library import 159 | 160 | # create config file data 161 | # create proper section and then add customer id and key as entries. 162 | parser.add_section(section) 163 | parser.set(section, 'custid', self.custid) 164 | parser.set(section, 'custkey', self.custkey) 165 | parser.set(section, 'perpage', self.perPage) 166 | 167 | # write to disk 168 | f = open(fileName, "w") 169 | parser.write(f) 170 | f.close() 171 | # end writeConfig() 172 | 173 | def getHeaders(self): 174 | """ 175 | Need to pass customer id and key as headers. 176 | This will return the correct syntax to do so. These fields need to be 177 | passed as headers and not in the URL request itself. 178 | headers = {'X-CSIX-CUSTID': custid, 'X-CSIX-CUSTKEY': custkey} 179 | """ 180 | headers = {'X-CSIX-CUSTID': self.custid, 'X-CSIX-CUSTKEY': self.custkey} 181 | # this is the format needed by the requests module 182 | return headers 183 | # end getHeaders 184 | 185 | #def request(self, query): 186 | def request(self, query, queryType="indicator"): 187 | """ 188 | This function was intended as an internal method - it just takes the query 189 | you pass it and sends it to the API along with your API ID & Key. If you 190 | know the API or rest real well have at it. 191 | 192 | """ 193 | 194 | if not self.custid or not self.custkey: 195 | raise Exception('Customer ID and Customer Key are required') 196 | 197 | fullQuery = self.host 198 | 199 | #Host + Intel API type (e.g. indicator, malquery) 200 | if queryType == "indicator": 201 | fullQuery += self.indicatorPath 202 | elif queryType == "reports": 203 | fullQuery += self.reportsPath 204 | elif queryType == "malquery": 205 | fullQuery += self.malqueryPath 206 | 207 | #TODO else error checking 208 | 209 | #Specific query 210 | fullQuery += query 211 | 212 | if self.debug: # Show the full query URL in debug 213 | print("fullQuery: " + fullQuery) 214 | 215 | headers = self.getHeaders() # format the API key & ID 216 | 217 | # check for proxy information 218 | proxies = urllib.getproxies() 219 | 220 | # use requests library to pull request 221 | r = requests.get(fullQuery, headers=headers, proxies=proxies) 222 | 223 | # Error handling for HTTP request 224 | 225 | # 400 - bad request 226 | if r.status_code == 400: 227 | raise Exception('HTTP Error 400 - Bad request.') 228 | 229 | # 404 - oh shit 230 | if r.status_code == 404: 231 | raise Exception('HTTP Error 404 - awww snap.') 232 | 233 | # catch all? 234 | if r.status_code != 200: 235 | raise Exception('HTTP Error: ' + str(r.status_code)) 236 | 237 | return r 238 | # end request() 239 | 240 | def getURLParams(self, **kwargs): 241 | """ 242 | This funciton takes a series of keyword arguments and then 243 | encodes them all in a single URL string. 244 | """ 245 | 246 | query = urlencode(kwargs) 247 | return query 248 | 249 | def getActorQuery(self, actor, searchFilter="equal", **kwargs): 250 | """ 251 | A specific API query - provide an actor name to retrieve data 252 | on that actor from the intel API. 253 | First optional argument is "searchFilter" which defaults to 254 | "equal" for an exact match (e.g. Putter Panda) but you can use 255 | searchFilter="match" instead to search for a pattern (e.g. 256 | panda). 257 | Any other keywords passed to the function will be encoded in the 258 | URL request - in case you want to filter or sort, for example. 259 | """ 260 | 261 | valid = ["match", "equal"] 262 | if searchFilter not in valid: 263 | raise Exception("not a valid search filter: " + searchFilter) 264 | 265 | encodedargs = "" 266 | 267 | if any(kwargs): 268 | encodedargs = "&" + self.getURLParams(**kwargs) 269 | 270 | query = "actor?" + searchFilter + "=" + actor + encodedargs 271 | 272 | return query 273 | # end getActorQuery 274 | 275 | def SearchActorEqual(self, actor, **kwargs): 276 | """ 277 | A specific use of getActorQuery() to match a specific actor and 278 | perform the search and return the results found. 279 | Any other keywords passed to the function will be encoded in the 280 | URL request - in case you want to filter or sort, for example. 281 | """ 282 | query = self.getActorQuery(actor, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 283 | result = self.request(query) 284 | return result 285 | 286 | def SearchActorMatch(self, actor, **kwargs): 287 | """ 288 | A specific use of getActorQuery() to match actors matching a pattern. 289 | The API will be queried and will return the results found. 290 | Any other keywords passed to the function will be encoded in the 291 | URL request - in case you want to filter or sort, for example. 292 | """ 293 | query = self.getActorQuery(actor, searchFilter="match", perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 294 | result = self.request(query) 295 | return result 296 | 297 | def getIndicatorQuery(self, indicator, searchFilter="equal", **kwargs): 298 | """ 299 | This function builds a URL query to search for an indicator. The 300 | indicator string is what to search for. The searchFilter allows you 301 | specific match or equal. Any other keyword attributes passed will also 302 | be encoded as parameters. 303 | """ 304 | encodedargs = "" 305 | 306 | if any(kwargs): 307 | # extra keyword arguments get passed - use to sort, filter. 308 | encodedargs = "&" + self.getURLParams(**kwargs) 309 | 310 | query = "indicator?" + searchFilter + "=" + indicator + encodedargs 311 | 312 | return query 313 | # end getIndicatorQuery 314 | 315 | def SearchIndicatorEqual(self, indicator, **kwargs): 316 | """ 317 | Search the API for an indicator. 318 | Any other keyword arguments passed here will be encoded in the URL 319 | """ 320 | 321 | # build URL query 322 | query = self.getIndicatorQuery(indicator, perPage=self.perpage, include_deleted=self.deleted, **kwargs) 323 | # search API 324 | result = self.request(query) 325 | # return results 326 | return result 327 | # end SearchIndicatorEqual() 328 | 329 | def SearchIndicatorMatch(self, indicator, **kwargs): 330 | """ 331 | Search the API for an indicator pattern. 332 | """ 333 | 334 | query = self.getIndicatorQuery(indicator, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 335 | result = self.request(query) 336 | return result 337 | # end SearchIndicatorMatch() 338 | 339 | def SearchIP(self, ip): 340 | """ 341 | Search the API for an IP address 342 | """ 343 | 344 | query = self.getIndicatorQuery(ip, searchFilter="match", type='ip_address', perPage=self.perpage, include_deleted=self.deleted) 345 | result = self.request(query) 346 | return result 347 | # end SearchIP() 348 | 349 | def SearchDomain(self, domain): 350 | """ 351 | Search the API for a domain 352 | """ 353 | 354 | query = self.getIndicatorQuery(domain, searchFilter="match", type='domain', perPage=self.perpage, include_deleted=self.deleted) 355 | result = self.request(query) 356 | return result 357 | # end SearchDomain() 358 | 359 | def SearchMutex(self, mutex): 360 | """ 361 | Search the API for a Mutex name 362 | """ 363 | 364 | query = self.getIndicatorQuery(mutex, searchFilter="match", type='mutex_name', perPage=self.perpage) 365 | result = self.request(query) 366 | return result 367 | # end SearchMutex() 368 | 369 | def SearchHash(self, myhash): 370 | """ 371 | Search the API for a file hash. 372 | The function will use the length of the hash string to limit the API 373 | query to a specific type of hash (i.e. MD5, SHA-1, SHA-256. 374 | """ 375 | 376 | # get length of hash and figure out what type it is 377 | hash_len = len(myhash) 378 | if hash_len == 32: 379 | htype = 'hash_md5' 380 | elif hash_len == 40: 381 | htype = 'hash_sha1' 382 | elif hash_len == 64: 383 | htype = 'hash_sha256' 384 | else: 385 | raise Exception("You sure that hash was right?") 386 | 387 | # build query to search for hash by type 388 | query = self.getIndicatorQuery(myhash, type=htype, perPage=self.perpage) 389 | # search API 390 | result = self.request(query) 391 | return result 392 | # end SearchHash() 393 | 394 | def getLastUpdatedQuery(self, date, searchFilter, **kwargs): 395 | """ 396 | Build a query to get changes before/after date argument 397 | The searchfilter defaults to greater than or equal to the time passed. 398 | """ 399 | 400 | encodedargs = "" 401 | 402 | if any(kwargs): 403 | # extra keyword arguments get passed - use to sort, filter. 404 | encodedargs = self.getURLParams(**kwargs) 405 | 406 | query = "last_updated?" + searchFilter + "=" + str(date) + "&" + encodedargs 407 | 408 | if self.debug: 409 | print("query: " + query) 410 | 411 | return query 412 | # end getLastUpdatedQuery 413 | 414 | def SearchLastUpdated(self, date, searchFilter="gte", **kwargs): 415 | """ 416 | Get API updates before or after the date specified and return the results 417 | Extra keyword arguments are passed so you can sort, etc. 418 | """ 419 | 420 | if searchFilter not in self.validFilter: 421 | raise Exception("Invalid search filter for last_updated") 422 | 423 | query = self.getLastUpdatedQuery(date, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 424 | 425 | result = self.request(query) 426 | 427 | return result 428 | # end SearchDate() 429 | 430 | def getEpochDaysAgo(self, days): 431 | """ 432 | Pass this function an interger n and this function will return 433 | the epoc time for n days ago. 434 | getEpochDaysAgo(1), for example, returns the epoch time for 435 | yesterday (24 hours ago). 436 | """ 437 | 438 | # get datetime object for n days ago. 439 | daysago = datetime.now() - timedelta(days=days) 440 | 441 | # convert that to standard unix time, ust int() to chop off decimal. 442 | etime = int((daysago - datetime(1970, 1, 1)).total_seconds()) 443 | 444 | # return number of seconds 445 | return etime 446 | 447 | def SearchLastDay(self, **kwargs): 448 | """ 449 | Pull any Intel data that has been updated in the last 24 hours 450 | """ 451 | 452 | etime = self.getEpochDaysAgo(1) 453 | 454 | result = self.SearchLastUpdated(etime, **kwargs) 455 | 456 | return result 457 | # end SearchLastDay() 458 | 459 | def SearchLastWeek(self, **kwargs): 460 | """ 461 | Pull any Intel data that has been updated in the last week. 462 | """ 463 | 464 | etime = self.getEpochDaysAgo(7) 465 | 466 | result = self.SearchLastUpdated(etime, **kwargs) 467 | 468 | return result 469 | # end SearchLastWeek() 470 | 471 | def GetReportQuery(self, report, searchFilter, **kwargs): 472 | """ 473 | Build an API query to search by report name. 474 | Must pass it a report name as a string. 475 | I assume searchFilter is equal OR match but come to think of it I've 476 | only tested "equal." 477 | Other keyword arguments can be passed to include sorting etc. 478 | Returns a string for the URL query search. 479 | """ 480 | # TODO - match reports?? 481 | encodedargs = "" 482 | 483 | if any(kwargs): 484 | # extra keyword arguments get passed - use to sort, filter. 485 | encodedargs = "&" + self.getURLParams(**kwargs) 486 | 487 | # build the query string 488 | query = "report?" + searchFilter + "=" + report + encodedargs 489 | 490 | return query 491 | # end GetReportQuery() 492 | 493 | def SearchReport(self, report, searchFilter="equal", **kwargs): 494 | """ 495 | Search the API for a specific report. 496 | Pass the report name as a string, and any other options. 497 | Returns the results of the API query. 498 | """ 499 | query = self.GetReportQuery(report, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 500 | result = self.request(query) 501 | 502 | return result 503 | # end SearchReport() 504 | 505 | def SearchTarget(self, target, searchFilter="match", **kwargs): 506 | """ 507 | Search the API for a specific Target Industry. 508 | Pass the industry name as a string, and any other options. 509 | Returns the results of the API query. 510 | """ 511 | 512 | # validate target 513 | validTarget = ['Aerospace', 'Agricultural', 'Chemical', 'Defense', 'Dissident', 'Energy', 'Extractive', 'Financial', 'Government', 'Healthcare', 'Insurance', 'International Organizations', 'Legal', 'Manufacturing', 'Media', 'NGO', 'Pharmaceutical', 'Research', 'Retail', 'Shipping', 'Technology', 'Telecom', 'Transportation', 'Universities'] 514 | if searchFilter not in self.validFilter: 515 | raise Exception("Invalid search filter") 516 | if target not in validTarget: 517 | raise Exception("Invalid target industry") 518 | 519 | # append industry 520 | label = "Target/" + target 521 | 522 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 523 | result = self.request(query) 524 | 525 | return result 526 | # end SearchTarget() 527 | 528 | def GetLabelQuery(self, label, searchFilter, **kwargs): 529 | """ 530 | Build an API query to serach by Label. 531 | Must pass it a label as a string. 532 | 533 | Labels are a generic framework for attaching metadata to an intel 534 | indicator. See the documentation for full capabilities. 535 | 536 | Other keyword arguments can be passed to include sorting etc. 537 | Returns a string for the URL query search 538 | """ 539 | 540 | # good query: search/labels?match=Retail 541 | 542 | encodedargs = "" 543 | 544 | if any(kwargs): 545 | # extra keyword arguments get passed - used to sort, filter. 546 | encodedargs = "&" + self.getURLParams(**kwargs) 547 | 548 | # build the query string 549 | query = "labels?" + searchFilter + "=" + label + encodedargs 550 | 551 | return query 552 | # end GetLabelQuery 553 | 554 | def SearchLabel(self, label, searchFilter="match", **kwargs): 555 | """ 556 | Search the API for a specific Label 557 | Pass the label as a string, and any other options. 558 | Returns the results of the API query. 559 | 560 | Labels are a generic framework for attaching metadata to an intel 561 | indicator. See the documentation for full capabilities or checkout 562 | the functions that call this one. 563 | 564 | You can search the entirel label, with the forward slash. 565 | For example, MaliciousConfidence/High 566 | 567 | """ 568 | 569 | # validate 570 | if searchFilter not in self.validFilter: 571 | raise Exception("Invalid search filter") 572 | 573 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 574 | result = self.request(query) 575 | 576 | return result 577 | # end SearchLabel() 578 | 579 | def SearchConfidence(self, confidence, searchFilter="match", **kwargs): 580 | """ 581 | Search the API by Malicious Confidence. 582 | Pass the level (high, medium, low, unverified) as a string, 583 | and any other options. 584 | Returns the results of the API query. 585 | """ 586 | 587 | # validate target 588 | validConfidence = ['high', 'medium', 'low', 'unverified'] 589 | if searchFilter not in self.validFilter: 590 | raise Exception("Invalid search filter") 591 | if confidence not in validConfidence: 592 | raise Exception("Invalid confidence level: " + confidence) 593 | 594 | # append industry 595 | label = "MaliciousConfidence/" + confidence 596 | 597 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 598 | result = self.request(query) 599 | 600 | return result 601 | # end SearchConfidence() 602 | 603 | def SearchKillChain(self, chain, searchFilter="match", **kwargs): 604 | """ 605 | Search the API by Kill Chain stage. 606 | Pass the level as a string, 607 | and any other options. 608 | Returns the results of the API query. 609 | """ 610 | 611 | # validate parameters 612 | validKillChain = ['reconnaissance', 'weaponization', 'delivery', 'exploitation', 'installation', 'c2', 'actionsonobjectives'] 613 | if searchFilter not in self.validFilter: 614 | raise Exception("Invalid search filter") 615 | if chain not in validKillChain: 616 | raise Exception("Invalid kill chain link: " + chain) 617 | 618 | # append chain to label type 619 | label = "kill_chain/" + chain 620 | 621 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 622 | result = self.request(query) 623 | 624 | return result 625 | # end SearchKillChain() 626 | 627 | def SearchMalware(self, malware, searchFilter="match", **kwargs): 628 | """ 629 | Search the API by malware family. 630 | Pass the level as a string, 631 | and any other options. 632 | Returns the results of the API query. 633 | """ 634 | 635 | # validate parameters 636 | if searchFilter not in self.validFilter: 637 | raise Exception("Invalid search filter") 638 | 639 | encodedargs = "" 640 | if any(kwargs): 641 | encodedargs = "&" + self.getURLParams(**kwargs) 642 | 643 | query = "malware_family?" + searchFilter + "=" + malware + encodedargs 644 | 645 | result = self.request(query) 646 | 647 | return result 648 | # end SearchMalware() 649 | 650 | def SearchActive(self, searchFilter="match", **kwargs): 651 | """ 652 | Search the API for indicators confirmed active 653 | Pass the search filter 654 | and any other options. 655 | Returns the results of the API query. 656 | """ 657 | 658 | # validate parameters 659 | if searchFilter not in self.validFilter: 660 | raise Exception("Invalid search filter") 661 | 662 | # append chain to label type 663 | label = "confirmedactive" 664 | 665 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 666 | result = self.request(query) 667 | 668 | return result 669 | # end SearchActive() 670 | 671 | def SearchThreatType(self, threat, searchFilter="match", **kwargs): 672 | """ 673 | Search the API by threat type 674 | Pass the level as a string, 675 | and any other options. 676 | Returns the results of the API query. 677 | """ 678 | 679 | # validate parameters 680 | validThreat = ['ClickFraud', 'Commodity', 'PointOfSale', 'Ransomware', 'Suspicious', 'Targeted', 'TargetedCrimeware', 'Vulnerability'] 681 | if searchFilter not in self.validFilter: 682 | raise Exception("Invalid search filter") 683 | if threat not in validThreat: 684 | raise Exception("Invalid Threat type: " + threat) 685 | 686 | # append chain to label type 687 | label = "ThreatType/" + threat 688 | 689 | query = self.GetLabelQuery(label, searchFilter, **kwargs) 690 | result = self.request(query) 691 | 692 | return result 693 | # end SearchThreatType() 694 | 695 | def SearchDomainType(self, domain, searchFilter="match", **kwargs): 696 | """ 697 | Search the API by domain type 698 | Pass the level as a string, 699 | and any other options. 700 | Returns the results of the API query. 701 | """ 702 | 703 | # validate parameters 704 | validType = ['ActorControlled', 'DGA', 'DynamicDNS', 'DynamicDNS/Afraid', 'DynamicDNS/DYN', 'DynamicDNS/Hostinger', 'DynamicDNS/noIP', 'DynamicDNS/Oray', 'KnownGood', 'LegitimateCompromised', 'PhishingDomain', 'Sinkholed', 'StrategicWebCompromise', 'Unregistered'] 705 | if searchFilter not in self.validFilter: 706 | raise Exception("Invalid search filter") 707 | if domain not in validType: 708 | raise Exception("Invalid Domain type: " + domain) 709 | 710 | # append chain to label type 711 | label = "DomaintType/" + domain 712 | 713 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 714 | result = self.request(query) 715 | 716 | return result 717 | # end SearchDomainType() 718 | 719 | def SearchEmailType(self, email, searchFilter="match", **kwargs): 720 | """ 721 | Search the API by email address type 722 | Pass the email address type as a string, 723 | and any other options. 724 | Returns the results of the API query. 725 | """ 726 | 727 | # validate parameters 728 | validType = ['DomainRegistrant', 'SpearphishSender'] 729 | if searchFilter not in self.validFilter: 730 | raise Exception("Invalid search filter") 731 | if email not in validType: 732 | raise Exception("Invalid email type: " + email) 733 | 734 | # append chain to label type 735 | label = "EmailAddressType/" + email 736 | 737 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 738 | result = self.request(query) 739 | 740 | return result 741 | # end SearchEmailType() 742 | 743 | def SearchIPType(self, iptype, searchFilter="match", **kwargs): 744 | """ 745 | Search the API by ip address type 746 | Pass the ip address type as a string, 747 | and any other options. 748 | Returns the results of the API query. 749 | """ 750 | 751 | # validate parameters 752 | validType = ['HtranDestinationNode', 'HtranProxy', 'HtranProxy', 'LegitimateCompromised', 'Parking', 'PopularSite', 'SharedWebHost', 'Sinkholed', 'TorProxy'] 753 | if searchFilter not in self.validFilter: 754 | raise Exception("Invalid search filter") 755 | if iptype not in validType: 756 | raise Exception("Invalid email IP type: " + iptype) 757 | 758 | label = iptype 759 | 760 | query = self.GetLabelQuery(label, searchFilter, perPage=self.perpage, page=self.page, include_deleted=self.deleted, **kwargs) 761 | result = self.request(query) 762 | 763 | return result 764 | # end SearchIPType() 765 | 766 | def GetMQDownloadQuery(self, filehash, **kwargs): 767 | #Model query: 768 | #GET https://intelapi.crowdstrike.com/malquery/download/v1/ 769 | 770 | #TODO error checking on filehash 771 | 772 | # build the query string 773 | query = "download/v1/" + filehash 774 | 775 | return query 776 | #end GetMQDownloadQuery 777 | 778 | def MQDownloadHash(self, filehash, **kwargs): 779 | # Search malquery by file hash to download the sample 780 | query = self.GetMQDownloadQuery(filehash, **kwargs) 781 | 782 | result = self.request(query, queryType="malquery") 783 | return result 784 | 785 | # end MQDownloadHash 786 | 787 | def GetReportId(self, report): 788 | #/reports/queries/reports/v1?name=CSIT-18178 789 | query = "queries/reports/v1?name=" + report 790 | 791 | searchResult= self.request(query, queryType="reports") 792 | 793 | data = json.loads(searchResult.text) 794 | ids = data['resources'][0] #TODO could return more than one... 795 | 796 | return ids 797 | 798 | def GetReportDownloadJSONQuery(self, report): 799 | #entities/reports/v1?ids=40535 800 | reportId = self.GetReportId(report) 801 | query = "entities/reports/v1?ids=" + reportId 802 | return query 803 | 804 | def GetReportDownloadPDFQuery(self, report): 805 | #entities/report-files/v1?ids=40535 806 | reportId = self.GetReportId(report) 807 | query = "entities/report-files/v1?ids=" + reportId 808 | return query 809 | 810 | def GetReportJSON(self, report, **kwargs): 811 | reportId = self.GetReportId(report) 812 | query = self.GetReportDownloadJSONQuery(reportId, **kwargs) 813 | 814 | result = self.request(query, queryType="reports") 815 | return result 816 | 817 | def GetReportPDF(self, report, **kwargs): 818 | query = self.GetReportDownloadPDFQuery(report, **kwargs) 819 | 820 | result = self.request(query, queryType="reports") 821 | return result 822 | 823 | # =================================== 824 | # Output 825 | # =================================== 826 | 827 | def GetHashesFromResults(self, result, related=False): 828 | """ 829 | Give this function results from an API query and it will pull out 830 | all of the hashes from indicators in those results and return them as a list 831 | of hashes. 832 | Also takes the option to include indicators from the relations section. This 833 | is off by default. 834 | """ 835 | 836 | hashes = [] 837 | # loop through json 838 | for item in result: 839 | itype = item['type'] 840 | if itype.find("hash") >= 0: 841 | # If the indicator is a hash, add it do the list 842 | hashes.append(item['indicator']) 843 | if related: 844 | # are we doing related indicators too? 845 | for relation in item['relations']: 846 | itype = relation['type'] 847 | if itype.find("hash") >= 0: 848 | # if it's a hash, add it to the list. 849 | hashes.append(relation['indicator']) 850 | 851 | # return list of hashes 852 | return hashes 853 | # end GetHashesFromResults() 854 | 855 | def GetIndicatorsFromResults(self, result, labels=True, related=False): 856 | """ 857 | Give this function results from an API query and it will pull out 858 | all of the indicators and return them as a list. 859 | The labels option will include all the label strings tied to that 860 | indicator. Each label will be added to the same line but deliminated 861 | with a colon. This is enabled by default. 862 | This funciton is intended to be much more verbose with its indicators than 863 | others, since it's returning all types. So this will tell you the type, 864 | the indicator, and any labels. 865 | If you just want the simple list check other funcitons. 866 | Also takes the option to include indicators from the relations section. This 867 | is off by default. These indicators are labeled as related. 868 | """ 869 | 870 | indicators = [] 871 | for item in result: 872 | # loop through each JSON object 873 | la = ":" 874 | # format: type:indicator:(labels) 875 | x = item['type'] + ":" + item['indicator'] 876 | if labels: 877 | # if labels are included then grab them and add them 878 | # to the string we're building here. 879 | for label in item['labels']: 880 | # loop through each label and add it. 881 | la += label['name'] 882 | la += ":" # deliminator 883 | 884 | if related: 885 | # are we including related indicators? 886 | for relation in item['relations']: 887 | # loop through each item in relations 888 | y = relation['type'] + ":" + relation['indicator'] + ":related" 889 | # add it to the list 890 | indicators.append(y) 891 | x += la # add labels 892 | # add to list 893 | indicators.append(x) 894 | 895 | # return list of strings 896 | return indicators 897 | # end GetIndicatorFromResults() 898 | 899 | def GetReportsFromResults(self, result): 900 | """ 901 | Pass this funciton results pulled from the API and it will return 902 | any report names associated with the indicators you have. 903 | If you have an indicator or maybe a threat actor you're working with 904 | you can use this to see if there are written reports you should also 905 | check out. 906 | """ 907 | 908 | reports = [] 909 | for item in result: 910 | # loop through each JSON indicator 911 | r = item['reports'] 912 | # does it have anything in the reports section? 913 | if r: 914 | for x in r: 915 | # if it exists, add each item in the list of reports 916 | # to our list 917 | reports.append(x) 918 | 919 | # return list of strings of reports 920 | return reports 921 | # end GetReportsFromResults() 922 | 923 | def GetDomainsFromResults(self, result, related=False): 924 | """ 925 | Pass this function results pulled from the API and it will return 926 | any domains that were included in the list of indicators. 927 | The related option will include any indicators that are related 928 | to the primary indicators returned from your API search. 929 | This returns a simple list of domains, one per line. Want to collect 930 | domains to feed into your proxy to block? 931 | """ 932 | 933 | domains = [] 934 | # loop through each JSON object 935 | for item in result: 936 | # check each indicator to see if the type is domain 937 | if item['type'] == "domain": 938 | # add domains to the list 939 | domains.append(item['indicator']) 940 | 941 | if related: 942 | # should we include related indicators? 943 | for relation in item['relations']: 944 | if relation['type'] == "domain": 945 | # if it's a domain, add it to the list. 946 | domains.append(relation['indicator']) 947 | 948 | # return a list of strings of domains 949 | return domains 950 | # end GetDomainsFromResults() 951 | 952 | def GetIPsFromResults(self, result, related=False): 953 | """ 954 | Pass this function results from an API search and it will return 955 | a list of IP addresses from your list of indicators. 956 | You may optionally include IP indicators identified as related 957 | indicators. 958 | This returns a list of IP addresses, one per line. Good for collecting 959 | all the IPs you want to feed into your firewall to block. 960 | """ 961 | 962 | ips = [] 963 | # loop through each JSON indicator 964 | for item in result: 965 | # pick ou the indicators that are IPs 966 | if item['type'] == "ip_address": 967 | # and add them to the list 968 | ips.append(item['indicator']) 969 | 970 | if related: 971 | # If you also wanted related indicators, loop through 972 | # those and check for IP addresses as well. 973 | for relation in item['relations']: 974 | if relation['type'] == "ip_address": 975 | # find the IPs and add them to the list. 976 | ips.append(relation['indicator']) 977 | 978 | # return list of strings of IP addresses 979 | return ips 980 | # end GetIPsFromResults() 981 | 982 | def GetActorsFromResults(self, result): 983 | """ 984 | Feed this function the results from an API search and it will return 985 | the name(s) of any threat actors associated with this list of indicators. 986 | If you already have some IPs, or domains, for example, you can use this to 987 | check for attribution. 988 | The function returns a raw list of actor names, you may want to dedupe them. 989 | """ 990 | 991 | actors = [] 992 | # loop through each JSON item in the results 993 | for item in result: 994 | # grab the actors section 995 | a = item['actors'] 996 | if a: 997 | # if there's anything in here loop through them 998 | for x in a: 999 | # add each actor the list we're building 1000 | actors.append(x) 1001 | 1002 | # return the list of actors that have been identified. 1003 | # This list is raw, you may want to sort and dedupe. 1004 | return actors 1005 | # end GetActorsFromResults() 1006 | 1007 | 1008 | """ REFERENCE 1009 | example = "actor?equal=ROCKETKITTEN" 1010 | example = "indicator?equal=www.we11point.com" 1011 | example = "actor?match=panda" 1012 | example = "actor?match=panda&sort=malicious_confidence&order=desc" 1013 | example = "last_updated?gte=1427846400&sort=last_updated&order=asc&perPage=100&page=1" 1014 | """ 1015 | 1016 | # end class CSIntelAPI 1017 | ###################################################################################### 1018 | 1019 | """ 1020 | This section sets up the module to use not as a class but called directly from the CLI 1021 | """ 1022 | 1023 | if __name__ == "__main__": 1024 | """ 1025 | This function sets up the API python file to be called directly. So this code 1026 | can either be imported into another script or some functions can be called 1027 | directly from the command line by running this script directly. This section 1028 | never runs if this file was imported. 1029 | 1030 | To see how this file can be executed directly run it with "-h" to see the 1031 | help on which command line flags are needed. 1032 | 1033 | The primary requirement is to feed this scrip your Customer ID and Key. This 1034 | can be done through CLI arguments or by specificying a config file. See the 1035 | documentation at the beginning (pydoc ./CSIntel.py) 1036 | 1037 | There are output options to select as well, with --out. The default, all, is to 1038 | print all the json received. You can also select to print out all the 1039 | indicators, which will show each prepended by the type of indicator. 1040 | """ 1041 | import argparse 1042 | import json 1043 | import pprint 1044 | 1045 | # Let's set this up to parse setup config and other arguments from the CLI 1046 | parser = argparse.ArgumentParser(description="CS Intel API - This program can be executed directly to work with CrowdStrike's Threat Intel API or be imported into other scripts to use.") 1047 | parser.add_argument('--custid', type=str, help="API Customer ID", default=None) 1048 | parser.add_argument('--custkey', type=str, help="API Customer Key", default=None) 1049 | parser.add_argument('--perPage', '-p', type=str, help="How many indicators per page?", default="100") 1050 | parser.add_argument('--Page', type=str, help="Page number of results to get.", default="1") 1051 | parser.add_argument('--config', '-c', type=str, help="Configuration File Name", default=defaultConfigFileName) 1052 | parser.add_argument('--raw', action='store_true', default=False, help='Raw JSON, do not print pretty') 1053 | parser.add_argument('--debug', '-b', action='store_true', default=False, help='Turn on some debug strings') 1054 | 1055 | # Error management is easier by specificying a group for the commands that can be used. 1056 | # Each of these are actions/searches the script can take. 1057 | cmdGroup = parser.add_mutually_exclusive_group(required=True) 1058 | cmdGroup.add_argument('--write', '-w', action='store_true', default=False, help='Write the API config to the file specified by the --config option') 1059 | cmdGroup.add_argument('--actor', '-a', type=str, help="Search for an actor by name", default=None) 1060 | cmdGroup.add_argument('--actors', '-s', type=str, help="Search for a actors by pattern", default=None) 1061 | cmdGroup.add_argument('--ip', type=str, help="Search for an IP address", default=None) 1062 | cmdGroup.add_argument('--domain', type=str, help="Search for a domain", default=None) 1063 | cmdGroup.add_argument('--report', type=str, help="Search for a report name, e.g. CSIT-XXXX", default=None) 1064 | cmdGroup.add_argument('--indicator', '-i', type=str, help="Search for an indicator", default=None) 1065 | cmdGroup.add_argument('--indicatorList', type=str, help="Give a file name of indiactors to check", default=None) 1066 | cmdGroup.add_argument('--label', '-l', type=str, help="Search for a label", default=None) 1067 | cmdGroup.add_argument('--target', type=str, help="Search by Targeted Industry", default=None) 1068 | cmdGroup.add_argument('--confidence', type=str, help="Search by Malicious Confidence", default=None) 1069 | cmdGroup.add_argument('--killchain', type=str, help="Search by kill chain stage", default=None) 1070 | cmdGroup.add_argument('--malware', type=str, help="Search by malware family", default=None) 1071 | cmdGroup.add_argument('--active', action='store_true', help="Get confirmed active indicators", default=None) 1072 | cmdGroup.add_argument('--threat', type=str, help="Search by threat type", default=None) 1073 | cmdGroup.add_argument('--domaintype', type=str, help="Search by domain type", default=None) 1074 | cmdGroup.add_argument('--iptype', type=str, help="Search by IP Type", default=None) 1075 | cmdGroup.add_argument('--emailtype', type=str, help="Search by email address type", default=None) 1076 | cmdGroup.add_argument('--day', action='store_true', help="Get all indicators that have changed in 24 hours", default=None) 1077 | cmdGroup.add_argument('--week', action='store_true', help="Get all indicators that have changed in the past week", default=None) 1078 | cmdGroup.add_argument('--download', '-f', type=str, help="Download a file by hash from Malquery", default=None) 1079 | cmdGroup.add_argument('--downloadReport', '-R', type=str, help="Download a report, e.g. CSIT-XXXX", default=None) 1080 | cmdGroup.add_argument('--downloadReportFiles', '-F', type=str, help="Download files associated with a report name, e.g. CSIT-XXXX", default=None) 1081 | 1082 | 1083 | parser.add_argument('--out', '-o', choices=['all', 'indicators', 'hashes', 'domains', 'ips', 'actors', 'reports', 'IfReport'], help="What should I print? Default: all", default='all') 1084 | parser.add_argument('--count', choices=['actors'], help="Tally count totals by variable stipulated here.", default=None) 1085 | #TODO Mutually exclusive group ^^ 1086 | 1087 | parser.add_argument('--related', action='store_true', help="Flag: Include related indicators.", default=False) 1088 | parser.add_argument('--deleted', action='store_true', help="Include deleted indicators.", default=False) 1089 | 1090 | # run this and parse out the arguments 1091 | args = parser.parse_args() 1092 | 1093 | # Some error checking on parsed configuration 1094 | if args.write is True and args.config is None: 1095 | raise Exception("To write to config file you must pass file name to --config") 1096 | if (args.custid is None and args.custkey is not None) or (args.custkey is None and args.custid is not None): 1097 | raise Exception("Must include both customer ID and key") 1098 | 1099 | # Now check to see if we're getting API settings from CLI or config file 1100 | # if the write flag is set pass the config file name to be written to. 1101 | if args.custid is not None: 1102 | # use CLI parameters 1103 | custid = args.custid 1104 | custkey = args.custkey 1105 | else: 1106 | # no ID and key from argument, get them from config file 1107 | (custid, custkey, perpage) = readConfig(args.config) 1108 | 1109 | # Create the API object 1110 | api_obj = CSIntelAPI(custid, custkey, args.perPage, args.Page, args.deleted, args.debug) 1111 | 1112 | # Check to see if config in memory should be written to disk 1113 | if args.write: 1114 | api_obj.writeConfig(args.config) 1115 | 1116 | # now do stuff... 1117 | 1118 | if args.actors is not None: # search actors for a pattern 1119 | result = api_obj.SearchActorMatch(args.actors, sort='actor') 1120 | 1121 | if args.actor is not None: # search actors for a specific actor 1122 | result = api_obj.SearchActorEqual(args.actor, sort="malicious_confidence", order='desc') 1123 | 1124 | if args.ip is not None: # search API for an IP address 1125 | result = api_obj.SearchIP(args.ip) 1126 | 1127 | if args.domain is not None: # search API for a domain 1128 | result = api_obj.SearchDomain(args.domain) 1129 | 1130 | if args.report is not None: # search API for a report name 1131 | result = api_obj.SearchReport(args.report) 1132 | 1133 | if args.indicator is not None: # generic indicator search 1134 | result = api_obj.SearchIndicatorMatch(args.indicator) 1135 | 1136 | if args.indicatorList is not None: # Check a file of indicators 1137 | 1138 | #experimental 1139 | 1140 | results = [] 1141 | 1142 | #open file 1143 | with open(args.indicatorList, 'r') as f: 1144 | #loop through each indicator 1145 | for indicator in f: 1146 | if not indicator: continue 1147 | #check indicator 1148 | result = api_obj.SearchIndicatorMatch(indicator.rstrip()) 1149 | data = json.loads(result.text) 1150 | if args.raw is False: 1151 | pprint.pprint(data) 1152 | else: 1153 | print(data) 1154 | 1155 | raise SystemExit #handling data like this was unexpected... 1156 | 1157 | if args.label is not None: # generic label search 1158 | result = api_obj.SearchLabel(args.label) 1159 | 1160 | if args.target is not None: # search targeted industry 1161 | result = api_obj.SearchTarget(args.target) 1162 | 1163 | if args.confidence is not None: # search malicious confidence 1164 | result = api_obj.SearchConfidence(args.confidence) 1165 | 1166 | if args.killchain is not None: # search by kill chain stage 1167 | result = api_obj.SearchKillChain(args.killchain) 1168 | 1169 | if args.malware is not None: # search by malware family 1170 | result = api_obj.SearchMalware(args.malware) 1171 | 1172 | if args.active is not None: # search for confirmed active malware 1173 | print("WTF LOL") 1174 | result = api_obj.SearchActive() 1175 | 1176 | if args.threat is not None: # search by threat type 1177 | result = api_obj.SearchThreatType(args.threat) 1178 | 1179 | if args.domaintype is not None: # search by domain type 1180 | result = api_obj.SearchDomainType(args.domaintype) 1181 | 1182 | if args.iptype is not None: # search by IP Address type 1183 | result = api_obj.SearchIPType(args.iptype) 1184 | 1185 | if args.emailtype is not None: # search by email type 1186 | result = api_obj.SearchEmailType(args.emailtype) 1187 | 1188 | if args.day is not None: # grab indicators for the last day 1189 | result = api_obj.SearchLastDay() 1190 | 1191 | if args.week is not None: # grab indicators for the last week 1192 | result = api_obj.SearchLastWeek() 1193 | 1194 | if args.download is not None: # try to download file from MQ 1195 | result = api_obj.MQDownloadHash(args.download) 1196 | #print(result.headers.get('content-type')) #debug 1197 | filename = args.download #name file the hash 1198 | open(filename, 'wb').write(result.content) 1199 | #exit 1200 | raise SystemExit 1201 | 1202 | 1203 | if args.downloadReport is not None: 1204 | result = api_obj.GetReportPDF(args.downloadReport) 1205 | filename = args.downloadReport + ".pdf" 1206 | open(filename, 'wb').write(result.content) 1207 | raise SystemExit 1208 | 1209 | 1210 | if args.downloadReportFiles is not None: 1211 | 1212 | result = api_obj.SearchReport(args.downloadReportFiles) 1213 | data = json.loads(result.text) 1214 | hashes = api_obj.GetHashesFromResults(data, related=args.related) 1215 | 1216 | for h in hashes: 1217 | result = api_obj.MQDownloadHash(h) 1218 | filename = h 1219 | open(filename, 'wb').write(result.content) 1220 | 1221 | raise SystemExit 1222 | 1223 | 1224 | 1225 | # load the raw JSON into python friendly structure 1226 | # print(result.text) 1227 | data = json.loads(result.text) 1228 | 1229 | # print results 1230 | if args.out == "hashes": 1231 | # get hashes form results, pass related option 1232 | hashes = api_obj.GetHashesFromResults(data, related=args.related) 1233 | for h in hashes: 1234 | print(h) 1235 | elif args.out == "indicators": 1236 | # get all the indicators 1237 | indicators = api_obj.GetIndicatorsFromResults(data, related=args.related) 1238 | # dedupe indicators 1239 | uniqueIndicators = set(indicators) 1240 | # print them one per line 1241 | for i in uniqueIndicators: 1242 | print(i) 1243 | elif args.out == "reports": 1244 | # print any report names associated with these indicators 1245 | reports = api_obj.GetReportsFromResults(data) 1246 | reports.sort() # sort list 1247 | for r in reports: 1248 | # print one per line 1249 | print(r) 1250 | elif args.out == "domains": 1251 | # get the domains from our results 1252 | domains = api_obj.GetDomainsFromResults(data, related=args.related) 1253 | domains.sort() # sort list 1254 | # print one per line 1255 | for d in domains: 1256 | print(d) 1257 | elif args.out == "actors": 1258 | # print what actors are tied to these indicators 1259 | actors = api_obj.GetActorsFromResults(data) 1260 | # dedupe the list 1261 | uniqueActors = set(actors) 1262 | for a in uniqueActors: 1263 | # print one per line 1264 | print(a) 1265 | elif args.out == "ips": 1266 | # get the IP addresses from our list of indicators 1267 | ips = api_obj.GetIPsFromResults(data, related=args.related) 1268 | uniqueIps = set(ips) # dedupe list 1269 | for i in uniqueIps: 1270 | # print one per line 1271 | print(i) 1272 | elif args.out == "IfReport": 1273 | # print out the raw data, but only if there is an associated 1274 | # report with it. 1275 | for datum in data: 1276 | if len(datum['reports']) > 0: 1277 | print(datum) 1278 | elif args.count == "actors": 1279 | from collections import Counter 1280 | actorSet = [] 1281 | 1282 | for datum in data: 1283 | if len(datum['actors']) > 0: 1284 | actorSet.extend( datum['actors'] ) 1285 | else: 1286 | actorSet.append( 'None' ) 1287 | 1288 | cActors = Counter(actorSet) 1289 | 1290 | for key, value in cActors.items(): 1291 | print(key, ",", value) 1292 | 1293 | 1294 | else: 1295 | # by default pretty print the whole JSON 1296 | if args.raw is False: 1297 | pprint.pprint(data) 1298 | else: 1299 | print(data) 1300 | 1301 | # EOF 1302 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # EOL 4 | This script uses the now depreciated key based API keys. The [falconpy project](https://github.com/CrowdStrike/falconpy) has standardized the OAuth API functions. 5 | 6 | 7 | # CSIntel 8 | CrowdStrike Threat Intelligence 9 | 10 | This file will act as a Python API for CrowdStrike's Threat Intelligence API. It was built to make it 11 | easy to use the Intel API. 12 | 13 | This module can be used one of two ways: by executing it directly from the command line or by importing 14 | it into another script. 15 | 16 | Example 1: 17 | 18 | $> ./CSIntel.py --custid ABC --custkey DEF --day 19 | 20 | Example 2: 21 | 22 | #!/usr/bin/python 23 | import CSIntel 24 | api_obj = CSIntel.CSIntelAPI(custid, custkey) 25 | results = api_obj.SearchLastWeek() 26 | 27 | To learn more about the functions you can use when importing this file see the included python documentation: 28 | 29 | 30 | $> pydoc ./CSIntel.py 31 | 32 | You can also see the examples included within for the simple functions that are used to enable 33 | the CLI commands. 34 | 35 | 36 | usage: CSIntel.py [-h] [--custid CUSTID] [--custkey CUSTKEY] 37 | [--perPage PERPAGE] [--Page PAGE] [--write] 38 | [--config CONFIG] [--raw] [--debug] 39 | (--actor ACTOR | --actors ACTORS | --ip IP | --domain DOMAIN | --report REPORT | --indicator INDICATOR | --label LABEL | --target TARGET | --confidence CONFIDENCE | --killchain KILLCHAIN | --malware MALWARE | --active | --threat THREAT | --domaintype DOMAINTYPE | --iptype IPTYPE | --emailtype EMAILTYPE | --day | --week) 40 | [--out {all,indicators,hashes,domains,ips,actors,reports,IfReport}] 41 | [--related] 42 | 43 | CS Intel API - This program can be executed directly to work with 44 | CrowdStrike's Threat Intel API or be imported into other scripts to use. 45 | 46 | optional arguments: 47 | -h, --help show this help message and exit 48 | --custid CUSTID API Customer ID 49 | --custkey CUSTKEY API Customer Key 50 | --perPage PERPAGE, -p PERPAGE 51 | How many indicators per page? 52 | --Page PAGE Page number of results to get. 53 | --write, -w Write the API config to the file specified by the 54 | --config option 55 | --config CONFIG, -c CONFIG 56 | Configuration File Name 57 | --raw Raw JSON, do not print pretty 58 | --debug, -b Turn on some debug strings 59 | --actor ACTOR, -a ACTOR 60 | Search for an actor by name 61 | --actors ACTORS, -s ACTORS 62 | Search for a actors by pattern 63 | --ip IP Search for an IP address 64 | --domain DOMAIN Search for a domain 65 | --report REPORT Search for a report name, e.g. CSIT-XXXX 66 | --indicator INDICATOR, -i INDICATOR 67 | Search for an indicator 68 | --label LABEL, -l LABEL 69 | Search for a label 70 | --target TARGET Search by Targeted Industry 71 | --confidence CONFIDENCE 72 | Search by Malicious Confidence 73 | --killchain KILLCHAIN 74 | Search by kill chain stage 75 | --malware MALWARE Search by malware family 76 | --active Get confirmed active indicators 77 | --threat THREAT Search by threat type 78 | --domaintype DOMAINTYPE 79 | Search by domain type 80 | --iptype IPTYPE Search by IP Type 81 | --emailtype EMAILTYPE 82 | Search by email address type 83 | --day Get all indicators that have changed in 24 hours 84 | --week Get all indicators that have changed in the past week 85 | --out {all,indicators,hashes,domains,ips,actors,reports}, -o {all,indicators,hashes,domains,ips,actors,reports,IfReport} 86 | What should I print? Default: all 87 | --related Flag: Include related indicators. 88 | 89 | 90 | ## Prerequisites 91 | 92 | You must also install the python library "requests." 93 | 94 | pip install requests 95 | 96 | ## Using from the Command Line 97 | ------- 98 | 99 | The first step to using this from the Command Line is to make sure you're passing your Customer ID 100 | and your Customer Key. There are two ways you can do this: 101 | 102 | 1. Pass your Customer ID and Key from the command line: 103 | `$> ./CSintel.py --custid --custkey ` 104 | 2. Place your Customer ID and Key in a config file to be read by the script. By default the file 105 | expected is ~/.csintel.ini 106 | 107 | In order to create this config file you can either write it explicitly or save the config from the 108 | command line executation. 109 | `$> ./CSintel.py --custid ABCD --custkey EFGH --write` 110 | 111 | This will save Customer ID & Key to the default config file (~/.csintel.ini). If you wish to specify 112 | a different file you can pass that: `--write --config diffFile.ini` 113 | 114 | If you want to manually write the config file it follows this layout: 115 | 116 | [CrowdStrikeIntelAPI] 117 | custid = ABCD 118 | custkey = EFGH 119 | perpage = 10 120 | 121 | Once you are setup to pass your Customer ID and Key you can start searching the Threat Intel API. 122 | 123 | 124 | ##Search 125 | 126 | Search Options 127 | 128 | ### Actor(s) 129 | * --actor 130 | 131 | Query a specific actor. 132 | 133 | The named Actor the indicator is associated with (e.g. Panda, Bear, Spider, etc). The actor list is also represented under the labels list in the JSON data structure. 134 | 135 | --actor rocketkitten 136 | 137 | * --actors 138 | 139 | Query a pattern and return all actors that match it. 140 | 141 | --actors kitten 142 | 143 | ###IP 144 | 145 | * --ip 146 | 147 | Search for an IP address. 148 | 149 | This is performed as an indicator search where type specifies IP address. 150 | 151 | ###Domain 152 | * --domain 153 | 154 | Search for a domain. 155 | 156 | This is performed as an indicator search where type specifies domain. 157 | 158 | ###Report 159 | * --report 160 | 161 | Search for a report name and get the indicators associated with it. 162 | 163 | The report ID the indicator is associated with (e.g. CSIT-XXXX, CSIR-XXXX, etc). The report list is also represented under the labels list in the JSON data structure. 164 | 165 | ###Indicator 166 | * --indicator 167 | 168 | Possible indicator types, from the following set: 169 | 170 | * binary\_string 171 | * compile\_time 172 | * device\_name 173 | * domain 174 | * email\_address 175 | * email\_subject 176 | * event\_name 177 | * file\_mapping 178 | * file\_name 179 | * file\_path 180 | * hash\_ion 181 | * hash\_md5 182 | * hash\_sha1 183 | * hash\_sha256 184 | * ip\_address 185 | * ip\_address\_block 186 | * mutex\_name 187 | * password 188 | * persona\_name 189 | * phone\_number 190 | * port 191 | * registry 192 | * semaphore\_name 193 | * service\_name 194 | * url 195 | 196 | ###Label 197 | * --label 198 | 199 | The Intel API contains additional context around an indicator under the labels list. Some of these labels, such as 'malicious\_confidence' are accessible via the top level data structure. All labels, including their associated timestamps, will be accessible via the labels list. 200 | 201 | ###Target 202 | * --target 203 | 204 | The activity associated with this indicator is known to target the indicated vertical sector, which could be any of: 205 | 206 | * Aerospace 207 | * Agricultural 208 | * Chemical 209 | * Defense 210 | * Dissident 211 | * Energy 212 | * Extractive 213 | * Financial 214 | * Government 215 | * Healthcare 216 | * Insurance 217 | * International Organizations 218 | * Legal 219 | * Manufacturing 220 | * Media 221 | * NGO 222 | * Pharmaceutical 223 | * Research 224 | * Retail 225 | * Shipping 226 | * Technology 227 | * Telecom 228 | * Transportation 229 | * Universities 230 | 231 | ###Confidence 232 | * --confidence 233 | 234 | Indicates a confidence level by which an indicator is considered to be malicious. For example, a malicious file hash may always have a value of 'high' while domains and IP addresses will very likely change over time. The malicious confidence level is also represented under the labels list in the JSON data structure. 235 | 236 | * High - If indicator is an IP or domain, it has been associated with malicious activity within the last 60 days. 237 | * Medium - If indicator is an IP or domain, it has been associated with malicious activity within the last 60-120 days. 238 | * Low - If indicator is an IP or domain, it has been associated with malicious activity exceeding 120 days. 239 | * Unverified - This indicator has not been verified by a Crowdstrike Intelligence analyst or an automated system. 240 | 241 | ###Kill Chain 242 | * --killchain 243 | 244 | The point in the kill chain at which an indicator is associated. The kill chain list is also represented under the labels list in the JSON data structure. The following italicized entries are sub-labels of the parent kill\_chain. 245 | 246 | An example Search is: search for “/labels?match=reconnaissance” 247 | 248 | * Reconnaissance 249 | This indicator is associated with the research, identification, and selection of targets by a malicious actor. 250 | 251 | * Weaponization 252 | This indicator is associated with assisting a malicious actor create malicious content. 253 | 254 | * Delivery 255 | This indicator is associated with the delivery of an exploit or malicious payload. 256 | 257 | * Exploitation 258 | This indicator is associated with the exploitation of a target system or environment. 259 | 260 | * Installation 261 | This indicator is associated with the installation or infection of a target system with a remote access tool or other tool allowing for persistence in the target environment. 262 | 263 | * C2 (Command and Control) 264 | This indicator is associated with malicious actor command and control. 265 | 266 | * Actionsonobjectives (Actions on Objectives) 267 | This indicator is associated with a malicious actor's desired effects and goals. 268 | 269 | 270 | ###Malware 271 | * --malware 272 | 273 | Indicates the malware family an indicator has been associated with. An indicator may be associated with more than one malware family. The malware family list is also represented under the labels list in the JSON data structure. 274 | 275 | ###Active 276 | * --active 277 | 278 | Status Type contains information tagged in the below italicized list which is broken down by the current status of the indicator. 279 | An example Search is: “/labels?match=confirmedactive” 280 | 281 | This indicator is likely to be currently supporting malicious activity 282 | 283 | ###Threat 284 | * --threat 285 | 286 | Threat Type contains information tagged in the below italicized list which is broken down by the type of threat category that was associated with the indicator. 287 | An example Search is: “/labels?match=clickfraud” 288 | 289 | * ClickFraud 290 | This indicator is used by actors engaging in click or ad fraud 291 | 292 | * Commodity 293 | This indicator is used with commodity type malware such as Zeus or Pony Downloader. 294 | PointOfSale 295 | This indicator is associated with activity known to target point-of- sale machines such as AlinaPoS or BlackPoS. 296 | 297 | * Ransomware 298 | This indicator is associated with ransomware malware such as Crytolocker or Cryptowall. 299 | 300 | * Suspicious 301 | This indicator is not currently associated with a known threat type but should be considered suspicious. 302 | Targeted 303 | This indicator is associated with a known actor suspected to associated with a nation-state such as DEEP PANDA or ENERGETIC BEAR. 304 | 305 | * TargetedCrimeware 306 | This indicator is associated with a known actor suspected to be engaging in criminal activity such as WICKED SPIDER. 307 | 308 | ###Domain Type 309 | * --domaintype 310 | 311 | Domain Type contains information tagged in the below italicized list which is broken down by the type of domain category that was identified. 312 | An example Search is: “/labels?match=actorcontrolled” 313 | 314 | * ActorControlled 315 | It is believed the malicious actor is still in control of this domain. 316 | 317 | * DGA 318 | This domain is the result of malware utilizing a domain generation algorithm. 319 | 320 | * DynamicDNS 321 | This domain is owned or used by a dynamic DNS service. 322 | 323 | * DynamicDNS/Afraid 324 | This domain is owned or used by the Afraid.org dynamic DNS service. 325 | 326 | * DynamicDNS/DYN 327 | This domain is owned or used by the DYN dynamic DNS service. 328 | 329 | * DynamicDNS/Hostinger 330 | This domain is owned or used by the Hostinger dynamic DNS service. 331 | 332 | * DynamicDNS/noIP 333 | This domain is owned or used by the NoIP dynamic DNS service. 334 | 335 | * DynamicDNS/Oray 336 | This domain is owned or used by the Oray dynamic DNS service. 337 | 338 | * KnownGood 339 | The domain itself (or the domain portion of a URL) is known to be legitimate, despite having been associated with malware or malicious activity. 340 | 341 | * LegitimateCompromised 342 | This domain does not typically pose a threat but has been compromised by a malicious actor and may be serving malicious content. 343 | 344 | * PhishingDomain 345 | This domain has been observed to be part of a phishing campaign. 346 | 347 | * Sinkholed 348 | The domain is being sinkholed, likely by a security research team. This indicates that, while traffic to the domain likely has a malicious source, the IP address to which it is resolving is controlled by a legitimate 3rd party. It is no longer believed to be under the control of the actor. 349 | 350 | * StrategicWebCompromise 351 | While similar to the DomainType/LegitimateCompromised label, this label indicates that the activity is of a more targeted nature. Oftentimes, targeted attackers will compromise a legitimate domain that they know to be a watering hole frequently visited by the users at the organizations they are looking to attack. 352 | 353 | * Unregistered 354 | The domain is not currently registered with any registrars. 355 | 356 | ###IP Address Type 357 | * --iptype 358 | 359 | IP Address Type contains information tagged in the below italicized list which is broken down by the type of IP category that was identified. 360 | An example Search is: “/labels?match=htrandestinationnode” 361 | 362 | * HtranDestinationNode 363 | An IP address with this label is being used as a destination address with the HTran Proxy Tool. 364 | 365 | * HtranProxy 366 | An IP address with this label is being used as a relay or proxy node with the HTran Proxy Tool. 367 | LegitimateCompromised 368 | It is suspected an IP address with this label is compromised by malicious actors. 369 | 370 | * Parking 371 | This IP address is likely being used as parking IP address. 372 | 373 | * PopularSite 374 | This IP address could be utilized for a variety of purposes and may appear more frequently than other IPs. 375 | 376 | * SharedWebHost 377 | This IP address may be hosting more than one website. 378 | 379 | * Sinkhole 380 | This IP address is likely a sinkhole being operated by a security researcher or vendor. 381 | 382 | * TorProxy 383 | This IP address is acting as a TOR (The Onion Router) Proxy 384 | 385 | ###Email Address Type 386 | * --emailtype 387 | 388 | Email Address Type contains information tagged in the below italicized list which is broken down by the type of email category that was identified. 389 | An example Search is: “/labels?match=domainregistrant” 390 | 391 | * DomainRegistrant 392 | This email address has been supplied in the registration information for known malicious domains. 393 | 394 | * SpearphishSender 395 | This email address has been used to send spearphishing emails. 396 | 397 | ##Output 398 | 399 | You can also specify what output you want to receive. By default these methods will pretty print all 400 | JSON received from the API request. Altenatively you can specify: 401 | 402 | --out indicators -print all indicators 403 | --out hashes -print just the hashes 404 | --out domains -print just domains 405 | --out ips -print just IP addresses 406 | --out actors -print any Actors associated with the API request data 407 | --out reports -print any reports associated with the API request data 408 | --out IfReport -print the raw data in JSON, but only for those indicators that are associated with a published report. 409 | 410 | 411 | 412 | Examples 413 | ========== 414 | 415 | Tell me about Rocket Kitten. 416 | 417 | >./CSIntel.py --actor rocketkitten 418 | ... 419 | 420 | Get all intel data that has been updated in the last 24 hours and print all indicators returned 421 | 422 | >./CSIntel.py --day --out indicators 423 | hash_md5:XXXXXXXXXXXXXXXXXXXXXXXXX:Malware/njRAT:MaliciousConfidence/High: 424 | hash_sha1:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:Malware/njRAT:MaliciousConfidence/High: 425 | ip_address:XXX.XX.XXX.X:ThreatType/Suspicious:IpAddressType/TorProxy: 426 | ... 427 | 428 | Having found an interesting IP address, search CrowdStrike's API for it and return if any threat 429 | actors have been associated with it. 430 | 431 | >./CSIntel.py --ip XXX.XXX.XXX.XXX --out actors 432 | WETPANDA 433 | 434 | Search the same IP address to see if it has been discussed in any Intelligence Reports. 435 | 436 | >./CSIntel.py --ip XXX.XXX.XXX.XXX --out reports 437 | CSIR-13017 438 | CSIT-13051 439 | 440 | Search a specified report and print all hashes associated with it 441 | 442 | >./CSIntel.py --report CSIR-13017 --out hashes 443 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXX 444 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXX 445 | ... 446 | 447 | ------------------------------------------------------------------------------------------------------- 448 | written by: adam.hogan@crowdstrike.com 449 | 450 | Change log 451 | ========= 452 | Version 0.8 453 | * PEP8 compliance added by Christophe Vandeplas 454 | * Python 3 compliance added by Christophe Vandeplas 455 | * API v 2 Support 456 | * Deleted option - there is now a command line option to include deleted indicators. 457 | 458 | Version 0.7.2 459 | * Added the '--out IfReport' option that will output all returned indicators (in JSON) if they have an associated report. 460 | 461 | Version 0.7.1 462 | * Added the --Page option to manually specifcy differnt pages if the number of results is larger than your perPage setting. For real this time. 463 | 464 | Version 0.7 465 | * Updated the '--malware' keyword to use direct search instead of a label search. 466 | * First shot at adding Pagination. 467 | * Added keyword '--perPage' that lets you specify how many results you want. 468 | * Added keyword '--page' that lets you specify which page of results (if the total number of indicators is greater than your perPage value). 469 | 470 | Version 0.6 471 | * Added the '--raw' keyword which will output raw json instead of pretty printed text. 472 | 473 | Version 0.5 474 | * Added proxy support using urllib. Proxy settings are automatically read from the OS settings 475 | 476 | Version 0.4.1 477 | * Serious bug, identified by Wes Bateman, where every search was a search for confirmed active malware. 478 | 479 | Version 0.4 480 | * Changed default location for config file to ~/.csintel.ini 481 | * Check to see if config file exists when it's specified, more detailed errors. 482 | * Added Label search framework 483 | * Added specific functions to search for specific labels: 484 | * SearchTarget 485 | * SearchConfidence 486 | * SearchKillChain 487 | * SearchMalware 488 | * SearchActive 489 | * SearchThreat 490 | * SearchDomainType 491 | * SearchIPType 492 | * SearchEmailType 493 | 494 | Version 0.3 495 | * Added search for report name 496 | * Added documentation examples 497 | * Cleaned up config write 498 | 499 | Version 0.2 500 | * Added indicator labels option, and it's availability from the CLI 501 | * Added "related" options to data methods to get indicators related to the original indicators. 502 | Also available from the Command Line with the --related flag. 503 | 504 | Version 0.1 505 | * Initial release 506 | 507 | 508 | 509 | TODO 510 | ==== 511 | 512 | * search by vulnerability 513 | * input validating 514 | * error control 515 | * add proxy server support 516 | 517 | 518 | ------------------------------------------------------------------------------------------------------- 519 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /csintel.ini: -------------------------------------------------------------------------------- 1 | [CrowdStrikeIntelAPI] 2 | custid = 3 | custkey = 4 | perpage = 100 5 | --------------------------------------------------------------------------------