Mercurial > farol > farolluz
changeset 49:3fd16093536e
merged
author | Benoît Allard <benoit.allard@greenbone.net> |
---|---|
date | Tue, 30 Dec 2014 12:31:50 +0100 |
parents | 3826f2701ff2 (diff) 2e36289616db (current diff) |
children | e6b52a8119cd |
files | CHANGES |
diffstat | 9 files changed, 997 insertions(+), 31 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES Tue Dec 23 09:01:38 2014 +0100 +++ b/CHANGES Tue Dec 30 12:31:50 2014 +0100 @@ -1,7 +1,17 @@ +FarolLuz next +============= + +Main changes since FarolLuz 1.0: +-------------------------------- +* Add parsing for CPE. +* Add parsing for CVEs format from the OpenVAS Greenbone Security Manager. +* Implement an HTML export format. + + FarolLuz 1.0 (2014-11-05) ========================= -THis is the first public release of FarolLuz. FarolLuz is a set of library / +This is the first public release of FarolLuz. FarolLuz is a set of library / utilities to manipulate Security Advisories. It is part of Farol, the Security Advisory Management Platform. @@ -13,13 +23,6 @@ * Add method to rename groupIDs in the whole document. * Split the big cvrf.py file into smaller ones -FarolLuz next -============= - -Main changes since FarolLuz 0.1.1 ---------------------------------- -* Implement an HTML export format. - FarolLuz 0.1.1 (2014-10-17) ===========================
--- a/farolluz/document.py Tue Dec 23 09:01:38 2014 +0100 +++ b/farolluz/document.py Tue Dec 30 12:31:50 2014 +0100 @@ -82,6 +82,7 @@ class CVRFTracking(object): STATUSES = ('Draft', 'Interim', 'Final') def __init__(self, _id, status, version, initial, current): + """ version must be a tuple of (max four) ints """ self._identification = _id self._status = status self._version = version @@ -148,6 +149,7 @@ class CVRFRevision(object): def __init__(self, number, date, description): + """ number is a tuple of (max four) ints """ self._number = number self._date = date self._description = description
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/parsers/cpe.py Tue Dec 30 12:31:50 2014 +0100 @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# Description: +# Methods for parsing CPEs +# +# Authors: +# Benoît Allard <benoit.allard@greenbone.net> +# +# Copyright: +# Copyright (C) 2014 Greenbone Networks GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +"""\ +a cpe class to ease the creation of a producttree based on cpe + +This is based on: + + NIST Interagency Report 7695 + Common Platform Enumeration: Naming Specification Version 2.3 + +CPE is a trademark of The MITRE Corporation. + +""" + +import re + +from ..producttree import CVRFFullProductName, CVRFRelationship + +def capitalize(s): + """ A custom version of string.capwords that split on _, and join on ' ' + """ + s = s.replace('\\', '') + return ' '.join(c.capitalize() for c in s.split('_')) + + +PCT_MAP ={'!': "%21", '"': "%22", '#': "%23", '$': "%24", '%': "%25", '&': "%26", + "'": "%27", '(': "%28", ')': "%29", '*': "%2a", '+': "%2b", ',': "%2c", + '/': "%2f", ':': "%3a", ';': "%3b", '<': "%3c", "=": "%3d", '>': "%3e", + '?': "%3f", '@': "%40", '[': "%5b", '\\': "%5c","]": "%5d", '^': "%5e", + '`': "%60", '{': "%7b", '|': "%7c", '}': "%7d", "~": "%7e"} + +PCT_MAP_i = dict((v, k) for k, v in PCT_MAP.iteritems()) + +def pct_encode(c): + """ Returns the right percent-encoding of c """ + if c in "-.": + return c + return PCT_MAP[c] + return {'!': "%21", '"': "%22", '#': "%23", '$': "%24", '%': "%25", '&': "%26", + "'": "%27", '(': "%28", ')': "%29", '*': "%2a", '+': "%2b", ',': "%2c", + "-": c, '.': c, '/': "%2f", ':': "%3a", ';': "%3b", '<': "%3c", + "=": "%3d", '>': "%3e", '?': "%3f", '@': "%40", '[': "%5b", '\\': "%5c", + "]": "%5d", '^': "%5e", '`': "%60", '{': "%7b", '|': "%7c", '}': "%7d", + "~": "%7e"}[c] + +def decode(s): + if s == '': + return ANY + if s == '-': + return NA + s = s.lower() + res = "" + idx = 0 + embedded = False + while idx < len(s): + c = s[idx] + if c in ".-~": + res += "\\" + c + embedded = True + elif c != '%': + res += c + embedded = True + else: + form = s[idx:idx+3] + if form == "%01": + if (((idx == 0) or (idx == (len(s) - 3))) or + ( not embedded and (s[idx-4:idx-1] == "%01")) or + (embedded and (len(s) > idx + 6) and (s[idx+3:idx+6] == "%01"))): + res += '?' + else: + raise ValueError + elif form == "%02": + if (idx == 0) or (idx == len(s) - 3): + res += '*' + else: + raise ValueError + else: + res += '\\' + PCT_MAP_i[form] + embedded = True + idx += 2 + idx += 1 + return CPEAttribute(res) + +def unbind_value_fs(s): + if s == '*': + return ANY + if s == '-': + return NA + res = "" + idx = 0 + embedded = False + while idx < len(s): + c = s[idx] + if re.match("[a-zA-Z0-9_]", c) is not None: + res += c + embedded = True + elif c == "\\": + res += s[idx:idx+2] + embedded = True + idx += 1 + elif c == "*": + if (idx == 0) or (idx == (len(s) - 1)): + res += c + embedded = True + else: + raise ValueError + elif c == "?": + if (((idx == 0) or (idx == (len(s) - 1))) or + (not embedded and (s[idx - 1] == "?")) or + (embedded and (s[idx + 1] == "?"))): + res += c + embedded = False + else: + raise ValueError + else: + res += "\\" + c + embedded = True + idx += 1 + return CPEAttribute(res) + +class CPEAttribute(object): + """ We need a special class to deal with ANY / NA / "string" """ + + def __init__(self, value=None, any=False, na=False): + self.any = any + self.na = na + self.value = value + + def bind_for_URI(self): +# print self.any, self.na, self.value + if self.any: + return "" + if self.na: + return '-' + return self.transform_for_uri() + + def transform_for_uri(self): + res = "" + idx = 0 + while idx < len(self.value): + c = self.value[idx] + if re.match("[a-zA-Z0-9_]", c) is not None: + res += c + elif c == '\\': + idx += 1 + c = self.value[idx] + res += pct_encode(c) + elif c == '?': + res += "%01" + elif c == '*': + res += "%02" + idx += 1 + return res + + def bind_for_fs(self): + if self.any: + return "*" + if self.na: + return "-" + return self.process_quoted_chars() + + def process_quoted_chars(self): + res = "" + idx = 0 + while idx < len(self.value): + c = self.value[idx] + if c != '\\': + res += c + else: + idx += 1 + c = self.value[idx] + if c in ".-_": + res += c + else: + res += '\\' + c + idx += 1 + return res + +ANY = CPEAttribute(any=True) +NA = CPEAttribute(na=True) + +class CPE(object): + + def __init__(self, part=None, vendor=None, product=None, version=None, update=None, edition=None, language=None, sw_edition=None, target_sw=None, target_hw=None, other=None): + self.part = part or CPEAttribute(any=True) + self.vendor = vendor or CPEAttribute(any=True) + self.product = product or CPEAttribute(any=True) + self.version = version or CPEAttribute(any=True) + self.update = update or CPEAttribute(any=True) + self.edition = edition or CPEAttribute(any=True) + self.language = language or CPEAttribute(any=True) + # Extended attributes: + self.sw_edition = sw_edition or CPEAttribute(any=True) + self.target_sw = target_sw or CPEAttribute(any=True) + self.target_hw = target_hw or CPEAttribute(any=True) + self.other = other or CPEAttribute(any=True) + + def bind_to_URI(self): + uri = 'cpe:/' + uri += ':'.join(a.bind_for_URI() for a in (self.part, self.vendor, self.product, self.version, self.update)) + # Special handling for edition + ed = self.edition.bind_for_URI() + sw_ed = self.sw_edition.bind_for_URI() + t_sw = self.target_sw.bind_for_URI() + t_hw = self.target_hw.bind_for_URI() + oth = self.other.bind_for_URI() + if sw_ed == "" and t_sw == "" and t_hw == "" and oth == "": + uri += ":" + ed + else: + uri += ":~" + '~'.join([ed, sw_ed, t_sw, t_hw, oth]) + uri += ':' + self.language.bind_for_URI() + return uri.rstrip(':') + + def unbind_URI(self, uri): + for idx, comp in enumerate(uri.split(':')): + if idx == 0: + continue + elif idx == 1: + self.part = decode(comp[1:]) + elif idx == 2: + self.vendor = decode(comp) + elif idx == 3: + self.product = decode(comp) + elif idx == 4: + self.version = decode(comp) + elif idx == 5: + self.update = decode(comp) + elif idx == 6: + if comp == "" or comp[0] != '~': + self.edition = decode(comp) + else: + ed, sw_ed, t_sw, t_hw, oth = comp[1:].split('~') + self.edition = decode(ed) + self.sw_edition = decode(sw_ed) + self.target_sw = decode(t_sw) + self.target_hw = decode(t_hw) + self.other = decode(oth) + elif idx == 7: + self.language = decode(comp) + + def bind_to_fs(self): + fs = 'cpe:2.3:' + fs += ':'.join(a.bind_for_fs() for a in (self.part, self.vendor, self.product, self.version, self.update, self.edition, self.language, self.sw_edition, self.target_sw, self.target_hw, self.other)) + return fs + + def unbind_fs(self, fs): + for idx, v in enumerate(fs.split(':')): + v = unbind_value_fs(v) + if idx == 2: + self.part = v + elif idx == 3: + self.vendor = v + elif idx == 4: + self.product = v + elif idx == 5: + self.version = v + elif idx == 6: + self.update = v + elif idx == 7: + self.edition = v + elif idx == 8: + self.language = v + elif idx == 9: + self.sw_edition = v + elif idx == 10: + self.target_sw = v + elif idx == 11: + self.target_hw = v + elif idx == 12: + self.other = v + + def addToDoc(self, document, finalProduct=True): + """ Add the CPE value as full producttree in the document + If finalProduct is false, only the elements leading to the product + will be added. + """ + ptree = document._producttree + if ptree is None: + ptree = document.createProductTree() + + def next_prodid(): + """ A handy function to generate the next available productid """ + prods = document._producttree._products + if len(prods) > 0: + last_prodid = prods[-1]._productid + numlen = 0 + while last_prodid[- (numlen + 1)] in "0123456789": + numlen += 1 + if numlen != 0: + return last_prodid[:-numlen] + str(int(last_prodid[-numlen:]) + 1) + return document.getDocId() + '-P0' + + # Create the main product tree + tree = [] + for value, valtype in [(self.vendor, 'Vendor'), + (self.product, 'Product Name'), + (self.version, 'Product Version'), + (self.update, 'Patch Level'), + (self.language, 'Language'), + (self.target_hw, 'Architecture')]: + if value.value is not None: + tree.append((valtype, capitalize(value.value))) + + # Import it + last_branch = ptree.importTree(tree) + # Add a product there + if self.target_sw.value is None: + if not finalProduct: + return last_branch + product = CVRFFullProductName(next_prodid(), str(self), last_branch, self.bind_to_fs()) + ptree.addProduct(product) + return product + else: + product = CVRFFullProductName(next_prodid(), str(self), last_branch) + ptree.addProduct(product) + + # We do have a target software, we need to create a relationship ! + os = CVRFFullProductName(next_prodid(), self.target_sw.value, ptree) + ptree.addProduct(os) + + rel = CVRFRelationship(product._productid, 'Installed On', os._productid) + ptree.addRelationship(rel) + if not finalProduct: + return rel + + final_prod = CVRFFullProductName(next_prodid(), ptree.getNameOfRelationship(rel), rel, self.bind_to_fs()) + ptree.addProduct(final_prod) + return final_prod + + def __str__(self): + res = [] + if self.product.value: + res.append(capitalize(self.product.value)) + if self.version.value: + res.append(capitalize(self.version.value)) + if not res: + return capitalize(self.vendor.value) + return ' '.join(res) + +def parse(s): + cpe = CPE() + if s[:5] == 'cpe:/': + cpe.unbind_URI(s) + elif s[:8] == 'cpe:2.3:': + cpe.unbind_fs(s) + else: + raise ValueError(s) + return cpe
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/parsers/cve.py Tue Dec 30 12:31:50 2014 +0100 @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# Description: +# Methods for parsing CVE XML documents +# +# Authors: +# Benoît Allard <benoit.allard@greenbone.net> +# +# Copyright: +# Copyright (C) 2014 Greenbone Networks GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +"""\ +Methods for parsing of CVE XML Documents + +Ref: http://scap.nist.gov/schema/vulnerability/0.4 +""" + +from __future__ import absolute_import + +import xml.etree.ElementTree as ET + +from .cpe import parse as parseCPE +from .xml import parseDate + +from .. import __version__ +from ..common import CVRFNote, CVRFReference +from ..document import CVRF, CVRFPublisher, CVRFTracking, CVRFTrackingID, CVRFRevision, CVRFGenerator +from ..producttree import CVRFFullProductName +from ..utils import utcnow +from ..vulnerability import CVRFVulnerability, CVRFCVSSSet, CVRFCWE, CVRFProductStatus + +NAMESPACES = { + 'cve': "http://scap.nist.gov/schema/feed/vulnerability/2.0", + 'vuln': "http://scap.nist.gov/schema/vulnerability/0.4", + 'cvss': "http://scap.nist.gov/schema/cvss-v2/0.2", + 'xml': "http://www.w3.org/XML/1998/namespace", +} + + +def UN(ns, name): + """ returns a Universal Name """ + return "{%s}%s" % (NAMESPACES[ns], name) + +def parseCVSS(xmlElem): + """ Make a vector out of a list of elements """ + def get(name): + return xmlElem.findtext('/'.join([UN('cvss', 'base_metrics'), UN('cvss', name)])) + + cvss_set = CVRFCVSSSet(float(get('score'))) + vector = [ + 'AV:%s' % {'LOCAL': 'L', + 'ADJACENT_NETWORK': 'A', + 'NETWORK': 'N'}[get('access-vector')], + 'AC:%s' % {'HIGH': 'H', + 'MEDIUM': 'M', + 'LOW': 'L'}[get('access-complexity')], + 'Au:%s' % {'MULTIPLE': 'M', + 'SINGLE': 'S', + 'NONE': 'N'}[get('authentication')], + 'C:%s' % {'NONE': 'N', + 'PARTIAL': 'P', + 'COMPLETE': 'C'}[get('confidentiality-impact')], + 'I:%s' % {'NONE': 'N', + 'PARTIAL': 'P', + 'COMPLETE': 'C'}[get('integrity-impact')], + 'A:%s' % {'NONE': 'N', + 'PARTIAL': 'P', + 'COMPLETE': 'C'}[get('availability-impact')], + ] + cvss_set.setVector('/'.join(vector)) + return cvss_set + +def parseXML(data): + """ returns am ET.Element from the input stuff. + input can be: + - a string + - a file handle + - an ET.Element instance + """ + if isinstance(data, ET.Element): + return data + # To allow passing file handles + if hasattr(data, 'read'): + data = data.read() + # Parse it. + return ET.fromstring(data) + +def parse_CVE_from_GSA(data): + xml = parseXML(data) + content = xml.find('/'.join(['get_info', 'get_info_response', 'info', 'cve', 'raw_data', UN('cve', 'entry')])) + if content is None: + return None + return parse(content) + +def parse(xml): + xml = parseXML(xml) + + # Create an extra-minimal document + doc = CVRF(xml.findtext(UN('vuln', 'cve-id')), + 'Vulnerability Description') + pub = CVRFPublisher("Other") + doc.setPublisher(pub) + now = utcnow() + tracking = CVRFTracking( + CVRFTrackingID('000000'), + "Draft", + (0,), + now, now + ) + doc.setTracking(tracking) + generator = CVRFGenerator() + generator.setEngine('FarolLuz ' + __version__) + generator.setDate(now) + tracking.setGenerator(generator) + tracking.addRevision(CVRFRevision((0,), now, 'Document created')) + + # Add the CVE to that document + return addToDoc(doc, xml) + +def addToDoc(doc, xml): + """ Adds the CVE as vulnerability in the document """ + xml = parseXML(xml) + + vulnid = xml.attrib['id'] + + # Get a new ordinal for our new Vulnerability + if len(doc._vulnerabilities) == 0: + ordinal = 1 + else: + ordinal = doc._vulnerabilities[-1]._ordinal + 1 + + # Create a Vulnerability + vuln = CVRFVulnerability(ordinal) + doc.addVulnerability(vuln) + + vulnerable_products = [] + # Set the vulnerable products in productTree + for i, cpe in enumerate(xml.findall( + '/'.join([UN('vuln', 'vulnerable-software-list'), + UN('vuln', 'product')]))): + prod = parseCPE(cpe.text).addToDoc(doc) + vulnerable_products.append(prod) + + if vulnerable_products: + status = CVRFProductStatus('Known Affected') + for product in vulnerable_products: + status.addProductID(product._productid) + vuln.addProductStatus(status) + + # Add the CVE-id + vuln.setCVE(xml.findtext(UN('vuln', 'cve-id'))) + + # The release date + vuln.setReleaseDate(parseDate(xml.findtext(UN('vuln', 'published-datetime')))) + + # Add the CVSS + xmlcvss = xml.find(UN('vuln', 'cvss')) + if xmlcvss is not None: + vuln.addCVSSSet(parseCVSS(xmlcvss)) + + # Add the CWE id + xmlcwe = xml.find(UN('vuln', 'cwe')) + if xmlcwe is not None: + # XXX: Get a Description for the CWE ! + vuln.addCWE(CVRFCWE(xmlcwe.attrib['id'], xmlcwe.attrib['id'])) + + # Add references + for xmlref in xml.findall(UN('vuln', 'references')): + vuln.addReference(CVRFReference(xmlref.find(UN('vuln','reference')).attrib['href'], + xmlref.findtext(UN('vuln', 'reference')))) + + xmlsummary = xml.findtext(UN('vuln', 'summary')) + if xmlsummary is not None: + vuln.addNote(CVRFNote( + 'Summary', + 1, + xmlsummary + )) + + return doc
--- a/farolluz/parsers/cvrf.py Tue Dec 23 09:01:38 2014 +0100 +++ b/farolluz/parsers/cvrf.py Tue Dec 30 12:31:50 2014 +0100 @@ -27,16 +27,13 @@ """ from __future__ import print_function +# Allow .xml to be different from xml +from __future__ import absolute_import -import re import textwrap import xml.etree.ElementTree as ET -from datetime import datetime, timedelta -try: - from datetime import timezone -except ImportError: - from ..py2 import FixedTimeZone as timezone +from .xml import parseDate from ..common import CVRFNote, CVRFAcknowledgment, CVRFReference from ..document import (CVRF, CVRFPublisher, CVRFTracking, CVRFRevision, @@ -63,19 +60,6 @@ return tuple(int(i) for i in string.split('.')) -def parseDate(string): - m = re.match('(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:([+-])(\d{2}):(\d{2})|(Z))?', string) - if (m.group(7) is None) or (m.group(7) == 'Z'): - tzhours = 0 - tzmin = 0 - else: - tzhours = int(m.group(8)) - if m.group(7) == '-': - tzhours = - tzhours - tzmin = int(m.group(9)) - return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6)), tzinfo=timezone(timedelta(hours=tzhours, minutes=tzmin))) - - def parseNote(elem): return CVRFNote( elem.attrib['Type'],
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/parsers/xml.py Tue Dec 30 12:31:50 2014 +0100 @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Description: +# Methods for parsing CVE XML documents +# +# Authors: +# Benoît Allard <benoit.allard@greenbone.net> +# +# Copyright: +# Copyright (C) 2014 Greenbone Networks GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +"""\ +Methods for parsing of CVE XML Documents + +Ref: http://scap.nist.gov/schema/vulnerability/0.4 +""" + +import re + +from datetime import datetime, timedelta + +try: + from datetime import timezone +except ImportError: + from ..py2 import FixedTimeZone as timezone + +def parseDate(string): + m = re.match('(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:([+-])(\d{2}):(\d{2})|(Z))?', string) + if (m.group(7) is None) or (m.group(7) == 'Z'): + tzhours = 0 + tzmin = 0 + else: + tzhours = int(m.group(8)) + if m.group(7) == '-': + tzhours = - tzhours + tzmin = int(m.group(9)) + return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6)), tzinfo=timezone(timedelta(hours=tzhours, minutes=tzmin)))
--- a/farolluz/producttree.py Tue Dec 23 09:01:38 2014 +0100 +++ b/farolluz/producttree.py Tue Dec 30 12:31:50 2014 +0100 @@ -51,6 +51,12 @@ return product raise KeyError(productid) + def getProductForCPE(self, cpe): + for product in self._products: + if product._cpe == cpe: + return product + raise KeyError(cpe) + def getGroupForID(self, groupid): for group in self._groups: if group._groupid == groupid: @@ -75,6 +81,7 @@ ) def getBranch(self, path): + """ path is a tuple of indexes """ if len(path) == 0: return self branches = self._branches @@ -114,9 +121,10 @@ The branches that could accept `b2` as new sub-branches Note that b2 and all its sub-branches cannot be listed """ - black_list = [] + black_list = set() if b2 is not None: - black_list = [b2] + list(b2.getBranches()) + black_list.add(b2) + black_list.update(b2.getBranches()) for branch in self.getBranches(): if branch in black_list: continue @@ -136,6 +144,19 @@ """ Amount of 'raw' Products """ return len([p for p in self._products if p._parent is self]) + def importTree(self, tree): + """ tree is a list of tuple (type, value) like the one generated by + getTree() """ + if len(tree) == 0: + return self + found = None + for branch in self._branches: + if branch.getTree() == tree[:1]: + found = branch + if found is None: + found = CVRFProductBranch(tree[0][0], tree[0][1], self) + return found.importTree(tree, 1) + def validate(self): for branch in self._branches: branch.validate() @@ -181,7 +202,8 @@ return self._parentbranch def getPath(self, string=False): - """ return the path to that branch element as a tuple """ + """ return the path to that branch element as a tuple or as '/' + separated string """ if self.isRoot(): for i, b in enumerate(self._parentbranch._branches): if b is self: @@ -233,13 +255,25 @@ self._parentbranch = None def link(self, parent): - """ Actually, only set the parent """ + """ Set the parent, and add ourself to our parent's childs """ self._parentbranch = parent if self.isRoot(): parent._branches.append(self) else: parent._childs.append(self) + def importTree(self, tree, index): + """ tree is a list of tuple (type, value) like the one generated by + getTree() """ + if len(tree) == index: + return self + found = None + for branch in self._childs: + if branch.getTree() == tree[:index + 1]: + found = branch + if found is None: + found = CVRFProductBranch(tree[index][0], tree[index][1], self) + return found.importTree(tree, index + 1) def validate(self): if not self._type:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/testCPE.py Tue Dec 30 12:31:50 2014 +0100 @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# Description: +# Tests for the CPE parsing methods +# +# Authors: +# Benoît Allard <benoit.allard@greenbone.net> +# +# Copyright: +# Copyright (C) 2014 Greenbone Networks GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +import unittest + +from farolluz.parsers.cpe import CPE, CPEAttribute, parse + +class testbindToURI(unittest.TestCase): + + def test_example1(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.0\.6001'), update=CPEAttribute('beta'), edition=CPEAttribute(any=True)) + + self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:microsoft:internet_explorer:8.0.6001:beta') + + def test_example2(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.*'), update=CPEAttribute('sp?')) + + self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:microsoft:internet_explorer:8.%02:sp%01') + + def test_example3(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('insight_diagnostics'), version=CPEAttribute(r'7\.4\.0\.1570'), update=CPEAttribute(na=True), sw_edition=CPEAttribute('online'), target_sw=CPEAttribute('win2003'), target_hw=CPEAttribute("x64")) + + self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win2003~x64~') + + def test_example4(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('openview_network_manager'), version=CPEAttribute(r'7\.51'), target_sw=CPEAttribute('linux')) + + self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:hp:openview_network_manager:7.51::~~~linux~~') + + def test_example5(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute(r'foo\\bar'), product=CPEAttribute(r'big\$money_manager_2010'), sw_edition=CPEAttribute('special'), target_sw=CPEAttribute('ipod_touch'), target_hw=CPEAttribute("80gb")) + + self.assertEqual(cpe.bind_to_URI(), 'cpe:/a:foo%5cbar:big%24money_manager_2010:::~~special~ipod_touch~80gb~') + +class testunbindURI(unittest.TestCase): + + def test_example1(self): + cpe = CPE() + cpe.unbind_URI('cpe:/a:microsoft:internet_explorer:8.0.6001:beta') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'microsoft') + self.assertEqual(cpe.product.value, 'internet_explorer') + self.assertEqual(cpe.version.value, r'8\.0\.6001') + self.assertEqual(cpe.update.value, 'beta') + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.language.any) + + def test_example2(self): + cpe = CPE() + cpe.unbind_URI('cpe:/a:microsoft:internet_explorer:8.%2a:sp%3f') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'microsoft') + self.assertEqual(cpe.product.value, 'internet_explorer') + self.assertEqual(cpe.version.value, r'8\.\*') + self.assertEqual(cpe.update.value, 'sp\?') + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.language.any) + + def test_example3(self): + cpe = CPE() + cpe.unbind_URI('cpe:/a:microsoft:internet_explorer:8.%02:sp%01') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'microsoft') + self.assertEqual(cpe.product.value, 'internet_explorer') + self.assertEqual(cpe.version.value, r'8\.*') + self.assertEqual(cpe.update.value, 'sp?') + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.language.any) + + def test_example4(self): + cpe = CPE() + cpe.unbind_URI('cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~x64~') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'hp') + self.assertEqual(cpe.product.value, 'insight_diagnostics') + self.assertEqual(cpe.version.value, '7\.4\.0\.1570') + self.assertTrue(cpe.update.any) + self.assertTrue(cpe.edition.any) + self.assertEqual(cpe.sw_edition.value, 'online') + self.assertEqual(cpe.target_sw.value, 'win2003') + self.assertEqual(cpe.target_hw.value, 'x64') + self.assertTrue(cpe.other.any) + self.assertTrue(cpe.language.any) + + def test_example5(self): + cpe = CPE() + cpe.unbind_URI('cpe:/a:hp:openview_network_manager:7.51:-:~~~linux~~') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'hp') + self.assertEqual(cpe.product.value, 'openview_network_manager') + self.assertEqual(cpe.version.value, '7\.51') + self.assertTrue(cpe.update.na) + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.sw_edition.any) + self.assertEqual(cpe.target_sw.value, 'linux') + self.assertTrue(cpe.target_hw.any) + self.assertTrue(cpe.other.any) + self.assertTrue(cpe.language.any) + + + def test_example6(self): + cpe = CPE() + self.assertRaises(KeyError, cpe.unbind_URI, 'cpe:/a:foo%5cbar:big%24money_2010%07:::~~special~ipod_touch~80gb~') + + + def test_example7(self): + cpe = CPE() + cpe.unbind_URI('cpe:/a:foo~bar:big%7emoney_2010') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'foo\~bar') + self.assertEqual(cpe.product.value, 'big\~money_2010') + self.assertTrue(cpe.version.any) + self.assertTrue(cpe.update.any) + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.language.any) + + def test_example8(self): + cpe = CPE() + self.assertRaises(ValueError, cpe.unbind_URI, 'cpe:/a:foo:bar:12.%02.1234') + +class testbindFS(unittest.TestCase): + + def test_example1(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.0\.6001'), update=CPEAttribute('beta'), edition=CPEAttribute(any=True)) + + self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*') + + def test_example2(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('microsoft'), product=CPEAttribute('internet_explorer'), version=CPEAttribute(r'8\.*'), update=CPEAttribute('sp?'), edition=CPEAttribute(any=True)) + + self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:microsoft:internet_explorer:8.*:sp?:*:*:*:*:*:*') + + cpe.version = CPEAttribute(r'8\.\*') + + self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:microsoft:internet_explorer:8.\*:sp?:*:*:*:*:*:*') + + def test_example3(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('insight'), version=CPEAttribute(r'7\.4\.0\.1570'), update=CPEAttribute(na=True), sw_edition=CPEAttribute("online"), target_sw=CPEAttribute("win2003"), target_hw=CPEAttribute("x64")) + + self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:hp:insight:7.4.0.1570:-:*:*:online:win2003:x64:*') + + def test_example4(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute('hp'), product=CPEAttribute('openview_network_manager'), version=CPEAttribute(r'7\.51'), target_sw=CPEAttribute('linux')) + + self.assertEqual(cpe.bind_to_fs(), 'cpe:2.3:a:hp:openview_network_manager:7.51:*:*:*:*:linux:*:*') + + def test_example5(self): + cpe = CPE(part=CPEAttribute('a'), vendor=CPEAttribute(r'foo\\bar'), product=CPEAttribute(r'big\$money_2010'), sw_edition=CPEAttribute('special'), target_sw=CPEAttribute('ipod_touch'), target_hw=CPEAttribute("80gb")) + + self.assertEqual(cpe.bind_to_fs(), r'cpe:2.3:a:foo\\bar:big\$money_2010:*:*:*:*:special:ipod_touch:80gb:*') + +class testunbind_fs(unittest.TestCase): + + def test_example1(self): + cpe = CPE() + cpe.unbind_fs('cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'microsoft') + self.assertEqual(cpe.product.value, 'internet_explorer') + self.assertEqual(cpe.version.value, '8\.0\.6001') + self.assertEqual(cpe.update.value, "beta") + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.sw_edition.any) + self.assertTrue(cpe.target_sw.any) + self.assertTrue(cpe.target_hw.any) + self.assertTrue(cpe.other.any) + self.assertTrue(cpe.language.any) + + def test_example2(self): + cpe = CPE() + cpe.unbind_fs('cpe:2.3:a:microsoft:internet_explorer:8.*:sp?:*:*:*:*:*:*') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'microsoft') + self.assertEqual(cpe.product.value, 'internet_explorer') + self.assertEqual(cpe.version.value, '8\.*') + self.assertEqual(cpe.update.value, "sp?") + self.assertTrue(cpe.edition.any) + self.assertTrue(cpe.sw_edition.any) + self.assertTrue(cpe.target_sw.any) + self.assertTrue(cpe.target_hw.any) + self.assertTrue(cpe.other.any) + self.assertTrue(cpe.language.any) + + def test_example3(self): + cpe = CPE() + cpe.unbind_fs('cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win2003:x64:*') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, 'hp') + self.assertEqual(cpe.product.value, 'insight_diagnostics') + self.assertEqual(cpe.version.value, '7\.4\.0\.1570') + self.assertTrue(cpe.update.na) + self.assertTrue(cpe.edition.any) + self.assertEqual(cpe.sw_edition.value, "online") + self.assertEqual(cpe.target_sw.value, "win2003") + self.assertEqual(cpe.target_hw.value, "x64") + self.assertTrue(cpe.other.any) + self.assertTrue(cpe.language.any) + + self.assertRaises(ValueError, cpe.unbind_fs, 'cpe:2.3:a:hp:insight_diagnostics:7.4.*.1570:*:*:*:*:*:*') + + + def test_example4(self): + cpe = CPE() + cpe.unbind_fs(r'cpe:2.3:a:foo\\bar:big\$money:2010:*:*:*:special:ipod_touch:80gb:*') + self.assertEqual(cpe.part.value, 'a') + self.assertEqual(cpe.vendor.value, r'foo\\bar') + self.assertEqual(cpe.product.value, 'big\$money') + self.assertEqual(cpe.version.value, '2010') + self.assertTrue(cpe.update.any) + self.assertTrue(cpe.edition.any) + self.assertEqual(cpe.sw_edition.value, "special") + self.assertEqual(cpe.target_sw.value, "ipod_touch") + self.assertEqual(cpe.target_hw.value, "80gb") + self.assertTrue(cpe.other.any) + self.assertTrue(cpe.language.any) + +class testParse(unittest.TestCase): + + def testURI(self): + cpe = parse('cpe:/a:fogproject:fog:0.31') + self.assertEqual(cpe.part.value, 'a') + + def testFS(self): + cpe = parse('cpe:2.3:a:tenable:web_ui:2.3.3:*:*:*:*:*:*') + self.assertEqual(cpe.vendor.value, 'tenable') + + def testGarbage(self): + self.assertRaises(ValueError, parse, 'garbage')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/testParseCVE.py Tue Dec 30 12:31:50 2014 +0100 @@ -0,0 +1,81 @@ +import utils + +from farolluz.parsers.cve import parse + +FULL_CVE = """\ +<entry xmlns:scap-core="http://scap.nist.gov/schema/scap-core/0.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:patch="http://scap.nist.gov/schema/patch/0.1" xmlns:vuln="http://scap.nist.gov/schema/vulnerability/0.4" xmlns:cvss="http://scap.nist.gov/schema/cvss-v2/0.2" xmlns:cpe-lang="http://cpe.mitre.org/language/2.0" xmlns="http://scap.nist.gov/schema/feed/vulnerability/2.0" id="CVE-2014-7088"> +<vuln:vulnerable-configuration id="http://nvd.nist.gov/"> +<cpe-lang:logical-test operator="OR" negate="false"> +<cpe-lang:fact-ref name="cpe:/a:jdm_lifestyle_project:jdm_lifestyle:6.4::~~~android~~"/> +</cpe-lang:logical-test> +</vuln:vulnerable-configuration> +<vuln:vulnerable-software-list> +<vuln:product> +cpe:/a:jdm_lifestyle_project:jdm_lifestyle:6.4::~~~android~~ +</vuln:product> +</vuln:vulnerable-software-list> +<vuln:cve-id>CVE-2014-7088</vuln:cve-id> +<vuln:published-datetime>2014-10-18T21:55:17.027-04:00</vuln:published-datetime> +<vuln:last-modified-datetime>2014-11-14T09:07:51.650-05:00</vuln:last-modified-datetime> +<vuln:cvss> +<cvss:base_metrics> +<cvss:score>5.4</cvss:score> +<cvss:access-vector>ADJACENT_NETWORK</cvss:access-vector> +<cvss:access-complexity>MEDIUM</cvss:access-complexity> +<cvss:authentication>NONE</cvss:authentication> +<cvss:confidentiality-impact>PARTIAL</cvss:confidentiality-impact> +<cvss:integrity-impact>PARTIAL</cvss:integrity-impact> +<cvss:availability-impact>PARTIAL</cvss:availability-impact> +<cvss:source>http://nvd.nist.gov</cvss:source> +<cvss:generated-on-datetime>2014-11-14T09:07:51.290-05:00</cvss:generated-on-datetime> +</cvss:base_metrics> +</vuln:cvss> +<vuln:cwe id="CWE-310"/> +<vuln:references reference_type="UNKNOWN" xml:lang="en"> +<vuln:source>CERT-VN</vuln:source> +<vuln:reference href="http://www.kb.cert.org/vuls/id/582497" xml:lang="en">VU#582497</vuln:reference> +</vuln:references> +<vuln:references reference_type="UNKNOWN" xml:lang="en"> +<vuln:source>MISC</vuln:source> +<vuln:reference href="https://docs.google.com/spreadsheets/d/1t5GXwjw82SyunALVJb2w0zi3FoLRIkfGPc7AMjRF0r4/edit?usp=sharing" xml:lang="en"> +https://docs.google.com/spreadsheets/d/1t5GXwjw82SyunALVJb2w0zi3FoLRIkfGPc7AMjRF0r4/edit?usp=sharing +</vuln:reference> +</vuln:references> +<vuln:summary> +The JDM Lifestyle (aka com.hondatech) application 6.4 for Android does not verify X.509 certificates from SSL servers, which allows man-in-the-middle attackers to spoof servers and obtain sensitive information via a crafted certificate. +</vuln:summary> +</entry>""" + +CVE_NO_CVSS = """\ +<entry xmlns:scap-core="http://scap.nist.gov/schema/scap-core/0.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:patch="http://scap.nist.gov/schema/patch/0.1" xmlns:vuln="http://scap.nist.gov/schema/vulnerability/0.4" xmlns:cvss="http://scap.nist.gov/schema/cvss-v2/0.2" xmlns:cpe-lang="http://cpe.mitre.org/language/2.0" xmlns="http://scap.nist.gov/schema/feed/vulnerability/2.0" id="CVE-2014-9388"> +<vuln:cve-id>CVE-2014-9388</vuln:cve-id> +<vuln:published-datetime>2014-12-17T14:59:08.587-05:00</vuln:published-datetime> +<vuln:last-modified-datetime>2014-12-17T14:59:09.620-05:00</vuln:last-modified-datetime> +<vuln:references reference_type="UNKNOWN" xml:lang="en"> +<vuln:source>CONFIRM</vuln:source> +<vuln:reference href="https://www.mantisbt.org/bugs/view.php?id=17878" xml:lang="en">https://www.mantisbt.org/bugs/view.php?id=17878</vuln:reference> +</vuln:references> +<vuln:references reference_type="UNKNOWN" xml:lang="en"> +<vuln:source>CONFIRM</vuln:source> +<vuln:reference href="https://www.mantisbt.org/bugs/changelog_page.php?version_id=191" xml:lang="en"> +https://www.mantisbt.org/bugs/changelog_page.php?version_id=191 +</vuln:reference> +</vuln:references> +<vuln:references reference_type="UNKNOWN" xml:lang="en"> +<vuln:source>MLIST</vuln:source> +<vuln:reference href="http://seclists.org/oss-sec/2014/q4/955" xml:lang="en">[oss-security] 20141207 MantisBT 1.2.18 Released</vuln:reference> +</vuln:references> +<vuln:summary> +bug_report.php in MantisBT before 1.2.18 allows remote attackers to assign arbitrary issues via the handler_id parameter. +</vuln:summary> +</entry>""" + +class testCVEParsing(utils.TestCase): + + def test_Full(self): + self.doc = parse(FULL_CVE) + self._validate() + + def test_no_CVSS(self): + self.doc = parse(CVE_NO_CVSS) + self._validate()