├── README.md ├── nessus2es.config.example ├── nessus2es.py ├── nessus2wazuh.py ├── nessusdata.template.json └── quick_graph.png /README.md: -------------------------------------------------------------------------------- 1 | # nessus2es 2 | Send your nessus compliance and vulnerability scan data to ElasticSearch from a file. 3 | To download a file from Nessus without using an API (Nessus7) see https://github.com/Ar0xA/Nessus7proxy 4 | 5 | Tested with ElasticSearch 6 and Nessus 7 only! 6 | 7 | note: if you change the default indexname, be sure to apply the template to the new index too. 8 | 9 | note: assumes the timezone of your nessus server is the same timezone this script runs in 10 | 11 | #required 12 | - python 3 13 | - configparser 14 | - objdict 15 | - python3-dateutil 16 | 17 | # possible elastic queries 18 | - Show me all systems with CRITICAL vulnerabilities that have public exploits available. 19 | - Show me all compliance/vulnerability results of a system over a period of time. 20 | - Show me all systems with a CVSS score of 7 or higher. 21 | - Show me all systems of a certain OS where we log in locally to do vulnerability scanning. 22 | - Show me all systems that are vulnerable for CVE-2014-0160 23 | - etc., etc. 24 | 25 | 26 | # example kibana output 27 | This quick graph shows a set of vulnerability and compliance scan results 28 | 29 | 30 | 31 | # License 32 | This is "whatever"-ware. You can't hold me liable for anything but you can do whatever you like with this code. Credit would be appreciated. 33 | -------------------------------------------------------------------------------- /nessus2es.config.example: -------------------------------------------------------------------------------- 1 | #config file example for nessus2es 2 | #using the config file overwrites ALL other cli commands 3 | 4 | [General] 5 | #input file in .nessus format 6 | #either this or Input need to be defined 7 | Input: 8 | 9 | # {both,vulnerability,compliance} 10 | Type: both 11 | 12 | # Do everything but actually send data to Zabbix 13 | # False = send data for real, True = do NOT send data 14 | Fake: False 15 | 16 | [elasticsearch] 17 | elasticsearchServer: 127.0.0.1 18 | elasticsearchPort: 9200 19 | -------------------------------------------------------------------------------- /nessus2es.py: -------------------------------------------------------------------------------- 1 | #This takes as input a .nessus scan file with either vulnerability or compliance info (or both) 2 | #and dumps the data into elasticsearc 3 | # 4 | #autor: @Ar0xA / ar0xa@tldr.nu 5 | # 6 | #note: assumes timezone on nessus scanner and this script are the same! 7 | 8 | from bs4 import BeautifulSoup 9 | 10 | import argparse 11 | import sys 12 | import os 13 | import io 14 | import json 15 | import configparser 16 | import urllib3 17 | import time 18 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 19 | 20 | from objdict import ObjDict 21 | from dateutil.parser import parse 22 | from datetime import timedelta 23 | 24 | #check if index exists 25 | def ES_index_check (args,task_id): 26 | #what index to we need to post to? 27 | #can we reach the server? 28 | es_server = args.elasticsearchserver 29 | es_port = args.elasticsearchport 30 | es_index = args.elasticsearchindex 31 | es_url = "http://" + es_server + ":" + str(es_port) + "/" 32 | 33 | #construct indexname 34 | es_index = es_index + "-" + task_id 35 | 36 | #test if index esists 37 | http = urllib3.PoolManager() 38 | r = http.request('HEAD', es_url+es_index) 39 | 40 | #if index exists, there's already data from this task_id in ES. Quit 41 | #if index DOES NOT exist, create it. 42 | if r.status == 404: 43 | print ("Index \"%s\" does not exist. Creating it" % (es_index)) 44 | r = http.request('PUT', es_url +es_index) 45 | if r.status == 200: 46 | print ("Index %s created" % (es_index)) 47 | else: 48 | print ("Creating index failed. I give up") 49 | sys.exit(1) 50 | elif r.status == 200: 51 | print ("Index already exists. Not inserting same data into this index unless you override") 52 | print ("TODO: create override") 53 | sys.exit(1) 54 | elif not r.status == 200: 55 | print ("Something is wrong with the index, but i have no idea what. I give up!") 56 | print (r.status) 57 | sys.exit(1) 58 | 59 | #post data to elastic 60 | def post_to_ES(json_data,args, task_id): 61 | #what index to we need to post to? 62 | #can we reach the server? 63 | es_server = args.elasticsearchserver 64 | es_port = args.elasticsearchport 65 | es_index = args.elasticsearchindex 66 | es_url = "http://" + es_server + ":" + str(es_port) + "/" 67 | 68 | #construct indexname 69 | es_index = es_index + "-" + task_id 70 | http = urllib3.PoolManager() 71 | 72 | #index exists, lets post the data #yolo 73 | r = http.request('POST', es_url+es_index+"/vulnresult", headers={'Content-Type':'application/json'}, body=json_data) 74 | if not r.status == 201: 75 | print ("well, something went wrong, thats embarrasing") 76 | 77 | sys.exit(1) 78 | 79 | #here we parse results from the nessus file, we extract the vulnerabiltiy information 80 | # we create a host, where we have general data and findings. 81 | # General date is always there, findings can be none, one or many 82 | # Some items in teh findings are always there, some are optional. 83 | # The optional ones have some which can be arrays 84 | 85 | def parse_to_json(nessus_xml_data, args): 86 | 87 | #some quick report checking 88 | data =ObjDict() 89 | 90 | tmp_scanname = nessus_xml_data.report['name'] 91 | if len(tmp_scanname) == 0: 92 | print ('Didn\'t find report name in file. is this a valid nessus file?') 93 | sys.exit(1) 94 | else: 95 | data.scanname = tmp_scanname 96 | 97 | #policyused 98 | data.scanpolicy = nessus_xml_data.policyname.get_text() 99 | 100 | # see if there are any hosts that are reported on 101 | hosts = nessus_xml_data.findAll('reporthost') 102 | if len(hosts) == 0: 103 | print ('Didn\'t find any hosts in file. Is this a valid nessus file?') 104 | sys.exit(1) 105 | else: 106 | print ('Found %i hosts' % (len(hosts))) 107 | 108 | #find the Task ID for uniqueness checking 109 | #test: is this unique per RUN..or per task? 110 | task_id = "" 111 | tmp_prefs = nessus_xml_data.findAll('preference') 112 | for pref in tmp_prefs: 113 | if "report_task_id" in str(pref): 114 | task_id = pref.value.get_text() 115 | 116 | #ok we got the task ID; to be sure before anything else, lets see if the index already exists or not 117 | if not args.fake: 118 | ES_index_check (args, task_id) 119 | 120 | print ("Checking for results and posting to ElasticSearch. This might take a while...") 121 | for host in hosts: 122 | #lets iterate through the reportItem, here the compliance items will be 123 | reportItems = host.findAll('reportitem') 124 | for rItem in reportItems: 125 | host_info = ObjDict() 126 | #host_info.reportfindings = [] 127 | #lets get the host information 128 | host_info.hostname = host['name'] 129 | 130 | host_info.hostip = host.find('tag', attrs={'name': 'host-ip'}).get_text() 131 | macaddress = host.find('tag', attrs={'name': 'mac-address'}) 132 | if macaddress: 133 | host_info.hostmacaddress = macaddress.get_text() 134 | else: 135 | host_info.hostmacaddress = None 136 | 137 | credscan = host.find('tag', attrs={'name': 'Credentialed_Scan'}) 138 | if credscan: 139 | host_info.credentialedscan = credscan.get_text() 140 | else: 141 | host_info.credentialedscan = None 142 | 143 | host_info.hostscanstart = host.find('tag', attrs={'name': 'HOST_START'}).get_text() 144 | #convert to normal date format 145 | host_info.hostscanstart = parse(host_info.hostscanstart) 146 | #convert to UTC time 147 | timeoffset = int((time.localtime().tm_gmtoff)/3600) 148 | host_info.hostscanstart =host_info.hostscanstart - timedelta(hours=timeoffset) 149 | 150 | host_info.hostscanend = host.find('tag', attrs={'name': 'HOST_END'}).get_text() 151 | host_info.hostscanend = parse(host_info.hostscanend) 152 | host_info.hostscanend = host_info.hostscanend - timedelta(hours=timeoffset) 153 | host_info["@timestamp"] = host_info.hostscanend 154 | 155 | #fqdn might be optional 156 | host_fqdn = host.find('tag', attrs={'name': 'host-fqdn'}) 157 | if host_fqdn: 158 | host_info.hostfqdn = host_fqdn.get_text() 159 | else: 160 | host_info.hostfqdn = None 161 | 162 | #get all report findings info 163 | try: 164 | #these fields should always be present 165 | host_info.severity = rItem['severity'] 166 | host_info.port = rItem['port'] 167 | host_info.svc_name = rItem['svc_name'] 168 | host_info.protocol = rItem['protocol'] 169 | host_info.pluginid = rItem['pluginid'] 170 | host_info.pluginname = rItem['pluginname'] 171 | host_info.plugintype = rItem.find('plugin_type').get_text() 172 | host_info.pluginfamily = rItem['pluginfamily'] 173 | host_info.riskfactor = rItem.find('risk_factor').get_text() 174 | agent = rItem.find('agent') 175 | 176 | if agent: 177 | host_info.agent = agent.get_text() 178 | else: 179 | host_info.agent = None 180 | 181 | compliance_item = rItem.find('compliance') 182 | if compliance_item: 183 | host_info.compliance = True 184 | else: 185 | host_info.compliance = False 186 | 187 | #this stuff only around when its a compliance scan anyway 188 | host_info.compliancecheckname = None 189 | host_info.complianceauditfile = None 190 | host_info.complianceinfo = None 191 | host_info.complianceresult = None 192 | #host_info.compliancereference = None 193 | host_info.complianceseealso = None 194 | 195 | 196 | comaudit = rItem.find('cm:compliance-audit-file') 197 | if comaudit: 198 | host_info.complianceauditfile = comaudit.get_text() 199 | else: 200 | host_info.complianceauditfile = None 201 | 202 | comcheck = rItem.find('cm:compliance-check-name') 203 | if comcheck: 204 | host_info.compliancecheckname = comcheck.get_text() 205 | else: 206 | host_info.compliancecheckname = None 207 | 208 | cominfo = rItem.find('cm:compliance-info') 209 | if cominfo: 210 | host_info.complianceinfo = cominfo.get_text() 211 | else: 212 | host_info.complianceinfo = None 213 | 214 | comsee = rItem.find('cm:compliance-see-also') 215 | if comsee: 216 | host_info.complianceseealso = comsee.get_text() 217 | else: 218 | host_info.complianceseealso = None 219 | 220 | comref = rItem.find('cm:compliance-reference') 221 | #host_info.compliancereference['LEVEL']= ObjDict() 222 | 223 | if comref: 224 | host_info.compliancereference = ObjDict() 225 | 226 | compliancereference = comref.get_text().split(",") 227 | for ref in compliancereference: 228 | comprefsplit = ref.split("|") 229 | host_info.compliancereference[comprefsplit[0]] = ObjDict() 230 | host_info.compliancereference[comprefsplit[0]] =comprefsplit[1] 231 | else: 232 | host_info.compliancereference = None 233 | 234 | comres = rItem.find('cm:compliance-result') 235 | if comres: 236 | host_info.complianceresult = comres.get_text() 237 | else: 238 | host_info.complianceresult = None 239 | 240 | descrip = rItem.find('description') 241 | if descrip: 242 | host_info.description = descrip.get_text() 243 | else: 244 | host_info.description = None 245 | 246 | synop = rItem.find('synopsis') 247 | if synop: 248 | host_info.synopsis = synop.get_text() 249 | else: 250 | host_info.synopsis = None 251 | 252 | solut = rItem.find('solution') 253 | if solut: 254 | host_info.solution = solut.get_text() 255 | else: 256 | host_info.solution = None 257 | 258 | plugin_output = rItem.find('plugin_output') 259 | if plugin_output: 260 | host_info.pluginoutput = plugin_output.get_text() 261 | else: 262 | host_info.pluginoutput = None 263 | 264 | expl_avail = rItem.find('exploit_available') 265 | if expl_avail: 266 | host_info.exploitavailable = expl_avail.get_text() 267 | else: 268 | host_info.exploitavailable = None 269 | 270 | expl_ease = rItem.find('exploitability_ease') 271 | if expl_ease: 272 | host_info.exploitabilityease = expl_ease.get_text() 273 | else: 274 | host_info.exploitabilityease = None 275 | 276 | cvss = rItem.find('cvss_base_score') 277 | if cvss: 278 | host_info.cvssbasescore = cvss.get_text() 279 | else: 280 | host_info.cvssbasescore = None 281 | 282 | cvss3 = rItem.find('cvss3_base_score') 283 | if cvss3: 284 | host_info.cvss3basescore = cvss3.get_text() 285 | else: 286 | host_info.cvss3basescore = None 287 | 288 | ppdate = rItem.find('patch_publication_date') 289 | if ppdate: 290 | host_info.patchpublicationdate = parse(ppdate.get_text()) 291 | else: 292 | host_info.patchpublicationdate = None 293 | 294 | #these items can be none, one or many if found 295 | host_info.cve = [] 296 | host_info.osvdb = [] 297 | host_info.rhsa = [] 298 | host_info.xref = [] 299 | 300 | allcve = rItem.findAll('cve') 301 | if allcve: 302 | for cve in allcve: 303 | host_info.cve.append(cve.get_text()) 304 | 305 | allosvdb = rItem.findAll('osvdb') 306 | if allosvdb: 307 | for osvdb in allosvdb: 308 | host_info.osvdb.append(osvdb.get_text()) 309 | 310 | 311 | allrhsa = rItem.findAll('rhsa') 312 | if allrhsa: 313 | for rhsa in allrhsa: 314 | host_info.rhsa.append(rhsa.get_text()) 315 | 316 | allxref = rItem.findAll('xref') 317 | if allxref: 318 | for xref in allxref: 319 | host_info.xref.append(xref.get_text()) 320 | 321 | #we have all data in host_info, why not send that instead? 322 | #print ("Finding for %s complete, sending to ES" % (host_info.hostname)) 323 | json_data = host_info.dumps() 324 | #print (json_data) 325 | if not args.fake: 326 | post_to_ES(json_data, args, task_id) 327 | except Exception as e: 328 | print ("Error:") 329 | print (e) 330 | print (rItem) 331 | sys.exit(1) 332 | 333 | def parse_args(): 334 | parser = argparse.ArgumentParser(description = 'Push data into elasticsearch from a .nessus result file.') 335 | group = parser.add_mutually_exclusive_group(required=True) 336 | group.add_argument('-i', '--input', help = 'Input file in .nessus format', 337 | default = None) 338 | parser.add_argument('-es', '--elasticsearchserver', help = 'elasticsearch server', 339 | default = '127.0.0.1') 340 | parser.add_argument('-ep', '--elasticsearchport', help = 'elasticsearch port', 341 | default = 9200) 342 | parser.add_argument('-ei','--elasticsearchindex', help='What index to post the report data to', 343 | default = 'nessusdata') 344 | parser.add_argument('-t', '--type', help = 'What type of result to parse the file for.', choices = ['both', 'vulnerability','compliance' ], 345 | default = 'both') 346 | parser.add_argument('-f','--fake', help = 'Do everything but actually send data to elasticsearch', action = 'store_true') 347 | group.add_argument('-c', '--config', help = 'Config file for script to read settings from. Overwrites all other cli parameters', default = None) 348 | args = parser.parse_args() 349 | return args 350 | 351 | #replace args from config file instead 352 | def replace_args(args): 353 | if os.path.isfile(args.config): 354 | print ("Reading configuration from config file") 355 | Config = ConfigParser.ConfigParser() 356 | try: 357 | Config.read(args.config) 358 | args.input = Config.get("General","Input") 359 | args.type = Config.get("General","Type") 360 | args.fake = Config.getboolean("General","Fake") 361 | args.elasticsearchserver = Config.get("elasticsearch","elasticsearchServer") 362 | args.elasticsearchport = Config.getint("elasticsearch","elasticsearchPort") 363 | except IOError: 364 | print('could not read config file "' + args.config + '".') 365 | sys.exit(1) 366 | else: 367 | print('"' + args.config + '" is not a valid file.') 368 | sys.exit(1) 369 | return args 370 | 371 | def main(): 372 | args = parse_args() 373 | 374 | #do we have a config file instead of cli? 375 | if args.config: 376 | args = replace_args(args) 377 | 378 | #ok, if not 379 | if (not args.input) and (not args.nessusscanname): 380 | print('Need input file to export. Specify one in the configuation file, with -i (file) or -rn (reportname)\n See -h for more info') 381 | sys.exit(1) 382 | 383 | if args.input: 384 | nessus_scan_file = args.input 385 | else: 386 | nessus_scan_file = args.nessustmp + "/" + args.nessusscanname 387 | print ("Nessus file to parse is %s" % (nessus_scan_file)) 388 | 389 | # read the file..might be big though... 390 | with open(nessus_scan_file, 'r') as f: 391 | print ('Parsing file %s as xml into memory, hold on...' % (args.input)) 392 | nessus_xml_data = BeautifulSoup(f.read(), 'lxml') 393 | 394 | parse_to_json(nessus_xml_data, args) 395 | 396 | if __name__ == "__main__": 397 | main() 398 | print ("Done.") 399 | -------------------------------------------------------------------------------- /nessus2wazuh.py: -------------------------------------------------------------------------------- 1 | #This takes as input a .nessus scan file with either vulnerability or compliance info (or both) 2 | #and dumps the data into elasticsearc into wazuh index 3 | # 4 | #autor: @Ar0xA / ar0xa@tldr.nu 5 | # 6 | #note: assumes timezone on nessus scanner and this script are the same! 7 | 8 | from bs4 import BeautifulSoup 9 | 10 | import argparse 11 | import sys 12 | import os 13 | import io 14 | import json 15 | import configparser 16 | import urllib3 17 | import time 18 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 19 | 20 | from objdict import ObjDict 21 | from dateutil.parser import parse 22 | from datetime import timedelta 23 | 24 | #check if index exists 25 | def ES_index_check (args): 26 | #what index to we need to post to? 27 | #can we reach the server? 28 | es_server = args.elasticsearchserver 29 | es_port = args.elasticsearchport 30 | es_index = args.elasticsearchindex 31 | es_url = "http://" + es_server + ":" + str(es_port) + "/" 32 | 33 | #construct indexname 34 | year, month, day, hour, minute = time.strftime("%Y,%m,%d,%H,%M").split(',') 35 | es_index = es_index + "-" + year + "." + month +"."+ day 36 | 37 | #test if index esists 38 | http = urllib3.PoolManager() 39 | r = http.request('HEAD', es_url+es_index) 40 | 41 | #we need the existing index 42 | if r.status == 404: 43 | print ("Index %s does not exist, sorry. quitting." % (es_index)) 44 | sys.exit(1) 45 | elif r.status == 200: 46 | print ("Index already exists. Lets insert our data!") 47 | 48 | elif not r.status == 200: 49 | print ("Something is wrong with the index, but i have no idea what. I give up!") 50 | print (r.status) 51 | sys.exit(1) 52 | 53 | #we retreive the agent.id from the /var/ossec/etc/client.keys 54 | def get_id_from_keys(args, hostip): 55 | try: 56 | with open(args.ossec,'r') as keysfile: 57 | filedata = keysfile.readlines() 58 | for line in filedata: 59 | if hostip in line: 60 | #if we find it, we need the first number 61 | return line.split(" ",1)[0] 62 | except: 63 | print ("Cant open %s. am I in the correct group or on the right system?" % (args.ossec)) 64 | sys.exit(1) 65 | return False 66 | 67 | #post data to elastic 68 | def post_to_ES(json_data,args, task_id): 69 | #what index to we need to post to? 70 | #can we reach the server? 71 | es_server = args.elasticsearchserver 72 | es_port = args.elasticsearchport 73 | es_index = args.elasticsearchindex 74 | es_url = "http://" + es_server + ":" + str(es_port) + "/" 75 | 76 | #construct indexname 77 | year, month, day, hour, minute = time.strftime("%Y,%m,%d,%H,%M").split(',') 78 | es_index = es_index + "-" + year + "." + month +"."+ day 79 | 80 | http = urllib3.PoolManager() 81 | 82 | #index exists, lets post the data #yolo 83 | 84 | r = http.request('POST', es_url+es_index+"/wazuh", headers={'Content-Type':'application/json'}, body=json_data) 85 | if not r.status == 201: 86 | print ("well, something went wrong, thats embarrasing") 87 | print (r.status) 88 | print (r.reason) 89 | sys.exit(1) 90 | 91 | #here we parse results from the nessus file, we extract the vulnerabiltiy information 92 | # we create a host, where we have general data and findings. 93 | # General date is always there, findings can be none, one or many 94 | # Some items in teh findings are always there, some are optional. 95 | # The optional ones have some which can be arrays 96 | 97 | def parse_to_json(nessus_xml_data, args): 98 | 99 | #some quick report checking 100 | data =ObjDict() 101 | 102 | tmp_scanname = nessus_xml_data.report['name'] 103 | if len(tmp_scanname) == 0: 104 | print ('Didn\'t find report name in file. is this a valid nessus file?') 105 | sys.exit(1) 106 | else: 107 | data.scanname = tmp_scanname 108 | 109 | #policyused 110 | data.scanpolicy = nessus_xml_data.policyname.get_text() 111 | 112 | # see if there are any hosts that are reported on 113 | hosts = nessus_xml_data.findAll('reporthost') 114 | if len(hosts) == 0: 115 | print ('Didn\'t find any hosts in file. Is this a valid nessus file?') 116 | sys.exit(1) 117 | else: 118 | print ('Found %i hosts' % (len(hosts))) 119 | 120 | #find the Task ID for uniqueness checking 121 | #test: is this unique per RUN..or per task? 122 | task_id = "" 123 | tmp_prefs = nessus_xml_data.findAll('preference') 124 | for pref in tmp_prefs: 125 | if "report_task_id" in str(pref): 126 | task_id = pref.value.get_text() 127 | 128 | #Lets see if the index already exists or not 129 | if not args.fake: 130 | ES_index_check (args) 131 | 132 | print ("Checking for results and posting to ElasticSearch. This might take a while...") 133 | for host in hosts: 134 | #lets iterate through the reportItem, here the compliance items will be 135 | reportItems = host.findAll('reportitem') 136 | for rItem in reportItems: 137 | host_info = ObjDict() 138 | host_info.agent = ObjDict() 139 | host_info.rule = ObjDict() 140 | host_info.data = ObjDict() 141 | host_info.manager = ObjDict() 142 | 143 | #TODO make configurable to OSSEC server 144 | host_info.manager.name = "debian" 145 | #lets get the host information 146 | #host_info.hostname = host['name'] 147 | host_info.agent.ip = host.find('tag', attrs={'name': 'host-ip'}).get_text() 148 | #we got the IP...from here we need the agent.id 149 | agent_id = get_id_from_keys(args,host_info.agent.ip) 150 | host_info.agent.id = agent_id 151 | if agent_id: 152 | #default fields to make wazuh work 153 | #host_info.rule.groups = "oscap, oscap-result" 154 | host_info.rule.groups = "oscap" 155 | host_info.location = "nessus_CIS-benchmark" 156 | timeoffset = int((time.localtime().tm_gmtoff)/3600) 157 | hostscanend = host.find('tag', attrs={'name': 'HOST_END'}).get_text() 158 | hostscanend = parse(hostscanend) 159 | hostscanend = hostscanend - timedelta(hours=timeoffset) 160 | host_info["@timestamp"] = hostscanend.strftime("%Y-%m-%dT%H:%M:%S") 161 | 162 | #fqdn might be optional 163 | host_fqdn = host.find('tag', attrs={'name': 'host-fqdn'}) 164 | host_info.predecoder = ObjDict() 165 | if host_fqdn: 166 | host_info.predecoder.hostname = host_fqdn.get_text() 167 | else: 168 | host_info.predecoder.hostname = host_info.agent.ip 169 | 170 | #get all report findings info 171 | host_info.data.oscap = ObjDict() 172 | host_info.data.oscap.check = ObjDict() 173 | host_info.data.oscap.scan = ObjDict() 174 | host_info.data.oscap.check.oval = ObjDict() 175 | host_info.data.oscap.scan.benchmark = ObjDict() 176 | host_info.data.oscap.scan.profile = ObjDict() 177 | 178 | #a limitation of nessus is how severity is done in CIS benchmarking compared to oscap 179 | #a benchmark scan in NEssus is always a severity 3, 180 | #a missing warning because of wrong OS is 2 181 | try: 182 | severity = rItem['severity'] 183 | if severity == "0": 184 | host_info.data.oscap.check.severity = "informational" 185 | host_info.rule.level = 3 186 | elif severity == "1": 187 | print ("Severity 1 shouldn't happen with CIS benchmark scans!") 188 | sys.exit(1) 189 | # host_info.data.oscap.check.severity = "low" 190 | # host_info.rule.level = 5 191 | elif severity == "2": #warnings 192 | host_info.data.oscap.check.severity = "low" 193 | host_info.rule.level = 7 194 | elif severity == "3": 195 | host_info.data.oscap.check.severity = "medium" 196 | host_info.rule.level = 10 197 | else: 198 | print ("unknown severity: %s" % (severity)) 199 | sys.exit(1) 200 | 201 | host_info.data.oscap.scan.id = task_id 202 | #these fields should always be present 203 | host_info.data.oscap.check.oval.id = rItem['pluginid'] 204 | #host_info.data.oscap.scan.benchmark.id = rItem['pluginname'] #not needed for wazuh report screen 205 | host_info.data.oscap.scan.profile.title = rItem['pluginname'] 206 | 207 | compliance_item = rItem.find('compliance') 208 | 209 | #we're only interested in compliance items, really 210 | if compliance_item: 211 | #this stuff only around when its a compliance scan anyway 212 | 213 | comaudit = rItem.find('cm:compliance-audit-file') 214 | if comaudit: 215 | #host_info.data.oscap.check.id = comaudit.get_text() 216 | #host_info.data.oscap.scan.profile.id = comaudit.get_text() 217 | host_info.data.oscap.scan.content = comaudit.get_text() 218 | else: 219 | #host_info.data.oscap.check.id = None 220 | #host_info.data.oscap.scan.profile.id = None 221 | host_info.data.oscap.scan.content = None 222 | 223 | comcheck = rItem.find('cm:compliance-check-name') 224 | if comcheck: 225 | #host_info.data.oscap.check.description = comcheck.get_text() 226 | host_info.data.oscap.check.title = comcheck.get_text() 227 | else: 228 | #host_info.data.oscap.check.description = comcheck.get_text() 229 | host_info.data.oscap.check.title = comcheck.get_text() 230 | 231 | cominfo = rItem.find('cm:compliance-info') 232 | if cominfo: 233 | host_info.data.oscap.check.rationale = cominfo.get_text().replace("\n","") 234 | else: 235 | host_info.data.oscap.check.rationale = None 236 | 237 | 238 | comref = rItem.find('cm:compliance-reference') 239 | if comref: 240 | host_info.data.oscap.check.references = comref.get_text() 241 | else: 242 | host_info.data.oscap.check.references = None 243 | 244 | comres = rItem.find('cm:compliance-result') 245 | if comres: 246 | complianceresult = comres.get_text() 247 | if complianceresult == "PASSED": 248 | host_info.data.oscap.check.result = "pass" 249 | elif complianceresult == "FAILED": 250 | host_info.data.oscap.check.result = "fail" 251 | elif complianceresult == "WARNING": 252 | host_info.data.oscap.check.result = "informational" 253 | else: 254 | print ("unknown compliance result:") 255 | print (complianceresult) 256 | sys.exit(1) 257 | else: 258 | host_info.complianceresult = None 259 | 260 | #both compliance and vuln scan 261 | descrip = rItem.find('description') 262 | if descrip: 263 | host_info.full_log = descrip.get_text() 264 | else: 265 | host_info.full_log = None 266 | 267 | #we have all data in host_info, why not send that instead? 268 | #print ("Finding for %s complete, sending to ES" % (host_info.hostname)) 269 | json_data = host_info.dumps() 270 | #print (json_data) 271 | if not args.fake: 272 | post_to_ES(json_data, args, task_id) 273 | #sys.exit(1) 274 | except Exception as e: 275 | print ("Error:") 276 | print (e) 277 | print (rItem) 278 | sys.exit(1) 279 | 280 | def parse_args(): 281 | parser = argparse.ArgumentParser(description = 'Push data into elasticsearch from a .nessus result file.') 282 | group = parser.add_mutually_exclusive_group(required=True) 283 | group.add_argument('-i', '--input', help = 'Input file in .nessus format', 284 | default = None) 285 | parser.add_argument('-o', '--ossec', help = 'Ossec key file', 286 | default = '/var/ossec/etc/client.keys') 287 | parser.add_argument('-es', '--elasticsearchserver', help = 'elasticsearch server', 288 | default = '127.0.0.1') 289 | parser.add_argument('-ep', '--elasticsearchport', help = 'elasticsearch port', 290 | default = 9200) 291 | parser.add_argument('-ei','--elasticsearchindex', help='What index to post the report data to', 292 | default = 'wazuh-alerts-3.x') 293 | parser.add_argument('-t', '--type', help = 'What type of result to parse the file for.', choices = ['both', 'vulnerability','compliance' ], 294 | default = 'both') 295 | parser.add_argument('-f','--fake', help = 'Do everything but actually send data to elasticsearch', action = 'store_true') 296 | group.add_argument('-c', '--config', help = 'Config file for script to read settings from. Overwrites all other cli parameters', default = None) 297 | args = parser.parse_args() 298 | return args 299 | 300 | #replace args from config file instead 301 | def replace_args(args): 302 | if os.path.isfile(args.config): 303 | print ("Reading configuration from config file") 304 | Config = ConfigParser.ConfigParser() 305 | try: 306 | Config.read(args.config) 307 | args.input = Config.get("General","Input") 308 | args.type = Config.get("General","Type") 309 | args.fake = Config.getboolean("General","Fake") 310 | args.ossec = Config.get("General", "OSSEC") 311 | args.elasticsearchserver = Config.get("elasticsearch","elasticsearchServer") 312 | args.elasticsearchport = Config.getint("elasticsearch","elasticsearchPort") 313 | except IOError: 314 | print('could not read config file "' + args.config + '".') 315 | sys.exit(1) 316 | else: 317 | print('"' + args.config + '" is not a valid file.') 318 | sys.exit(1) 319 | return args 320 | 321 | def main(): 322 | args = parse_args() 323 | 324 | #do we have a config file instead of cli? 325 | if args.config: 326 | args = replace_args(args) 327 | 328 | #ok, if not 329 | if (not args.input) and (not args.nessusscanname): 330 | print('Need input file to export. Specify one in the configuation file, with -i (file) or -rn (reportname)\n See -h for more info') 331 | sys.exit(1) 332 | 333 | if args.input: 334 | nessus_scan_file = args.input 335 | else: 336 | nessus_scan_file = args.nessustmp + "/" + args.nessusscanname 337 | print ("Nessus file to parse is %s" % (nessus_scan_file)) 338 | 339 | # read the file..might be big though... 340 | with open(nessus_scan_file, 'r') as f: 341 | print ('Parsing file %s as xml into memory, hold on...' % (args.input)) 342 | nessus_xml_data = BeautifulSoup(f.read(), 'lxml') 343 | 344 | parse_to_json(nessus_xml_data, args) 345 | 346 | if __name__ == "__main__": 347 | main() 348 | print ("Done.") 349 | -------------------------------------------------------------------------------- /nessusdata.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": 0, 3 | "version": 2, 4 | "index_patterns": [ 5 | "nessusdata-*" 6 | ], 7 | "settings": {}, 8 | "mappings": { 9 | "_default_": { 10 | "dynamic_templates": [ 11 | { 12 | "string_as_keyword": { 13 | "match_mapping_type": "string", 14 | "mapping": { 15 | "type": "keyword", 16 | "doc_values": "true" 17 | } 18 | } 19 | } 20 | ], 21 | "properties": { 22 | "compliancereference": { 23 | "properties": { 24 | "800-53": { 25 | "type": "keyword", 26 | "doc_values": "true" 27 | }, 28 | "ISO/IEC-27001": { 29 | "type": "keyword", 30 | "doc_values": "true" 31 | }, 32 | "PCI-DSSv3.1": { 33 | "type": "keyword", 34 | "doc_values": "true" 35 | }, 36 | "PCI-DSSv3.2": { 37 | "type": "keyword", 38 | "doc_values": "true" 39 | }, 40 | "800-171": { 41 | "type": "keyword", 42 | "doc_values": "true" 43 | }, 44 | "CSF": { 45 | "type": "keyword", 46 | "doc_values": "true" 47 | }, 48 | "ITSG-33": { 49 | "type": "keyword", 50 | "doc_values": "true" 51 | }, 52 | "SWIFT-CSCv1": { 53 | "type": "keyword", 54 | "doc_values": "true" 55 | }, 56 | "CSCv6": { 57 | "type": "keyword", 58 | "doc_values": "true" 59 | }, 60 | "TBA-FIISB": { 61 | "type": "keyword", 62 | "doc_values": "true" 63 | }, 64 | "CN-L3": { 65 | "type": "keyword", 66 | "doc_values": "true" 67 | }, 68 | "SIP": { 69 | "type": "keyword", 70 | "doc_values": "true" 71 | }, 72 | "HIPAA": { 73 | "type": "keyword", 74 | "doc_values": "true" 75 | }, 76 | "LEVEL": { 77 | "type": "keyword", 78 | "doc_values": "true" 79 | } 80 | } 81 | }, 82 | "pluginname": { 83 | "type": "text" 84 | }, 85 | "hostip": { 86 | "type": "ip" 87 | }, 88 | "complianceresult": { 89 | "type": "text" 90 | }, 91 | "description": { 92 | "type": "text", 93 | "fields": { 94 | "keyword": { 95 | "ignore_above": 256, 96 | "type": "keyword" 97 | } 98 | } 99 | }, 100 | "complianceinfo": { 101 | "type": "text", 102 | "fields": { 103 | "keyword": { 104 | "ignore_above": 256, 105 | "type": "keyword" 106 | } 107 | } 108 | }, 109 | "hostscanstart": { 110 | "format": "yyyy-MM-dd HH:mm:ss", 111 | "type": "date" 112 | }, 113 | "plugintype": { 114 | "type": "text" 115 | }, 116 | "complianceseealso": { 117 | "type": "text", 118 | "fields": { 119 | "keyword": { 120 | "ignore_above": 256, 121 | "type": "keyword" 122 | } 123 | } 124 | }, 125 | "hostname": { 126 | "type": "text" 127 | }, 128 | "protocol": { 129 | "type": "text" 130 | }, 131 | "pluginoutput": { 132 | "type": "text", 133 | "fields": { 134 | "keyword": { 135 | "ignore_above": 256, 136 | "type": "keyword" 137 | } 138 | } 139 | }, 140 | "solution": { 141 | "type": "text", 142 | "fields": { 143 | "keyword": { 144 | "ignore_above": 256, 145 | "type": "keyword" 146 | } 147 | } 148 | }, 149 | "hostmacaddress": { 150 | "type": "text" 151 | }, 152 | "compliancecheckname": { 153 | "type": "text" 154 | }, 155 | "hostfqdn": { 156 | "type": "text" 157 | }, 158 | "severity": { 159 | "type": "long" 160 | }, 161 | "complianceauditfile": { 162 | "type": "text" 163 | }, 164 | "riskfactor": { 165 | "type": "text" 166 | }, 167 | "pluginid": { 168 | "type": "long" 169 | }, 170 | "synopsis": { 171 | "type": "text", 172 | "fields": { 173 | "keyword": { 174 | "ignore_above": 256, 175 | "type": "keyword" 176 | } 177 | } 178 | }, 179 | "svc_name": { 180 | "type": "text" 181 | }, 182 | "@timestamp": { 183 | "format": "yyyy-MM-dd HH:mm:ss", 184 | "type": "date", 185 | "doc_values": true 186 | }, 187 | "port": { 188 | "type": "long" 189 | }, 190 | "compliance": { 191 | "type": "boolean" 192 | }, 193 | "credentialedscan": { 194 | "type": "boolean" 195 | }, 196 | "pluginfamily": { 197 | "type": "text" 198 | }, 199 | "hostscanend": { 200 | "format": "yyyy-MM-dd HH:mm:ss", 201 | "type": "date" 202 | } 203 | } 204 | } 205 | }, 206 | "aliases": {} 207 | } 208 | -------------------------------------------------------------------------------- /quick_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ar0xA/nessus2es/d4833689185f0d427968a4322593a33f4663b665/quick_graph.png --------------------------------------------------------------------------------