├── ElasticSearchQuery.py ├── LICENSE ├── NOTICE ├── README.txt ├── TARDIS.py ├── VulnXML ├── CVE-2014-6271.xml ├── IPV4-10.10.10.10.xml ├── MD5-ffffffffffffffffffffffffffffffff.xml └── MS15-034.xml ├── config.xml ├── dbColumns.config ├── dependencies.py ├── findStixObservables.py ├── idmap.config ├── parseIP360.py ├── parseSTIX.py ├── splunk.py └── sql_tardis.sql /ElasticSearchQuery.py: -------------------------------------------------------------------------------- 1 | ''' 2 | * Copyright (C) 2015 Tripwire, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ''' 16 | 17 | 18 | from datetime import datetime 19 | from elasticsearch import Elasticsearch 20 | import xml.etree.ElementTree as ET 21 | import socket, struct 22 | import mysql.connector 23 | 24 | def ip2long(ip): 25 | return struct.unpack("!L", socket.inet_aton(ip))[0] 26 | 27 | 28 | def searchVulnerability(searchString,vulnerability,sourceIP,sourceHost): 29 | #Get Settings 30 | elasticSearch_ip='' 31 | db_ip='' 32 | db_user='' 33 | db_name='' 34 | db_pass='' 35 | try: 36 | configFile = 'config.xml' 37 | tree = ET.parse(configFile) 38 | root = tree.getroot() 39 | except: 40 | sys.exit("Not a valid XML file") 41 | for settings in root.findall("./elastic_search"): 42 | for ip in settings.findall("./ip"): 43 | elasticSearch_ip=ip.text 44 | for dbsettings in root.findall("./db"): 45 | for dbip in dbsettings.findall("./ip"): 46 | db_ip=dbip.text 47 | for dbname in dbsettings.findall("./db_name"): 48 | db_name=dbname.text 49 | for dbuser in dbsettings.findall("./user"): 50 | db_user=dbuser.text 51 | for dbpass in dbsettings.findall("./password"): 52 | db_pass=dbpass.text 53 | 54 | es = Elasticsearch( 55 | [ 56 | #Example: 'http://192.168.1.12:9200/', 57 | elasticSearch_ip, 58 | 59 | ] 60 | ) 61 | 62 | 63 | res = es.search(index="logstash-*", body=searchString) 64 | 65 | return(res) 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ========================================================================= 2 | == NOTICE file corresponding to section 4(d) of the Apache License, == 3 | == Version 2.0, in this case for the TARDIS distribution. == 4 | ========================================================================= 5 | 6 | This product includes software developed by 7 | Tripwire, Inc. (http://www.tripwire.com/). 8 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | 1) Execute 'python dependencies.py' to ensure all Python modules have been installed. Continue when no errors are generated. 2 | 3 | 2) Execute the sql_tardis.sql script to create the appropriate database and tables within MySQL. 4 | 5 | 3) Edit the config.xml with the appropriate credentials of the MySQL Server, Splunk, or Elastic Search instances. Update the log_source element with either 'splunk' or 'elastic_search', depending on the log repository being searched. 6 | 7 | 4) Edit the dbColumns.config document. This will be a mapping of STIX fields to the appropriate normalized column name in the log repository. Samples are provided for reference. 8 | 9 | 5a) If using an IP360 scan output file, execute 'python parseIP360.py -f ' 10 | -Note: Referenced STIX files are stored in the VulnXML directory. A sample for ShellShock (98520.xml) is provided for reference. Any number of STIX documents can be referenced. 11 | 12 | 5b) If using an individual STIX file, execute 'python parseSTIX.py -f -i -d 13 | -Note: the -d argument is optional. If no hostname is provided, TARDIS will attempt to look up the hostname from the IP provided. 14 | -Note: A copy of the STIX file is saved to the VulnXML directory using the CVE name as the filename. -------------------------------------------------------------------------------- /TARDIS.py: -------------------------------------------------------------------------------- 1 | ''' 2 | * Copyright (C) 2015 Tripwire, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ''' 16 | 17 | import re, csv, os, json 18 | from collections import defaultdict 19 | import ElasticSearchQuery 20 | from elasticsearch import Elasticsearch 21 | import findStixObservables 22 | import socket, struct 23 | import mysql.connector 24 | import xml.etree.ElementTree as ET 25 | import splunk 26 | import dateutil.parser 27 | import datetime 28 | 29 | def ip2long(ip): 30 | if ip =='*': 31 | return 0 32 | return struct.unpack("!L", socket.inet_aton(ip))[0] 33 | 34 | def long2ip(ip): 35 | return socket.inet_ntoa(struct.pack('!L', ip)) 36 | 37 | def getDBConnector(): 38 | try: 39 | configFile = 'config.xml' 40 | tree = ET.parse(configFile) 41 | root = tree.getroot() 42 | except: 43 | sys.exit("Not a valid config XML file") 44 | for settings in root.findall("./db"): 45 | for dbip in settings.findall("./ip"): 46 | db_ip=dbip.text 47 | for dbname in settings.findall("./db_name"): 48 | db_name=dbname.text 49 | for dbuser in settings.findall("./user"): 50 | db_user=dbuser.text 51 | for dbpass in settings.findall("./password"): 52 | db_pass=dbpass.text 53 | cnx = mysql.connector.connect(user=db_user, password=db_pass, host=db_ip, database=db_name) 54 | return cnx 55 | 56 | def writeElasticSearchResults(searchResults,vulnObject,sourceIP,vulnerability): 57 | for hit in searchResults['hits']['hits']: 58 | #check if vulnerability/asset combo exists in assetVulnerability Table 59 | es = Elasticsearch() 60 | doc = { 61 | 'doc' : { 62 | vulnObject: 'YES', 63 | 'CVE': vulnerability 64 | } 65 | } 66 | 67 | res = es.update(index=hit["_index"], id=hit["_id"], doc_type='logs', body=doc) 68 | cnx = getDBConnector() 69 | cursor = cnx.cursor() 70 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_log = '" + hit["_source"]["message"] + "'") 71 | 72 | cursor.execute(query) 73 | for row in cursor: 74 | logCheck=row[0] 75 | cursor.close() 76 | if logCheck==0: 77 | #Write data to DB 78 | cursor = cnx.cursor() 79 | add_logInstance = ("INSERT INTO attackInventory " 80 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 81 | "VALUES (%s, %s, %s, %s, %s)") 82 | try: 83 | attackerIP=ip2long(hit["_source"]["host"]) 84 | except: 85 | attackerIP='0' 86 | logData = (ip2long(sourceIP), attackerIP, hit["_source"]["@timestamp"], hit["_source"]["message"], vulnerability) 87 | 88 | # Insert new entry 89 | cursor.execute(add_logInstance , logData ) 90 | 91 | cnx.commit() 92 | cursor.close() 93 | cnx.close() 94 | 95 | def getElasticSearchResults(searchResults): 96 | numResults="%d" % searchResults['hits']['total'] 97 | return numResults 98 | 99 | def main(vulnerability,vulnObject,sourceIP,sourceHost): 100 | #Create results and working directories 101 | if not os.path.exists('Results'): 102 | os.makedirs('Results') 103 | if not os.path.exists('Working'): 104 | os.makedirs('Working') 105 | 106 | #Make sure the vulnerability is valid 107 | if vulnerability != "": 108 | vulnCheck=0 109 | resultCount=0 110 | logsource='' 111 | print("Searching for evidence of \"" + vulnerability + "\"") 112 | print(" Host: " + sourceIP) 113 | 114 | try: 115 | configFile = 'config.xml' 116 | tree = ET.parse(configFile) 117 | root = tree.getroot() 118 | except: 119 | sys.exit("Not a valid config XML file") 120 | for settings in root.findall("./log_source"): 121 | logsource=settings.text 122 | cnx = getDBConnector() 123 | 124 | 125 | #check if vulnerability/asset combo exists in assetVulnerability Table 126 | cursor = cnx.cursor() 127 | query = ("SELECT count(*) as count from assetVulnerabilities where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "'") 128 | 129 | cursor.execute(query) 130 | for row in cursor: 131 | vulnCheck=row[0] 132 | cursor.close() 133 | 134 | if vulnCheck==0: 135 | #No combination exists, write data to DB 136 | 137 | cursor = cnx.cursor() 138 | add_vulnInstance = ("INSERT INTO assetVulnerabilities " 139 | "(victim_ip, threat_id, active) " 140 | "VALUES (%s, %s, %s)") 141 | vulnData = (ip2long(sourceIP), vulnerability, '1') 142 | 143 | # Insert new entry 144 | cursor.execute(add_vulnInstance , vulnData ) 145 | 146 | cnx.commit() 147 | cursor.close() 148 | cnx.close() 149 | searchStringResults= findStixObservables.run(vulnerability) 150 | isExploitFound=False 151 | searchStringCount=0 152 | operator=searchStringResults[0] 153 | numResults=0 154 | if(searchStringResults[1]=="No search file found"): 155 | searchResults="0" 156 | print(" No search file found\n") 157 | elif(searchStringResults[1]=="No supported observables found"): 158 | searchResults="0" 159 | print(" No supported observables found\n") 160 | else: 161 | #run search... 162 | #search should return number of results 163 | #Insert source host from arguments 164 | for entry in searchStringResults: 165 | if logsource=="splunk": 166 | if (searchStringCount == 1): 167 | searchString=entry + " AND (host=\"" + sourceHost + "\" OR s_ip=\"" + sourceIP + "\" OR d_host=\"" + sourceHost + "\") | fields host, c_ip | fields - _bkt, _cd, _indextime, _kv, _serial, _si, _sourcetype | rename _raw as \"Raw Log\" | rename c_ip as clientip" 168 | numResults=splunk.searchVulnerability(searchString,vulnerability,sourceIP,sourceHost) 169 | if (numResults != "0"): 170 | data = json.load(numResults) 171 | 172 | if (operator=="AND"): 173 | if (searchStringCount > 1): 174 | resultCount=0 175 | for result in data["results"]: 176 | startTime = dateutil.parser.parse(data["results"][resultCount]["_time"]) + datetime.timedelta(days =- 300) 177 | endTime = dateutil.parser.parse(data["results"][resultCount]["_time"]) + datetime.timedelta(days = 300) 178 | searchString=entry + " AND (host=\"" + sourceHost + "\" OR s_ip=\"" + sourceIP + "\" OR d_host=\"" + sourceHost + "\") | fields host, clientip | fields - _bkt, _cd, _indextime, _kv, _serial, _si, _sourcetype | rename _raw as \"Raw Log\"" 179 | newResults=splunk.searchVulnerabilityTimeRange(searchString,vulnerability,sourceIP,sourceHost,startTime.isoformat(),endTime.isoformat()) 180 | if (newResults != "0"): 181 | #This is the result from search 1 182 | newData = json.load(newResults) 183 | newResultCount=0 184 | for result in newData["results"]: 185 | try: 186 | clientip=newData["results"][newResultCount]["clientip"] 187 | except: 188 | clientip="0" 189 | isExploitFound=True 190 | #These are the results from any further results proving the AND condition 191 | cnx = getDBConnector() 192 | cursor = cnx.cursor() 193 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_time = '" + data["results"][resultCount]["_time"] + "'") 194 | cursor.execute(query) 195 | for row in cursor: 196 | logCheck=row[0] 197 | cursor.close() 198 | if logCheck==0: 199 | #Write data to DB 200 | cursor = cnx.cursor() 201 | add_logInstance = ("INSERT INTO attackInventory " 202 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 203 | "VALUES (%s, %s, %s, %s, %s)") 204 | 205 | logData = (ip2long(sourceIP), ip2long(clientip), newData["results"][newResultCount]["_time"], newData["results"][newResultCount]["Raw Log"], vulnerability) 206 | # Insert new entry 207 | cursor.execute(add_logInstance , logData ) 208 | cnx.commit() 209 | cursor.close() 210 | cnx.close() 211 | newResultCount=newResultCount+1 212 | else: 213 | newResultCount=0 214 | if (isExploitFound==True): 215 | try: 216 | clientip=data["results"][resultCount]["clientip"] 217 | except: 218 | clientip="0" 219 | cnx = getDBConnector() 220 | cursor = cnx.cursor() 221 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_time = '" + data["results"][resultCount]["_time"] + "'") 222 | cursor.execute(query) 223 | for row in cursor: 224 | logCheck=row[0] 225 | cursor.close() 226 | if logCheck==0: 227 | #Write data to DB 228 | cursor = cnx.cursor() 229 | add_logInstance = ("INSERT INTO attackInventory " 230 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 231 | "VALUES (%s, %s, %s, %s, %s)") 232 | 233 | logData = (ip2long(sourceIP), ip2long(clientip), data["results"][resultCount]["_time"], data["results"][resultCount]["Raw Log"], vulnerability) 234 | # Insert new entry 235 | cursor.execute(add_logInstance , logData ) 236 | cnx.commit() 237 | cursor.close() 238 | cnx.close() 239 | resultCount=newResultCount+1 240 | else: 241 | resultCount=newResultCount 242 | elif (operator=="OR"): 243 | if (searchStringCount > 0): 244 | #only keep searching if there are more IOCS to look at... 245 | if len(searchStringResults)>2: 246 | searchString=entry + " AND (host=\"" + sourceHost + "\" OR s_ip=\"" + sourceIP + "\" OR d_host=\"" + sourceHost + "\") | fields host, clientip | fields - _bkt, _cd, _indextime, _kv, _serial, _si, _sourcetype | rename _raw as \"Raw Log\"" 247 | numResults=splunk.searchVulnerability(searchString,vulnerability,sourceIP,sourceHost) 248 | if (numResults != "0"): 249 | data = json.load(numResults) 250 | resultCount=0 251 | for result in data["results"]: 252 | isExploitFound=True 253 | cnx = getDBConnector() 254 | cursor = cnx.cursor() 255 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_time = '" + data["results"][resultCount]["_time"] + "'") 256 | cursor.execute(query) 257 | for row in cursor: 258 | logCheck=row[0] 259 | cursor.close() 260 | if logCheck==0: 261 | #Write data to DB 262 | cursor = cnx.cursor() 263 | add_logInstance = ("INSERT INTO attackInventory " 264 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 265 | "VALUES (%s, %s, %s, %s, %s)") 266 | logData = (ip2long(sourceIP), ip2long(data["results"][resultCount]["clientip"]), data["results"][resultCount]["_time"], data["results"][resultCount]["Raw Log"], vulnerability) 267 | 268 | # Insert new entry 269 | cursor.execute(add_logInstance , logData ) 270 | 271 | cnx.commit() 272 | cursor.close() 273 | cnx.close() 274 | resultCount=resultCount+1 275 | elif len(searchStringResults)==2: 276 | searchString=entry + " AND (host=\"" + sourceHost + "\" OR host=\"" + sourceIP + "\" OR s_ip=\"" + sourceIP + "\" OR d_host=\"" + sourceHost + "\") | fields host, clientip | fields - _bkt, _cd, _indextime, _kv, _serial, _si, _sourcetype | rename _raw as \"Raw Log\"" 277 | numResults=splunk.searchVulnerability(searchString,vulnerability,sourceIP,sourceHost) 278 | if (numResults != "0"): 279 | data = json.load(numResults) 280 | resultCount=0 281 | for result in data["results"]: 282 | isExploitFound=True 283 | cnx = getDBConnector() 284 | cursor = cnx.cursor() 285 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_time = '" + data["results"][resultCount]["_time"] + "'") 286 | cursor.execute(query) 287 | for row in cursor: 288 | logCheck=row[0] 289 | cursor.close() 290 | if logCheck==0: 291 | #Write data to DB 292 | cursor = cnx.cursor() 293 | add_logInstance = ("INSERT INTO attackInventory " 294 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 295 | "VALUES (%s, %s, %s, %s, %s)") 296 | 297 | logData = (ip2long(sourceIP), ip2long(data["results"][resultCount]["clientip"]), data["results"][resultCount]["_time"], data["results"][resultCount]["Raw Log"], vulnerability) 298 | 299 | # Insert new entry 300 | cursor.execute(add_logInstance , logData ) 301 | 302 | cnx.commit() 303 | cursor.close() 304 | cnx.close() 305 | resultCount=resultCount+1 306 | searchStringCount=searchStringCount+1 307 | elif logsource=="elastic_search": 308 | numResults=0 309 | startTime="-90d" 310 | endTime="now" 311 | #Insert source host from arguments 312 | entry = re.sub('\', sourceHost, entry) 313 | #Insert source IP from arguments 314 | entry = re.sub('\', sourceIP, entry) 315 | if (searchStringCount == 1): 316 | #Insert startTime 317 | entry = re.sub('\', startTime, entry) 318 | #Insert endTime 319 | entry = re.sub('\', endTime, entry) 320 | if sourceIP == '*': 321 | entry = re.sub('\', '1', entry) 322 | else: 323 | entry = re.sub('\', '2', entry) 324 | #print entry 325 | searchResults = ElasticSearchQuery.searchVulnerability(entry,vulnerability,sourceIP,sourceHost) 326 | #print searchResults 327 | numResults = getElasticSearchResults(searchResults) 328 | #print numResults 329 | if (operator=="AND"): 330 | if (searchStringCount > 1): 331 | resultCount=0 332 | for hit in searchResults['hits']['hits']: 333 | startTime = dateutil.parser.parse(hit["_source"]["@timestamp"]) + datetime.timedelta(days =- 1) 334 | 335 | endTime = dateutil.parser.parse(hit["_source"]["@timestamp"]) + datetime.timedelta(days = 1) 336 | #Insert start time 337 | entry = re.sub('\', str(startTime.isoformat()), entry) 338 | #Insert end time 339 | entry = re.sub('\', str(endTime.isoformat()), entry) 340 | newSearchResults = ElasticSearchQuery.searchVulnerability(entry,vulnerability,sourceIP,sourceHost) 341 | newResults = getElasticSearchResults(newSearchResults) 342 | if (newResults != "0"): 343 | #This is the result from search 1 344 | newResultCount=0 345 | isExploitFound=True 346 | for newhit in newSearchResults['hits']['hits']: 347 | try: 348 | attackerIP=newhit["_source"]["evt_srcip"] 349 | except: 350 | attackerIP="0.0.0.0" 351 | #These are the results from any further results proving the AND condition 352 | cnx = getDBConnector() 353 | cursor = cnx.cursor() 354 | #Check original log hit 355 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_log = '" + newhit["_source"]["message"] + "'") 356 | cursor.execute(query) 357 | for row in cursor: 358 | logCheck=row[0] 359 | cursor.close() 360 | if logCheck==0: 361 | #Write data to DB 362 | cursor = cnx.cursor() 363 | add_logInstance = ("INSERT INTO attackInventory " 364 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 365 | "VALUES (%s, %s, %s, %s, %s)") 366 | 367 | logData = (ip2long(sourceIP), ip2long(attackerIP),hit["_source"]["@timestamp"], hit["_source"]["message"], vulnerability) 368 | # Insert new entry 369 | cursor.execute(add_logInstance , logData ) 370 | cursor = cnx.cursor() 371 | #check new log hit 372 | query = ("SELECT count(*) as count from attackInventory where victim_ip = '" + str(ip2long(sourceIP)) + "' and threat_id = '" + vulnerability + "' and attack_log = '" + newhit["_source"]["message"] + "'") 373 | cursor.execute(query) 374 | for row in cursor: 375 | logCheck=row[0] 376 | cursor.close() 377 | if logCheck==0: 378 | #Write data to DB 379 | cursor = cnx.cursor() 380 | add_logInstance = ("INSERT INTO attackInventory " 381 | "(victim_ip, attacker_ip, attack_time, attack_log, threat_id) " 382 | "VALUES (%s, %s, %s, %s, %s)") 383 | 384 | logData = (ip2long(sourceIP), ip2long(attackerIP),newhit["_source"]["@timestamp"], newhit["_source"]["message"], vulnerability) 385 | # Insert new entry 386 | cursor.execute(add_logInstance , logData ) 387 | 388 | cnx.commit() 389 | cursor.close() 390 | cnx.close() 391 | newResultCount=newResultCount+1 392 | else: 393 | newResultCount=0 394 | resultCount=newResultCount+1 395 | 396 | 397 | 398 | elif (operator=="OR"): 399 | if (searchStringCount == 1): 400 | if (int(numResults) > 0): 401 | resultCount = int(numResults) 402 | writeElasticSearchResults(searchResults,vulnObject,sourceIP,vulnerability) 403 | isExploitFound=True 404 | if (searchStringCount > 1): 405 | #Insert startTime 406 | entry = re.sub('\', startTime, entry) 407 | #Insert endTime 408 | entry = re.sub('\', endTime, entry) 409 | if sourceIP == '*': 410 | entry = re.sub('\', '1', entry) 411 | else: 412 | entry = re.sub('\', '2', entry) 413 | #only keep searching if there are more IOCS to look at... 414 | if len(searchStringResults)>1: 415 | searchResults = ElasticSearchQuery.searchVulnerability(entry,vulnerability,sourceIP,sourceHost) 416 | numResults = getElasticSearchResults(searchResults) 417 | if int(numResults) > 0: 418 | writeElasticSearchResults(searchResults,vulnObject,sourceIP,vulnerability) 419 | resultCount = resultCount + int(numResults) 420 | searchStringCount=searchStringCount+1 421 | if (isExploitFound==True): 422 | print(" Found " + str(resultCount) + " instances of exploitation!") 423 | print(" Generating attack logs") 424 | #Parse through data list to get elastic timestamp for audit log times... 425 | else: 426 | print(" No instances of exploitation found.\n") 427 | else: 428 | resultCount=0 429 | print("Invalid vulnerability ID") 430 | return(resultCount) 431 | -------------------------------------------------------------------------------- /VulnXML/CVE-2014-6271.xml: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | ShellShock 32 | Detect the attempted exploitation of Shell Shock 33 | 34 | 35 | 36 | 37 | This observable will look for the existence of shell shock in the user agent string. 38 | 39 | 40 | 41 | HTTP 42 | 43 | 44 | 45 | 46 | 47 | 48 | () { 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | USN-2362-1: Bash Vulnerability 68 | 69 | CVE-2014-6271 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /VulnXML/IPV4-10.10.10.10.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | IPV4-10.10.10.10 16 | Malicious IPV4 10.10.10.10 reported from malcode.com 17 | 18 | 19 | 20 | 10.10.10.10 21 | 22 | 23 | 24 | 25 | 26 | malcode.com 27 | 28 | 29 | 2015-07-17T21:00:15+00:00 30 | 31 | 32 | 33 | 34 | 35 | 36 | malcode.com observable 37 | 38 | maliciousIPV4 39 | CVE-2015-0000 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /VulnXML/MD5-ffffffffffffffffffffffffffffffff.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | MD5-00b854fb55044d529f5f192688d11a21 16 | Malicious hash ffffffffffffffffffffffffffffffff reported from malc0de.com 17 | 18 | 19 | 20 | 21 | 22 | MD5 23 | ffffffffffffffffffffffffffffffff 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | malc0de.com 32 | 33 | 34 | 2015-07-17T13:00:26+00:00 35 | 36 | 37 | 38 | 39 | 40 | 41 | malc0de.com observable 42 | 43 | maliciousMD5 44 | CVE-2015-0000 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /VulnXML/MS15-034.xml: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | MS15-035 Exploit Detection 29 | Detect the attempted exploitation of MS15-035/CVE-2015-1635 30 | 31 | 32 | 33 | 34 | 35 | HTTP 36 | 37 | 38 | 39 | 40 | 41 | 416 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 6008 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 66 | MS15-035 HTTP.sys Vulnerability 67 | 68 | CVE-2015-1635 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | localhost 5 | root 6 | password 7 | TARDIS 8 | 9 | elastic_search 10 | 11 | no 12 | localhost 13 | 8089 14 | admin 15 | password 16 | 17 | 18 | yes 19 | http://localhost:9200/ 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /dbColumns.config: -------------------------------------------------------------------------------- 1 | #TARDIS Object Types 2 | Source:host 3 | 4 | 5 | #STIX Object Types 6 | HTTPStatusCode:http_response 7 | HTTPUserAgent:http_user_agent 8 | IPAddress:evt_dstip 9 | WindowsEventID:event_id 10 | MD5Hash:md5 11 | SHA1Hash:sha1 12 | URI:fqdn.raw -------------------------------------------------------------------------------- /dependencies.py: -------------------------------------------------------------------------------- 1 | try: 2 | import subprocess 3 | except ImportError: 4 | print "No subprocess!" 5 | try: 6 | import json 7 | except ImportError: 8 | print "No json!" 9 | try: 10 | import os 11 | except ImportError: 12 | print "No os!" 13 | try: 14 | import re 15 | except ImportError: 16 | print "No re!" 17 | try: 18 | import sys 19 | except ImportError: 20 | print "No sys!" 21 | try: 22 | import datetime 23 | except ImportError: 24 | print "No datetime!" 25 | try: 26 | import socket 27 | except ImportError: 28 | print "No socket!" 29 | try: 30 | import struct 31 | except ImportError: 32 | print "No struct!" 33 | try: 34 | import urllib2 35 | except ImportError: 36 | print "No urllib2!" 37 | try: 38 | import pxssh 39 | except ImportError: 40 | print "No pexpext/pxssh, try pip install pexpect" 41 | try: 42 | import csv 43 | except ImportError: 44 | print "No csv!" 45 | try: 46 | import shutil 47 | except ImportError: 48 | print "No shutil!" 49 | try: 50 | import argparse 51 | except ImportError: 52 | print "No argparse, try pip install argparse" 53 | try: 54 | import base64 55 | except ImportError: 56 | print "No base64!" 57 | try: 58 | import cookielib 59 | except ImportError: 60 | print "No cookielib!" 61 | try: 62 | import email 63 | except ImportError: 64 | print "No email!" 65 | try: 66 | import requests 67 | except ImportError: 68 | print "No requests, try pip install requests" 69 | try: 70 | import xml.etree.ElementTree as ET 71 | except ImportError: 72 | print "No xml etree!" 73 | try: 74 | from collections import defaultdict 75 | except ImportError: 76 | print "No collections defaultdict!" 77 | try: 78 | import dateutil.parser 79 | except ImportError: 80 | print "No dateutil parser, try pip install python-dateutil" 81 | try: 82 | import mysql.connector 83 | except ImportError: 84 | print "No mysql connector, try pip install --allow-external mysql-connector-python mysql-connector-python" 85 | try: 86 | from elasticsearch import Elasticsearch 87 | except ImportError: 88 | print "No elasticsearch, try pip install elasticsearch" 89 | try: 90 | import splunklib.client as client 91 | except ImportError: 92 | print "No splunk, try pip install splunk-sdk" 93 | try: 94 | from stix.core import STIXPackage 95 | except ImportError: 96 | print "No STIX, try pip intsall stix" 97 | try: 98 | from cybox.objects.win_event_log_object import WinEventLog 99 | except ImportError: 100 | print "No TAXI, try pip intsall stix" 101 | 102 | -------------------------------------------------------------------------------- /findStixObservables.py: -------------------------------------------------------------------------------- 1 | ''' 2 | * Copyright (C) 2015 Tripwire, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ''' 16 | 17 | 18 | import os, re, sys 19 | import xml.etree.ElementTree as ET 20 | from pprint import pprint 21 | from stix.core import STIXPackage 22 | from stix.core import STIXHeader 23 | from stix.indicator import Indicator 24 | from cybox.objects.win_event_log_object import WinEventLog 25 | from cybox.objects.network_connection_object import NetworkConnection 26 | from cybox.objects.address_object import Address 27 | from cybox.objects.uri_object import URI 28 | from cybox.objects.file_object import File 29 | import splunk 30 | import dateutil.parser 31 | import datetime 32 | 33 | def buildSearchString(column,value,condition): 34 | logsource = '' 35 | try: 36 | configFile = 'config.xml' 37 | tree = ET.parse(configFile) 38 | root = tree.getroot() 39 | except: 40 | sys.exit("Not a valid config XML file") 41 | for settings in root.findall("./log_source"): 42 | logsource=settings.text 43 | if logsource == "splunk": 44 | searchString = "search " 45 | searchString = searchString + column 46 | searchString = searchString + "=\"" 47 | if condition == "EndsWith": 48 | searchString = searchString + "*" 49 | searchString = searchString + value 50 | if condition == "StartsWith": 51 | searchString = searchString + "*" 52 | searchString = searchString + "\"" 53 | return searchString 54 | elif logsource == "elastic_search": 55 | 56 | sourceColumn = getColumnName("Source") 57 | searchString = "{\"from\": 0, \"size\": 100, \"query\": { \"bool\": { \"must\": [ { \"range\": {\"@timestamp\": { \"gt\": \"\", lt:\"\" } } }], \"should\": [ { \"prefix\" : { \"" 58 | searchString = searchString + column + "" 59 | searchString = searchString + "\" : \"" 60 | searchString = searchString + value 61 | searchString = searchString + "\"} }, { \"bool\": { \"should\": [ { \"prefix\": { \"" + sourceColumn + "\": \"\" }}, { \"prefix\": { \"" + sourceColumn + "\": \"\" }} ], \"minimum_should_match\": 1 }} ], \"minimum_should_match\": } }}" 62 | return searchString 63 | else: 64 | sys.exit("Unknown Log Source in Config XML") 65 | 66 | def getColumnName(column): 67 | for line in open("dbColumns.config"): 68 | if (column + ":") in line: 69 | columnName = re.sub('\S+\:', '', line) 70 | columnName = re.sub('(\r\n|\r|\n)', '', columnName) 71 | return columnName 72 | 73 | def run(vid): 74 | vulnXMLFile='VulnXML/' + vid + '.xml' 75 | fileCheck = os.path.exists(vulnXMLFile) 76 | if fileCheck==True: 77 | searchString = search(vulnXMLFile) 78 | return(searchString) 79 | else: 80 | searchString = [] 81 | searchString.append("OR") 82 | searchString.append("No search file found") 83 | return(searchString) 84 | 85 | def search(file): 86 | searchString = [] 87 | operator="OR" 88 | columnName='' 89 | # Parse input file 90 | stix_package = STIXPackage.from_xml(file) 91 | for observableList in stix_package.indicators: 92 | condition="equals" 93 | try: 94 | operator=observableList.observable.observable_composition.operator 95 | except: 96 | operator="OR" 97 | searchString.append(operator) 98 | try: 99 | for child in observableList.observable.observable_composition.observables: 100 | condition="" 101 | observableValue='' 102 | if (type(child._object.properties) == NetworkConnection): 103 | if (child._object.properties._fields["Layer7_Connections"] != None): 104 | try: 105 | observableValue=child._object.properties._fields["Layer7_Connections"]._fields["HTTP_Session"]._fields["HTTP_Request_Response"][0]._fields["HTTP_Client_Request"]._fields["HTTP_Request_Header"]._fields["Parsed_Header"]._fields["User_Agent"].value 106 | HTTPUserAgentCondition=child._object.properties._fields["Layer7_Connections"]._fields["HTTP_Session"]._fields["HTTP_Request_Response"][0]._fields["HTTP_Client_Request"]._fields["HTTP_Request_Header"]._fields["Parsed_Header"]._fields["User_Agent"].condition 107 | columnName = getColumnName("HTTPUserAgent") 108 | if (HTTPUserAgentCondition=="StartsWith"): 109 | condition="StartsWith" 110 | except: 111 | error="don't worry about it, probably looking for a different value in field" 112 | try: 113 | observableValue=child._object.properties._fields["Layer7_Connections"]._fields["HTTP_Session"]._fields["HTTP_Request_Response"][0]._fields["HTTP_Server_Response"]._fields["HTTP_Status_Line"]._fields["Status_Code"].value 114 | columnName = getColumnName("HTTPStatusCode") 115 | except: 116 | error="don't worry about it, probably looking for a different value in field" 117 | 118 | 119 | if (type(child._object.properties) == WinEventLog): 120 | columnName = getColumnName("WindowsEventID") 121 | observableValue=child._object.properties._fields["EID"].value 122 | if (type(child._object.properties) == Address): 123 | columnName = getColumnName("IPAddress") 124 | observableValue= child._object.properties 125 | if (type(child._object.properties) == File): 126 | try: 127 | for hash in child._object.properties.hashes: 128 | if re.match('^\w{40}',str(hash)): 129 | columnName = getColumnName("SHA1Hash") 130 | observableValue=hash 131 | if re.match('^\w{32}',str(hash)): 132 | columnName = getColumnName("MD5Hash") 133 | observableValue=hash 134 | except: 135 | error="don't worry about it, probably looking for a different value in field" 136 | searchString.append(buildSearchString(columnName,str(observableValue),condition)) 137 | except: 138 | if (type(observableList.observable._object.properties) == File): 139 | hash = observableList.observable.object_.properties.hashes[0].simple_hash_value.value 140 | if re.match('^\w{40}',str(hash)): 141 | columnName = getColumnName("SHA1Hash") 142 | observableValue=hash 143 | if re.match('^\w{32}',str(hash)): 144 | columnName = getColumnName("MD5Hash") 145 | observableValue=hash 146 | searchString.append(buildSearchString(columnName,str(observableValue),condition)) 147 | if (type(observableList.observable._object.properties) == Address): 148 | ipv4 = observableList.observable.object_.properties.address_value.value 149 | columnName = getColumnName("IPAddress") 150 | observableValue=ipv4 151 | condition=observableList.observable.object_.properties.address_value.condition 152 | searchString.append(buildSearchString(columnName,str(observableValue),str(condition))) 153 | if (type(observableList.observable._object.properties) == URI): 154 | uri = observableList.observable.object_.properties.value.value 155 | observableValue = uri 156 | columnName = getColumnName("URI") 157 | searchString.append(buildSearchString(columnName,str(observableValue),str(condition))) 158 | if (len(searchString) < 2): 159 | searchString.append("No supported observables found") 160 | return searchString 161 | 162 | -------------------------------------------------------------------------------- /idmap.config: -------------------------------------------------------------------------------- 1 | 98504:CVE-2014-6271 2 | 98516:CVE-2014-6271 3 | 98518:CVE-2014-6271 4 | 98520:CVE-2014-6271 5 | 99999:CVE-2015-1635 -------------------------------------------------------------------------------- /parseIP360.py: -------------------------------------------------------------------------------- 1 | ''' 2 | * Copyright (C) 2015 Tripwire, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ''' 16 | 17 | 18 | import xml.etree.ElementTree as ET 19 | import argparse, sys, os, shutil, re 20 | import TARDIS 21 | 22 | 23 | if __name__=="__main__": 24 | #os.chdir('C:\\TARDIS') 25 | os.chdir('/opt/tardis') 26 | 27 | #Get options from the command line... 28 | parser = argparse.ArgumentParser(description='TARDIS Threat Parser') 29 | parser.add_argument('-f', help='IP360 XML3 File', dest='file', required=True) 30 | args = parser.parse_args() 31 | 32 | #File must be in the Tripwire IP360 XML3 format 33 | file=args.file 34 | 35 | try: 36 | tree = ET.parse(file) 37 | root = tree.getroot() 38 | except: 39 | sys.exit("Not a valid XML file, use IP360 XML3 audit output") 40 | 41 | #Clear results folder to have a fresh starting point... 42 | if os.path.exists('Results'): 43 | shutil.rmtree('Results') 44 | 45 | numHosts=0 46 | 47 | for host in root.findall("./audit/hosts/host"): 48 | numHosts=numHosts+1 49 | directory='Results' 50 | #Create results directory to store the raw output 51 | if not os.path.exists(directory): 52 | os.makedirs(directory) 53 | #Get IP address to run threat search against 54 | for ip in host.findall("./ip"): 55 | sourceIP=ip.text 56 | #We like individual directories per IP 57 | if not os.path.exists(directory + '/' + sourceIP): 58 | os.makedirs(directory + '/' + sourceIP) 59 | for hostname in host.findall("./dnsName"): 60 | sourceHost=hostname.text 61 | for vulnerability in host.findall("./vulnerabilities/vulnerability"): 62 | internalVulnerabilityID=vulnerability.get('id') 63 | vulnName=internalVulnerabilityID 64 | #Convert internal vulnerability ID into a human readable name 65 | for line in open("idmap.config"): 66 | if internalVulnerabilityID in line: 67 | vulnName = re.sub('\d+\:', '', line) 68 | vulnName = re.sub('(\r\n|\r|\n)', '', vulnName) 69 | internalVulnerabilityID = vulnName 70 | numResults=TARDIS.main(vulnName, sourceIP, sourceHost) 71 | if numHosts<1: 72 | sys.exit("Not a valid XML file, use IP360 XML3 audit output") -------------------------------------------------------------------------------- /parseSTIX.py: -------------------------------------------------------------------------------- 1 | ''' 2 | * Copyright (C) 2015 Tripwire, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ''' 16 | 17 | 18 | import xml.etree.ElementTree as ET 19 | from stix.core import STIXPackage 20 | import argparse, sys, os, shutil, re, socket 21 | import TARDIS 22 | 23 | 24 | if __name__=="__main__": 25 | 26 | #Get options from the command line... 27 | parser = argparse.ArgumentParser(description='TARDIS Threat Parser') 28 | parser.add_argument('-f', help='STIX File', dest='file', required=True) 29 | parser.add_argument('-i', help='Vulnerable IP', dest='ip', required=True) 30 | parser.add_argument('-d', help='Vulnerable DNS Hostname', dest='hostname') 31 | args = parser.parse_args() 32 | 33 | file=args.file 34 | sourceIP = args.ip 35 | sourceHost = args.hostname 36 | cve='' 37 | vulnObject='' 38 | if not os.path.exists(file): 39 | print file + " does not exist." 40 | sys.exit() 41 | 42 | if (sourceHost is None): 43 | try: 44 | result = socket.gethostbyaddr(sourceIP) 45 | sourceHost = result[0] 46 | except: 47 | sourceHost = "" 48 | if (len(sourceHost) > 0): 49 | print ("File: " + file) 50 | print ("IP: " + sourceIP) 51 | print ("Host: " + sourceHost) 52 | 53 | if os.path.exists('Results'): 54 | shutil.rmtree('Results') 55 | directory='Results' 56 | #Create results directory to store the raw output 57 | if not os.path.exists(directory): 58 | os.makedirs(directory) 59 | if not os.path.exists(directory + '/' + sourceIP): 60 | os.makedirs(directory + '/' + sourceIP) 61 | 62 | #Get CVE from STIX 63 | stix_package = STIXPackage.from_xml(file) 64 | for target in stix_package.exploit_targets: 65 | for vuln in target.vulnerabilities: 66 | print "CVE: " + vuln.cve_id 67 | print "DESC:" + str(vuln.description) 68 | vulnObject=str(vuln.description) 69 | cve = vuln.cve_id 70 | if len(cve) > 0: 71 | if len(vulnObject) > 0: 72 | if not os.path.exists('VulnXML/' + vuln.cve_id + '.xml'): 73 | shutil.copyfile(file,'VulnXML/' + vuln.cve_id + '.xml') 74 | numResults=TARDIS.main(cve, vulnObject, sourceIP, sourceHost) 75 | else: 76 | print("Description missing from Exploit Target") 77 | else: 78 | print("CVE Missing from STIX File") 79 | 80 | else: 81 | print ("Unable to resolve hostname, please provide one with -d option") -------------------------------------------------------------------------------- /splunk.py: -------------------------------------------------------------------------------- 1 | ''' 2 | * Copyright (C) 2015 Tripwire, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ''' 16 | 17 | import os, re 18 | import splunklib.client as client 19 | import splunklib.results as results 20 | import xml.etree.ElementTree as ET 21 | 22 | def getSplunkService(): 23 | # Create a Splunk Service instance and log in 24 | try: 25 | configFile = 'config.xml' 26 | tree = ET.parse(configFile) 27 | root = tree.getroot() 28 | except: 29 | sys.exit("Not a valid XML file") 30 | for settings in root.findall("./splunk"): 31 | for ip in settings.findall("./ip"): 32 | splunkHOST=ip.text 33 | for ip in settings.findall("./adminport"): 34 | splunkAdminPORT=ip.text 35 | for ip in settings.findall("./user"): 36 | splunkUser=ip.text 37 | for ip in settings.findall("./password"): 38 | splunkPass=ip.text 39 | 40 | service = client.connect( 41 | host=splunkHOST, 42 | port=splunkAdminPORT, 43 | username=splunkUser, 44 | password=splunkPass) 45 | return service 46 | 47 | def searchVulnerability(searchString,vulnerability,sourceIP,sourceHost): 48 | directory='Results/'+sourceIP 49 | if not os.path.exists(directory): 50 | os.makedirs(directory) 51 | 52 | service = getSplunkService() 53 | 54 | # Get the collection of jobs 55 | jobs = service.jobs 56 | 57 | # Run a blocking search--search everything, return 1st 100 events 58 | kwargs_blockingsearch = {"exec_mode": "blocking"} 59 | 60 | # A blocking search returns the job's SID when the search is done 61 | job = jobs.create(searchString, **kwargs_blockingsearch) 62 | 63 | # Get properties of the job from Splunk 64 | #print "Search job properties" 65 | #print "Search job ID: ", job["sid"] 66 | #print "The number of events: ", job["eventCount"] 67 | #print "The number of results:", job["resultCount"] 68 | #print "Search duration: ", job["runDuration"], "seconds" 69 | #print "This job expires in: ", job["ttl"], "seconds" 70 | 71 | # Prints a parsed, formatted CSV stream to a file 72 | 73 | numResults = job["resultCount"] 74 | if numResults=="0": 75 | return numResults 76 | else: 77 | 78 | result_stream = job.results(**{"output_mode": "json"}) 79 | 80 | return result_stream 81 | 82 | def searchVulnerabilityTimeRange(searchString,vulnerability,sourceIP,sourceHost,earliest,latest): 83 | directory='Results/'+sourceIP 84 | if not os.path.exists(directory): 85 | os.makedirs(directory) 86 | 87 | service = getSplunkService() 88 | 89 | # Get the collection of jobs 90 | jobs = service.jobs 91 | 92 | # Run a blocking search--search everything, return 1st 100 events 93 | kwargs_blockingsearch = {"exec_mode": "blocking", "earliest_time": earliest, "latest_time": latest,} 94 | 95 | # A blocking search returns the job's SID when the search is done 96 | job = jobs.create(searchString, **kwargs_blockingsearch) 97 | 98 | # Get properties of the job from Splunk 99 | #print "Search job properties" 100 | #print "Search job ID: ", job["sid"] 101 | #print "The number of events: ", job["eventCount"] 102 | #print "The number of results:", job["resultCount"] 103 | #print "Search duration: ", job["runDuration"], "seconds" 104 | #print "This job expires in: ", job["ttl"], "seconds" 105 | 106 | # Prints a parsed, formatted CSV stream to a file 107 | 108 | numResults = job["resultCount"] 109 | if numResults=="0": 110 | return numResults 111 | else: 112 | result_stream = job.results(**{"output_mode": "json"}) 113 | return result_stream 114 | -------------------------------------------------------------------------------- /sql_tardis.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE TARDIS; 2 | 3 | use TARDIS; 4 | 5 | CREATE TABLE attackInventory( 6 | attack_id INT NOT NULL AUTO_INCREMENT, 7 | attacker_ip INT(11) UNSIGNED, 8 | attacker_host VARCHAR(255), 9 | attacker_user VARCHAR(255), 10 | victim_ip INT(11) UNSIGNED, 11 | victim_host VARCHAR(255), 12 | attack_time VARCHAR(255) NOT NULL, 13 | attack_log VARCHAR(2048) NOT NULL, 14 | threat_id VARCHAR(255) NOT NULL, 15 | PRIMARY KEY ( attack_id ) 16 | ); 17 | 18 | CREATE TABLE assetVulnerabilities( 19 | victim_ip INT(11) NOT NULL , 20 | threat_id VARCHAR(255) NOT NULL, 21 | active VARCHAR(255) NOT NULL, 22 | PRIMARY KEY ( victim_ip, threat_id ) 23 | ); 24 | --------------------------------------------------------------------------------