MD to HTML: allow extra header stuff
[openssl-web.git] / bin / vulnxml2json.py
1 #! /usr/bin/python
2 #
3 # Convert our XML file to a JSON file as accepted by Mitre for CNA purposes
4 # as per https://github.com/CVEProject/automation-working-group/blob/master/cve_json_schema/DRAFT-JSON-file-format-v4.md
5 #
6 # ASF httpd and OpenSSL use quite similar files, so this script is designed to work with either
7 #
8
9 from xml.dom import minidom
10 import HTMLParser
11 import simplejson as json
12 import codecs
13 import re
14 from optparse import OptionParser
15
16 # for validation
17 import json
18 import jsonschema
19 from jsonschema import validate
20 from jsonschema import Draft4Validator
21 import urllib
22
23 # Specific project stuff is here
24 import vulnxml2jsonproject as cfg
25
26 # Location of CVE JSON schema (default, can use local file etc)
27 default_cve_schema = "https://raw.githubusercontent.com/CVEProject/automation-working-group/master/cve_json_schema/CVE_JSON_4.0_min_public.schema"
28
29 parser = OptionParser()
30 parser.add_option("-s", "--schema", help="location of schema to check (default "+default_cve_schema+")", default=default_cve_schema,dest="schema")
31 parser.add_option("-i", "--input", help="input vulnerability file vulnerabilities.xml", dest="input")
32 parser.add_option("-c", "--cve", help="comma separated list of cve names to generate a json file for (or all)", dest="cves")
33 parser.add_option("-o", "--outputdir", help="output directory for json file (default ./)", default=".", dest="outputdir")
34 (options, args) = parser.parse_args()
35
36 if not options.input:
37    print "needs input file"
38    parser.print_help()
39    exit();
40
41 if options.schema:
42    response = urllib.urlopen(options.schema)
43    schema_doc = json.loads(response.read())
44
45 cvej = list()
46     
47 with codecs.open(options.input,"r","utf-8") as vulnfile:
48     vulns = vulnfile.read()
49 dom = minidom.parseString(vulns.encode("utf-8"))
50
51 for issue in dom.getElementsByTagName('issue'):
52     if not issue.getElementsByTagName('cve'):
53         continue
54     # ASF httpd has CVE- prefix, but OpenSSL does not, make either work
55     cvename = issue.getElementsByTagName('cve')[0].getAttribute('name').replace('CVE-','')
56     if (cvename == ""):
57        continue
58     if (options.cves): # If we only want a certain list of CVEs, skip the rest
59        if (not cvename in options.cves):
60           continue
61
62     cve = dict()
63     cve['data_type']="CVE"
64     cve['data_format']="MITRE"
65     cve['data_version']="4.0"
66     cve['CVE_data_meta']= { "ID": "CVE-"+cvename, "ASSIGNER": cfg.config['cve_meta_assigner'], "STATE":"PUBLIC" }
67     datepublic = issue.getAttribute("public")
68     if datepublic:
69        cve['CVE_data_meta']['DATE_PUBLIC'] = datepublic[:4]+'-'+datepublic[4:6]+'-'+datepublic[6:8]
70     if issue.getElementsByTagName('title'):
71         cve['CVE_data_meta']['TITLE'] = issue.getElementsByTagName('title')[0].childNodes[0].nodeValue.strip()
72     desc = ""
73     for d in issue.getElementsByTagName('description')[0].childNodes:
74 #        if d.nodeType == d.ELEMENT_NODE:
75             if desc:
76                 desc += " "
77             desc += re.sub('<[^<]+?>', '', d.toxml().strip())
78     desc = HTMLParser.HTMLParser().unescape(desc)
79     problemtype = "(undefined)"
80     if issue.getElementsByTagName('problemtype'):
81         problemtype = issue.getElementsByTagName('problemtype')[0].childNodes[0].nodeValue.strip()    
82     cve['problemtype'] = { "problemtype_data": [ { "description" : [ { "lang":"eng", "value": problemtype} ] } ] }
83     impact = issue.getElementsByTagName('impact') # openssl does it like this
84     if impact:
85         cve['impact'] = [ { "lang":"eng", "value":impact[0].getAttribute('severity'), "url":cfg.config['security_policy_url']+impact[0].getAttribute('severity') } ]
86     impact = issue.getElementsByTagName('severity')  # httpd does it like this
87     if impact:
88         cve['impact'] = [ { "lang":"eng", "value":impact[0].childNodes[0].nodeValue, "url":cfg.config['security_policy_url']+impact[0].childNodes[0].nodeValue } ]
89
90     # Create the list of credits
91     
92     credit = list()
93     for reported in issue.getElementsByTagName('reported'):  # openssl style credits
94         credit.append( { "lang":"eng", "value":re.sub('[\n ]+',' ', reported.getAttribute("source"))} )
95     for reported in issue.getElementsByTagName('acknowledgements'): # ASF httpd style credits
96         credit.append(  { "lang":"eng", "value":re.sub('[\n ]+',' ', reported.childNodes[0].nodeValue.strip())} )
97     if credit:
98         cve['credit']=credit        
99
100     # Create the list of references
101         
102     refs = list()
103     for adv in issue.getElementsByTagName('advisory'):
104        url = adv.getAttribute("url")
105        if (not url.startswith("htt")):
106           url = cfg.config['default_reference_prefix']+url
107        refs.append({"url":url,"name":url,"refsource":"CONFIRM"})
108     for git in issue.getElementsByTagName('git'): # openssl style references to git
109        url = cfg.config['git_prefix']+git.getAttribute("hash")
110        refs.append({"url":url,"name":url,"refsource":"CONFIRM"})       
111     if cfg.config['project'] == 'httpd': # ASF httpd has no references so fake them
112        for fixed in issue.getElementsByTagName('fixed'):
113           base = "".join(fixed.getAttribute("version").split('.')[:-1])
114           refurl = cfg.config['default_reference']+base+".html#CVE-"+cvename
115           refs.append({"url":refurl,"name":refurl,"refsource":"CONFIRM"})
116     if refs:
117         cve['references'] = { "reference_data": refs  }
118
119     # Create the "affected products" list
120         
121     vv = list()
122     for affects in issue.getElementsByTagName('fixed'): # OpenSSL and httpd since April 2018 does it this way
123        text = "Fixed in %s %s (Affected %s)" %(cfg.config['product_name'],affects.getAttribute('version'),cfg.merge_affects(issue,affects.getAttribute("base")))
124        # Let's condense into a list form since the format of this field is 'free text' at the moment, not machine readable (as per mail with George Theall)
125        vv.append({"version_value":text})
126        # Mitre want the fixed/affected versions in the text too
127        desc += " "+text+"."
128
129 #    if issue.getAttribute('fixed'): # httpd used to do it this way
130 #        base = ".".join(issue.getAttribute("fixed").split('.')[:-1])+"."
131 #        text = "Fixed in %s %s (Affected %s)" %(cfg.config['product_name'],issue.getAttribute('fixed'),cfg.merge_affects(issue,base))
132 #        vv.append({"version_value":text})
133 #        # Mitre want the fixed/affected versions in the text too
134 #        desc += " "+text+"."            
135
136     cve['affects'] = { "vendor" : { "vendor_data" : [ { "vendor_name": cfg.config['vendor_name'], "product": { "product_data" : [ { "product_name": cfg.config['product_name'], "version": { "version_data" : vv}}]}}]}}
137             
138     # Mitre want newlines and excess spaces stripped
139     desc = re.sub('[\n ]+',' ', desc)        
140     cve['description'] = { "description_data": [ { "lang":"eng", "value": desc} ] }
141     cvej.append(cve)
142         
143 for issue in cvej:
144     fn = issue['CVE_data_meta']['ID'] + ".json"
145     if not issue:
146        continue
147
148     f = codecs.open(options.outputdir+"/"+fn, 'w', 'utf-8')
149     f.write(json.dumps(issue, sort_keys=True, indent=4))
150     print "wrote %s" %(options.outputdir+"/"+fn)
151     f.close()
152
153     try:
154        validate(issue, schema_doc)
155        print "%s passed validation" % (fn)
156     except jsonschema.exceptions.ValidationError as incorrect:
157        v = Draft4Validator(schema_doc)
158        errors = sorted(v.iter_errors(issue), key=lambda e: e.path)
159        for error in errors:
160           print "%s did not pass validation: %s" % (fn,str(error.message))
161     except NameError:
162        print "%s skipping validation, no schema defined" %(fn)
163