# HG changeset patch # User Benoît Allard # Date 1414162886 -7200 # Node ID 809db989cac5528456964d27e801d268da17e3a5 # Parent 3cab052872f43b49707f9dc53bdb3fff53e5fcfa Reorganize the code in smaller mpodules diff -r 3cab052872f4 -r 809db989cac5 CHANGES --- a/CHANGES Fri Oct 24 16:43:31 2014 +0200 +++ b/CHANGES Fri Oct 24 17:01:26 2014 +0200 @@ -5,6 +5,7 @@ ---------------------------------- * Add tests * Add method to rename productIDs in the whole document +* Split the big cvrf.py file into smaller ones FarolLuz 0.1.1 (2014-10-17) =========================== diff -r 3cab052872f4 -r 809db989cac5 farolluz/common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/common.py Fri Oct 24 17:01:26 2014 +0200 @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Authors: +# Benoît Allard +# +# 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. + +"""\ +Common Objects related to CVRF Documents +""" + +class ValidationError(Exception): pass + + +class CVRFNote(object): + TYPES = ('General', 'Details', 'Description', 'Summary', 'FAQ', + 'Legal Disclaimer', 'Other') + def __init__(self, _type, ordinal, note, title=None, audience=None): + self._type = _type + self._ordinal = ordinal + self._note = note + self._title = title + self._audience = audience + + def getTitle(self): + """ returns something that can be used as a title """ + if self._title is None: + return "%s (#%d)" % (self._type, self._ordinal) + return "%s (%s)" % (self._title, self._type) + + def validate(self): + if not self._type: + raise ValidationError('A Note needs to have a Type set') + if self._type not in self.TYPES: + raise ValidationError('A Note Type needs to be one of %s' % ', '.join(self.TYPES)) + if self._ordinal < 0: + raise ValidationError('A Note ordinal must be a positive integer') + if not self._note: + raise ValidationError('A Note must contain some text') + + + def __str__(self): + return self._note + + +class CVRFReference(object): + TYPES = ('Self', 'External') + def __init__(self, url, description, _type=None): + self._url = url + self._description = description + self._type = _type + + def validate(self): + if (self._type is not None) and (self._type not in self.TYPES): + raise ValidationError('If a Reference type is set, it mist be one of %s' % ', '.join(self.TYPES)) + if not self._url: + raise ValidationError('A Reference must contain an URL') + if not self._description: + raise ValidationError('A Reference must contain a description') + + +class CVRFAcknowledgment(object): + def __init__(self, names=[], organizations=[], description=None, + url=None): + self._names = names + self._organizations = organizations + self._description = description + self._url = url + + def getTitle(self): + return "%s - %s" % (', '.join(self._names), + ', '.join(self._organizations)) + + def validate(self): + if (not self._names) and (not self._organizations) and (not self._description): + raise ValidationError('An Acknowledgment must have at least a Name, an Organization or a Description') + + diff -r 3cab052872f4 -r 809db989cac5 farolluz/cvrf.py --- a/farolluz/cvrf.py Fri Oct 24 16:43:31 2014 +0200 +++ b/farolluz/cvrf.py Fri Oct 24 17:01:26 2014 +0200 @@ -21,1132 +21,16 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. """\ -Objects related to CVRF Documents +This is the public interface to the CVRF Elements """ -class ValidationError(Exception): pass - -class CVRFPublisher(object): - TYPES = ('Vendor', 'Discoverer', 'Coordinator', 'User', 'Other') - def __init__(self, _type, vendorid=None): - self._type = _type - self._vendorid = vendorid - self._contact = None - self._authority = None - - def setContact(self, contact): - self._contact = contact - - def setAuthority(self, authority): - self._authority = authority - - def validate(self): - if not self._type: - raise ValidationError('Document Publisher needs to have a type') - if self._type not in self.TYPES: - raise ValidationError('Document Publisher Type needs to be one of %s' % ', '.join(self.TYPES)) - - def __str__(self): - s = 'CVRFPublisher: %s' % self._type - if self._vendorid is not None: - s += ' ID: %s' % self._vendorid - if self._contact is not None: - s += ' Contact: "%s"' % self._contact - if self._authority is not None: - s += ' Authority: "%s"' % self._authority - return s - - -class CVRFTrackingID(object): - def __init__(self, _id): - self._id = _id - self._aliases = [] - - def addAlias(self, alias): - self._aliases.append(alias) - - def getId(self): - return self._id - - def validate(self): - if not self._id: - raise ValidationError('Document ID cannot be left empty') - - def __str__(self): - if self._aliases: - return "%s (%s)" % (self._id, ', '.join(self._aliases)) - return self._id - - -class CVRFTracking(object): - STATUSES = ('Draft', 'Interim', 'Final') - def __init__(self, _id, status, version, initial, current): - self._identification = _id - self._status = status - self._version = version - self._history = [] - self._initialDate = initial - self._currentDate = current - self._generator = None - - def addRevision(self, revision): - self._history.append(revision) - - def setGenerator(self, generator): - self._generator = generator - - def getId(self): - return self._identification.getId() - - def validate(self): - if self._identification is None: - raise ValidationError('Document Tracking needs to have an Identification') - self._identification.validate() - if not self._status: - raise ValidationError('Document status must be set') - if self._status not in self.STATUSES: - raise ValidationError('Document Status must be one of %s' % ', '.join(self.STATUSES)) - if not self._version: - raise ValidationError('Document Version must be set') - if len(self._version) > 4: - raise ValidationError('Document Version must be comprised between `nn` and `nn.nn.nn.nn`') - if not self._history: - raise ValidationError('Document must have at least a revision') - if not self._initialDate: - raise ValidationError('Document must have an initial Release date set') - prev_date = self._initialDate - if self._history[0]._date < self._initialDate: - # Documents could have revisions before being released - prev_date = self._history[0]._date - prev = () - for revision in self._history: - revision.validate() - if revision._number <= prev: - raise ValidationError('Revision numbers must always be increasing') - if revision._date < prev_date: - raise ValidationError('Revision dates must always be increasing') - prev = revision._number - prev_date = revision._date - if not self._currentDate: - raise ValidationError('Document must have a Current Release Date set') - if self._currentDate != self._history[-1]._date: - raise ValidationError('Current Release Date must be the same as the Date from the last Revision') - if self._initialDate > self._currentDate: - raise ValidationError('Initial date must not be after current Date') - if self._version != self._history[-1]._number: - raise ValidationError('Document version must be the same as the number of the last Revision') - - def __str__(self): - s = "ID: %s" % self._identification - s += " Status: %s" % self._status - s += " v%s" % '.'.join('%d' % i for i in self._version) - s += " %d revisions" % len(self._history) - s += " Initial release: %s" % self._initialDate.isoformat() - return s - - -class CVRFRevision(object): - def __init__(self, number, date, description): - self._number = number - self._date = date - self._description = description - - def validate(self): - if not self._number: - raise ValidationError('A Revision must have a Number') - if not self._date: - raise ValidationError('A Revision must have a Date') - if not self._description: - raise ValidationError('A Revision must have a Description') - -class CVRFGenerator(object): - def __init__(self): - self._engine = None - self._date = None - - def setEngine(self, engine): - self._engine = engine - - def setDate(self, date): - self._date = date - - def validate(self): - if (not self._engine) and (not self._date): - raise ValidationError('The Generator must have at least an Engine or a Date') - - -class CVRFNote(object): - TYPES = ('General', 'Details', 'Description', 'Summary', 'FAQ', - 'Legal Disclaimer', 'Other') - def __init__(self, _type, ordinal, note, title=None, audience=None): - self._type = _type - self._ordinal = ordinal - self._note = note - self._title = title - self._audience = audience - - def getTitle(self): - """ returns something that can be used as a title """ - if self._title is None: - return "%s (#%d)" % (self._type, self._ordinal) - return "%s (%s)" % (self._title, self._type) - - def validate(self): - if not self._type: - raise ValidationError('A Note needs to have a Type set') - if self._type not in self.TYPES: - raise ValidationError('A Note Type needs to be one of %s' % ', '.join(self.TYPES)) - if self._ordinal < 0: - raise ValidationError('A Note ordinal must be a positive integer') - if not self._note: - raise ValidationError('A Note must contain some text') - - - def __str__(self): - return self._note - - -class CVRFAggregateSeverity(object): - def __init__(self, severity): - self._severity = severity - self._namespace = None - - def setNamespace(self, namespace): - self._namespace = namespace - -class CVRFReference(object): - TYPES = ('Self', 'External') - def __init__(self, url, description, _type=None): - self._url = url - self._description = description - self._type = _type - - def validate(self): - if (self._type is not None) and (self._type not in self.TYPES): - raise ValidationError('If a Reference type is set, it mist be one of %s' % ', '.join(self.TYPES)) - if not self._url: - raise ValidationError('A Reference must contain an URL') - if not self._description: - raise ValidationError('A Reference must contain a description') - - -class CVRFAcknowledgment(object): - def __init__(self, names=[], organizations=[], description=None, - url=None): - self._names = names - self._organizations = organizations - self._description = description - self._url = url - - def getTitle(self): - return "%s - %s" % (', '.join(self._names), - ', '.join(self._organizations)) - - def validate(self): - if (not self._names) and (not self._organizations) and (not self._description): - raise ValidationError('An Acknowledgment must have at least a Name, an Organization or a Description') - - -class CVRFProductTree(object): - def __init__(self): - # All the branches, they can be order with their `parent` attribute - self._branches = [] - self._groups = [] - self._relationships = [] - self._products = [] - self._groups = [] - - def addProduct(self, product): - """ Add to the product list """ - self._products.append(product) - - def addRelationship(self, rel): - self._relationships.append(rel) - - def addGroup(self, group): - self._groups.append(group) - - def getProductForID(self, productid): - for product in self._products: - if product._productid == productid: - return product - raise KeyError(productid) - - def getGroupForID(self, groupid): - for group in self._groups: - if group._groupid == groupid: - return group - raise KeyError(groupid) - - def decomposeProduct(self, productid): - """ In case of product defined as a relationship (product X installed - on OS Y), this gives us the following tuple: (OS, product). """ - product = self.getProductForID(productid) - parent = product._parent - if parent is None: - return (None, None) - if not isinstance(parent, CVRFRelationship): - return (None, None) - relationtype = parent._relationtype.replace(' ', '').lower() - if relationtype not in ('defaultcomponentof', 'installedon'): - return (None, None) - return ( - self.getProductForID(parent._relatestoproductreference), - self.getProductForID(parent._productreference) - ) - - def getBranch(self, path): - if len(path) == 0: - return self - branches = self._branches - node = None - for idx in path: - node = branches[idx] - branches = node._childs - return node - - def getBranches(self): - for branch in self._branches: - yield branch - for sub_branch in branch.getBranches(): - yield sub_branch - - def getPath(self): - return () - - def getNameOfRelationship(self, relationship): - if relationship is None: - return '' - return ' '.join((self.getProductForID(relationship._productreference)._name, 'as', - relationship._relationtype.lower(), - self.getProductForID(relationship._relatestoproductreference)._name)) - - def getOrphanedBranches(self, product=None): - """ The branches that could accept `product` as Product Definition """ - white_list = [] - if product is not None: - white_list = [product._parent] - for branch in self.getBranches(): - if (branch in white_list) or branch.isOrphaned(): - yield branch - - def getNotTerminalBranches(self, b2=None): - """\ - The branches that could accept `b2` as new sub-branches - Note that b2 and all its sub-branches cannot be listed - """ - black_list = [] - if b2 is not None: - black_list = [b2] + list(b2.getBranches()) - for branch in self.getBranches(): - if branch in black_list: - continue - if branch._product is None: - yield branch - - def getOrphanedRelationships(self, product=None): - """ The relationships that need a product defninition """ - white_list = [] - if product is not None: - white_list = [product.getCurrentRelationship()] - for i, relationship in enumerate(self._relationships): - if (relationship in white_list) or relationship.isOrphaned(): - yield (i, relationship) - - def nbProducts(self): - """ Amount of 'raw' Products """ - return len([p for p in self._products if p._parent is self]) - - def validate(self): - for branch in self._branches: - branch.validate() - productids = set() - for product in self._products: - product.validate() - if product._productid in productids: - raise ValidationError('Each ProductID must be unique (%s)' % product._productid) - productids.add(product._productid) - for relationship in self._relationships: - relationship.validate() - for productid in (relationship._productreference, - relationship._relatestoproductreference): - if productid not in productids: - raise ValidationError('ProductID %s is unknown' % productid) - groupids = set() - for group in self._groups: - group.validate() - if group._groupid in groupids: - raise ValidationError('Duplicated GroupID: %s' % group._groupid) - groupids.add(group._groupid) - for productid in group._productids: - if productid not in productids: - raise ValidationError('ProductID %s is unknown' % productid) - return productids, groupids - - def __str__(self): - return 'Products: %s' % '\n'.join(str(p) for p in self._products) - - -class CVRFProductBranch(object): - TYPES = ('Vendor', 'Product Family', 'Product Name', 'Product Version', - 'Patch Level', 'Service Pack', 'Architecture', 'Language', - 'Legacy', 'Specification') - def __init__(self, _type, name, parentbranch): - self._type = _type - self._name = name - self._childs = [] - self._product = None - self.link(parentbranch) - - def getParent(self): - return self._parentbranch - - def getPath(self, string=False): - """ return the path to that branch element as a tuple """ - if self.isRoot(): - for i, b in enumerate(self._parentbranch._branches): - if b is self: - if string: - return '%d' % i - return (i, ) - else: - for i, b in enumerate(self._parentbranch._childs): - if b is self: - if string: - return '/'.join([self._parentbranch.getPath(string), '%d' % i]) - return self._parentbranch.getPath(string) + (i,) - if string: - return '' - return () - - def getTree(self): - """ this returns a list of tuples (type, name) leading to here""" - if self.isRoot(): - return [(self._type, self._name)] - return self._parentbranch.getTree() + [(self._type, self._name)] - - def getName(self): - return ' / '.join("%s: %s" % (type_, name) for type_, name in self.getTree()) - - def getParentPath(self): - """ return as string the path to the parent """ - return '/'.join('%s' % p for p in self.getPath()[:-1]) - - def isRoot(self): - return isinstance(self._parentbranch, CVRFProductTree) - - def isOrphaned(self): - """ Has no childs and no product """ - return len(self._childs) == 0 and (self._product is None) - - def getBranches(self): - for branch in self._childs: - yield branch - for sub_branch in branch.getBranches(): - yield sub_branch - - def unlink(self): - """ Unset our _parent, and remove us from the _parent._childs """ - if self.isRoot(): - self.getParent()._branches.remove(self) - else: - self.getParent()._childs.remove(self) - self._parentbranch = None - - def link(self, parent): - """ Actually, only set the parent """ - self._parentbranch = parent - if self.isRoot(): - parent._branches.append(self) - else: - parent._childs.append(self) - - - def validate(self): - if not self._type: - raise ValidationError('A Branch must have a Type') - if self._type not in self.TYPES: - raise ValidationError('A Branch Type must be one of %s' % ', '.join(self.TYPES)) - if not self._name: - raise ValidationError('A Branch must have a Name') - for branch in self._childs: - branch.validate() - if self.isOrphaned(): - raise ValidationError('A Branch must have at least a sub-product or sub-branches') - - def __str__(self): - return "%s: %s" % (self._type, self._name) - - -class CVRFFullProductName(object): - def __init__(self, productid, name, parent, cpe=None): - self._productid = productid - self._name = name - self._cpe = cpe - # Can be None (directly under the tree), a ProductBranch, or a - # Relationship - self.link(parent) - - def isRoot(self): - return isinstance(self._parent, CVRFProductTree) - - def isRelationship(self): - return isinstance(self._parent, CVRFRelationship) - - def getTree(self): - if not isinstance(self._parent, CVRFProductBranch): - return [] - return self._parent.getTree() - - def getParentPath(self): - if self.isRoot() or self.isRelationship(): - return '' - return self._parent.getPath(True) - - def getCurrentRelationship(self): - if self.isRelationship(): - return self._parent - return None - - def unlink(self): - """ Unset our _parent, and remove us from the _parent._childs - We are still in the product list. - """ - if not self.isRoot(): - self._parent._product = None - self._parent = None - - def link(self, parent): - self._parent = parent - if not self.isRoot(): - parent._product = self - - def validate(self): - if not self._productid: - raise ValidationError('A Product must have a ProductID') - if not self._name: - raise ValidationError('A Product must have a Name') - - def __str__(self): - return "%s (%s)" % (self._productid, self._name) - - -class CVRFRelationship(object): - TYPES = ('Default Component Of', 'Optional Component Of', - 'External Component Of', 'Installed On', 'Installed With') - def __init__(self, productref, reltype, relatestoproductref): - self._productreference = productref - self._relationtype = reltype - self._relatestoproductreference = relatestoproductref - self._product = None - - def getParent(self): - """ All parent element of a FullProductName should implement that - method """ - return None - - def isOrphaned(self): - return self._product is None - - def validate(self): - if not self._productreference: - raise ValidationError('A Relationship must have a Product Reference') - if not self._relationtype: - raise ValidationError('A Relationship must have a Relation Type') - if self._relationtype not in self.TYPES: - raise ValidationError('Relation Type must be one of %s' % ', '.join(self.TYPES)) - if not self._relatestoproductreference: - raise ValidationError('A Relationship must have a "Relates To product Reference"') - if self._productreference == self._relatestoproductreference: - raise ValidationError('A Relationship cannot reference twice the same Product') - - -class CVRFGroup(object): - def __init__(self, groupid): - self._groupid = groupid - self._description = None - self._productids = [] - - def setDescription(self, description): - self._description = description - - def addProductID(self, productid): - self._productids.append(productid) - - def getTitle(self): - if self._description: - return "%s (%d products)" % (self._description, len(self._productids)) - return "#%s (%d products)" % (self._groupid, len(self._productids)) - - def validate(self): - if not self._groupid: - raise ValidationError('A Group must have a GroupID') - if not self._productids or len(self._productids) < 2: - raise ValidationError('A Group must contain at least two products') - +from .common import (CVRFNote, CVRFReference, CVRFAcknowledgment, + ValidationError) +from .document import (CVRF, CVRFPublisher, CVRFTracking, CVRFTrackingID, + CVRFAggregateSeverity, CVRFRevision, CVRFGenerator) +from .producttree import (CVRFProductBranch, CVRFFullProductName, + CVRFRelationship, CVRFGroup) +from .vulnerability import (CVRFVulnerability, CVRFVulnerabilityID, + CVRFThreat, CVRFProductStatus, CVRFCVSSSet, CVRFRemediation, + CVRFInvolvement, CVRFCWE) -class CVRFVulnerabilityID(object): - def __init__(self, systemname, value): - self._systemname = systemname - self._value = value - - def validate(self): - if not self._systemname: - raise ValidationError('A Vulnerability ID must have a System Name') - if not self._value: - raise ValidationError('A Vulnerability ID must have a value') - - -class CVRFVulnerability(object): - def __init__(self, ordinal): - self._ordinal = ordinal - self._title = None - self._id = None - self._notes = [] - self._discoverydate = None - self._releasedate = None - self._involvements = [] - self._cve = None - self._cwes = [] - self._productstatuses = [] - self._threats = [] - self._cvsss = [] - self._remediations = [] - self._references = [] - self._acknowledgments = [] - - def setTitle(self, title): - self._title = title - - def setID(self, _id): - self._id = _id - - def addNote(self, note): - self._notes.append(note) - - def setDiscoveryDate(self, date): - self._discoverydate = date - - def setReleaseDate(self, date): - self._releasedate = date - - def addInvolvement(self, involvement): - self._involvements.append(involvement) - - def setCVE(self, cve): - self._cve = cve - - def addCWE(self, cwe): - self._cwes.append(cwe) - - def addProductStatus(self, productstatus): - self._productstatuses.append(productstatus) - - def addThreat(self, threat): - self._threats.append(threat) - - def addCVSSSet(self, cvss_set): - self._cvsss.append(cvss_set) - - def addRemediation(self, remediation): - self._remediations.append(remediation) - - def addReference(self, ref): - self._references.append(ref) - - def addAcknowledgment(self, ack): - self._acknowledgments.append(ack) - - def getTitle(self): - """ return something that can be used as a title """ - if self._title: - if self._id: - return "%s (%s)" % (self._title, self._id._value) - return self._title - if self._id: - return self._id._value - return "#%d" % self._ordinal - - def getNote(self, ordinal): - for note in self._notes: - if note._ordinal == ordinal: - return note - return None - - def mentionsProdId(self, productid): - """ Returns in which sub element, self is mentioning the productid """ - for category in (self._productstatuses, self._threats, self._cvsss, self._remediations): - for subelem in category: - if productid in subelem._productids: - yield subelem - - def isMentioningProdId(self, productid): - """ Returns if self is mentioning the productid """ - for e in self.mentionsProdId(productid): - # We only need to know if the generator yield at least one elem. - return True - return False - - def mentionsGroupId(self, groupid): - for category in (self._threats, self._remediations): - for subelem in category: - if groupid in subelem._groupids: - yield subelem - - def isMentioningGroupId(self, groupids): - """ Make sure you call this with a list (not a generator or a tuple) - when wished """ - if not isinstance(groupids, list): - groupids = [groupids] - for groupid in groupids: - for _ in self.mentionsGroupId(groupid): - # We only need to know if the generator yield at least one elem. - return True - return False - - def validate(self, productids, groupids): - if not self._ordinal: - raise ValidationError('A Vulnerability must have an ordinal') - if self._id is not None: - self._id.validate() - ordinals = set() - for note in self._notes: - note.validate() - if note._ordinal in ordinals: - raise ValidationError('Vulnerability Note Ordinal %d duplicated' % note._ordinal) - ordinals.add(note._ordinal) - for involvement in self._involvements: - involvement.validate() - for cwe in self._cwes: - cwe.validate() - for status in self._productstatuses: - status.validate(productids) - pids = set() - for status in self._productstatuses: - for pid in status._productids: - if pid in pids: - raise ValidationError('ProductID %s mentionned in two different ProductStatuses for Vulnerability %d' % (pid, self._ordinal)) - pids.add(pid) - for threat in self._threats: - threat.validate(productids, groupids) - for cvss in self._cvsss: - cvss.validate(productids) - pids = set() - for cvss in self._cvsss: - for pid in (cvss._productids or productids): - if pid in pids: - raise ValidationError('ProductID %s mentionned in two different CVSS Score Sets for Vulnerability %d' % (pid, self._ordinal)) - pids.add(pid) - for remediation in self._remediations: - remediation.validate(productids, groupids) - for reference in self._references: - reference.validate() - for acknowledgment in self._acknowledgments: - acknowledgment.validate() - - -class CVRFInvolvement(object): - PARTIES = CVRFPublisher.TYPES - STATUSES = ('Open', 'Disputed', 'In Progress', 'Completed', - 'Contact Attempted', 'Not Contacted') - def __init__(self, party, status): - self._party = party - self._status = status - self._description = None - - def setDescription(self, description): - self._description = description - - def getTitle(self): - return "From %s: %s" % (self._party, self._status) - - def validate(self): - if not self._party: - raise ValidationError('An Involvement must have a Party') - if self._party not in self.PARTIES: - raise ValidationError("An Involvement's Party must be one of %s" % ', '.join(self.PARTIES)) - if not self._status: - raise ValidationError('An Involvement must have a Status') - if self._status not in self.STATUSES: - raise ValidationError("An Involvement's Status must be one of %s" % ', '.join(self.STATUSES)) - - -class CVRFCWE(object): - def __init__(self, _id, value): - self._id = _id - self._value = value - - def validate(self): - if not self._id: - raise ValidationError('A CWE must have an ID') - if not self._value: - raise ValidationError('A CWE must have a description') - - -class CVRFProductStatus(object): - TYPES = ('First Affected', 'Known Affected', 'Known Not Affected', - 'First Fixed', 'Fixed', 'Recommended', 'Last Affected') - NAME = "Product Status" - def __init__(self, _type): - self._type = _type - self._productids = [] - - def addProductID(self, productid): - self._productids.append(productid) - - def getTitle(self): - return "%s: %d products" % (self._type, len(self._productids)) - - def validate(self, productids): - if not self._type: - raise ValidationError('A Product Status must have a Type') - if self._type not in self.TYPES: - raise ValidationError("A Product Status' Type must be one of %s" % ', '.join(self.TYPES)) - if len(self._productids) < 1: - raise ValidationError('A Product Status must mention at least one Product') - for productid in self._productids: - if productid not in productids: - raise ValidationError('Unknown ProductID: %s' % productid) - - -class CVRFThreat(object): - TYPES = ('Impact', 'Exploit Status', 'Target Set') - NAME = "Threat" - def __init__(self, _type, description): - self._type = _type - self._description = description - self._date = None - self._productids = [] - self._groupids = [] - - def setDate(self, date): - self._date = date - - def addProductID(self, productid): - self._productids.append(productid) - - def addGroupID(self, groupid): - self._groupids.append(groupid) - - def getTitle(self): - return self._type - - def validate(self, productids, groupids): - if not self._type: - raise ValidationError('A Threat must have a Type') - if self._type not in self.TYPES: - raise ValidationError("A Threat's Type must be one of %s" % ', '.join(self.TYPES)) - if not self._description: - raise ValidationError('A Threat must have a Description') - for productid in self._productids: - if productid not in productids: - raise ValidationError('Unknown ProductID: %s' % productid) - for groupid in self._groupids: - if groupid not in groupids: - raise ValidationError('Unknown GroupID: %s' % groupid) - - -class CVRFCVSSSet(object): - # To determine the base Score - VALUES = {'AV': {'L':0.395, 'A':0.646, 'N':1.0}, - 'AC': {'H':0.35, 'M':0.61 ,'L':0.71}, - 'Au': {'M':0.45, 'S':0.56, 'N':0.704}, - 'C': {'N':0.0, 'P':0.275, 'C':0.66}, - 'I': {'N':0.0, 'P':0.275, 'C':0.66}, - 'A': {'N':0.0, 'P':0.275, 'C':0.66}} - NAME = "CVSS Score Set" - def __init__(self, basescore): - self._basescore = basescore - self._temporalscore = None - self._environmentalscore = None - self._vector = None - self.vector = None - self._productids = [] - - def setTemporalScore(self, tempscore): - self._temporalscore = tempscore - - def setEnvironmentalScore(self, envscore): - self._environmentalscore = envscore - - def setVector(self, vector): - self._vector = vector - if vector is None: - self.vector = vector - return - try: - self.vector = {} - for component in vector[:26].split('/'): - name, value = component.split(':') - self.vector[name] = self.VALUES[name][value] - except (KeyError, ValueError): - self.vector = None - - def addProductID(self, productid): - self._productids.append(productid) - - def baseScore(self): - v = self.vector # make an alias for shorter lines - exploitability = 20 * v['AV'] * v['AC'] * v['Au'] - impact = 10.41 * (1 - (1 - v['C']) * (1 - v['I']) * (1 - v['A'])) - def f(i): return 0 if i == 0 else 1.176 - return ((0.6 * impact) + (0.4 * exploitability) - 1.5) * f(impact) - - def validate(self, productids): - if not self._basescore: - raise ValidationError('A CVSS Score Set must have a Base Score') - if self._vector and not self.vector: - raise ValidationError('Syntax Error in CVSS Vector') - if self.vector and (abs(self._basescore - self.baseScore()) >= 0.05): - raise ValidationError('Inconsistency in CVSS Score Set between Vector (%f) and Base Score (%f)' % (self.baseScore(), self._basescore)) - for productid in self._productids: - if productid not in productids: - raise ValidationError('Unknown ProductID: %s' % productid) - - -class CVRFRemediation(object): - TYPES = ('Workaround', 'Mitigation', 'Vendor Fix', 'None Available', - 'Will Not Fix') - NAME = "Remediation" - def __init__(self, _type, description): - self._type = _type - self._description = description - self._date = None - self._entitlement = None - self._url = None - self._productids = [] - self._groupids = [] - - def setDate(self, date): - self._date = date - - def setEntitlement(self, entitlement): - self._entitlement = entitlement - - def setURL(self, url): - self._url = url - - def addProductID(self, productid): - self._productids.append(productid) - - def addGroupID(self, groupid): - self._groupids.append(groupid) - - def getTitle(self): - return self._type - - def validate(self, productids, groupids): - if not self._type: - raise ValidationError('A Remediation must have a Type') - if self._type not in self.TYPES: - raise ValidationError("A Remediation's Type must be one of %s" % ', '.join(self.TYPES)) - if not self._description: - raise ValidationError('A Remediation must have a Description') - for productid in self._productids: - if productid not in productids: - raise ValidationError('Unknown ProductID: %s' % productid) - for groupid in self._groupids: - if groupid not in groupids: - raise ValidationError('Unknown GroupID: %s' % groupid) - - -class CVRF(object): - def __init__(self, title, _type): - self._title = title - self._type = _type - self._publisher = None - self._tracking = None - self._notes = [] - self._distribution = None - self._aggregateseverity = None - self._references = [] - self._acknowledgments = [] - self._producttree = None - self._vulnerabilities = [] - - def setPublisher(self, publisher): - self._publisher = publisher - - def setTracking(self, tracking): - self._tracking = tracking - - def addNote(self, note): - self._notes.append(note) - - def setDistribution(self, distribution): - self._distribution = distribution - - def setAggregateSeverity(self, aggregateseverity): - self._aggregateseverity = aggregateseverity - - def addReference(self, ref): - self._references.append(ref) - - def addAcknowledgment(self, ack): - self._acknowledgments.append(ack) - - def createProductTree(self): - """ only done if the element is there """ - self._producttree = CVRFProductTree() - return self._producttree - - def addVulnerability(self, vuln): - self._vulnerabilities.append(vuln) - - def getProductForID(self, productid): - if self._producttree is None: - raise ValueError('No ProductTree') - return self._producttree.getProductForID(productid) - - def getGroupForID(self, groupid): - if self._producttree is None: - raise ValueError('No ProductTree') - return self._producttree.getGroupForID(groupid) - - def getHighestCVSS(self): - highestBaseScore = 0 - highest = None - for vulnerability in self._vulnerabilities: - for cvss in vulnerability._cvsss: - if cvss._basescore <= highestBaseScore: - continue - highestBaseScore = cvss._basescore - highest = cvss - return highest - - def getProductList(self, type_='Fixed'): - products = set() - if type_ == 'Fixed': - # First try through the Remediation - for vulnerability in self._vulnerabilities: - for remediation in vulnerability._remediations: - if remediation._type != 'Vendor Fix': - continue - for productid in remediation._productids: - products.add(productid) - for groupid in remediation._groupids: - for productid in self.getGroupForID(groupid)._productids: - products.add(productid) - if not products: - # If nothing there, try through the productstatuses - for vulnerability in self._vulnerabilities: - for status in vulnerability._productstatuses: - if status._type != type_: - continue - for productid in status._productids: - products.add(productid) - return set(self.getProductForID(p) for p in products) - - def mentionsProductId(self, productid): - # We first look at the ProductTree - ptree = self._producttree - for relation in ptree._relationships: - if productid == relation._productreference: - yield relation - elif productid == relation._relatestoproductreference: - yield relation - # Then go through the groups - for group in ptree._groups: - if productid in group._productids: - yield group - # Finally, go through all the Vulnerabilities - for vulnerability in self._vulnerabilities: - for item in vulnerability.mentionsProdId(productid): - yield item - - def isProductOrphan(self, productid): - """ Returns if a productid is mentioned nowhere in the document """ - for item in self.mentionsProductId(productid): - return True - return False - - def changeProductID(self, old, new): - for item in self.mentionsProductId(old): - if isinstance(item, CVRFRelationship): - if old == item._productreference: - item._productreference = new - elif old == item._relatestoproductreference: - item._relatestoproductreference = new - else: - item._productids.remove(old) - item._productids.append(new) - - def isGroupOrphan(self, groupid): - """ Returns if a group can be safely deleted """ - for vulnerability in self._vulnerabilities: - if vulnerability.isMentioningGroupId(groupid): - return False - return True - - def isProductTreeOrphan(self): - """ Difference with the previous method is that we don;t care about - inter-producttree references """ - for vulnerability in self._vulnerabilities: - for product in self._producttree._products: - if vulnerability.isMentioningProdId(product._productid): - return False - for group in self._producttree._groups: - if vulnerability.isMentioningGroupId(group._groupid): - return False - return True - - def getNote(self, ordinal): - for note in self._notes: - if note._ordinal == ordinal: - return note - return None - - def getDocId(self): - if self._tracking is not None: - return self._tracking.getId() - # Make up something ... - return self._title.lower() - - def validate(self): - if not self._title: - raise ValidationError('Document Title cannot be empty') - if not self._type: - raise ValidationError('Document Type cannot be empty') - if self._publisher is None: - raise ValidationError('Document Publisher needs to be set') - self._publisher.validate() - if self._tracking is None: - raise ValidationError('Document Tracking needs to be set') - self._tracking.validate() - ordinals = set() - for note in self._notes: - note.validate() - if note._ordinal in ordinals: - raise ValidationError('Document Note ordinal %d is issued twice' % note._ordinal) - ordinals.add(note._ordinal) - for reference in self._references: - reference.validate() - for acknowledgment in self._acknowledgments: - acknowledgment.validate() - productids = set() - groupids = set() - if self._producttree: - productids, groupids = self._producttree.validate() - ordinals = set() - for vulnerability in self._vulnerabilities: - vulnerability.validate(productids, groupids) - if vulnerability._ordinal in ordinals: - raise ValidationError('Vulnerability ordinal %d is issued twice' % vulnerability._ordinal) - ordinals.add(vulnerability._ordinal) - - def __str__(self): - s = [ - 'Title: %s' % self._title, - 'Type: %s' % self._type, - 'Publisher: %s' % self._publisher, - 'tracking: %s' % self._tracking, - '%d Notes: %s' % (len(self._notes), ', '.join( - str(n) for n in self._notes)) - ] - if self._distribution is not None: - s.append('Distribution: %s' % self._distribution) - s.extend([ - '%d Acknowledgments' % len(self._acknowledgments), - 'Products: %s' % self._producttree, - ]) - return '\n'.join(s) diff -r 3cab052872f4 -r 809db989cac5 farolluz/document.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/document.py Fri Oct 24 17:01:26 2014 +0200 @@ -0,0 +1,387 @@ +# -*- coding: utf-8 -*- +# +# Authors: +# Benoît Allard +# +# 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. + +"""\ +Objects related to CVRF Documents +""" + +from .common import ValidationError +from .producttree import CVRFProductTree, CVRFRelationship + +class CVRFPublisher(object): + TYPES = ('Vendor', 'Discoverer', 'Coordinator', 'User', 'Other') + def __init__(self, _type, vendorid=None): + self._type = _type + self._vendorid = vendorid + self._contact = None + self._authority = None + + def setContact(self, contact): + self._contact = contact + + def setAuthority(self, authority): + self._authority = authority + + def validate(self): + if not self._type: + raise ValidationError('Document Publisher needs to have a type') + if self._type not in self.TYPES: + raise ValidationError('Document Publisher Type needs to be one of %s' % ', '.join(self.TYPES)) + + def __str__(self): + s = 'CVRFPublisher: %s' % self._type + if self._vendorid is not None: + s += ' ID: %s' % self._vendorid + if self._contact is not None: + s += ' Contact: "%s"' % self._contact + if self._authority is not None: + s += ' Authority: "%s"' % self._authority + return s + + +class CVRFTrackingID(object): + def __init__(self, _id): + self._id = _id + self._aliases = [] + + def addAlias(self, alias): + self._aliases.append(alias) + + def getId(self): + return self._id + + def validate(self): + if not self._id: + raise ValidationError('Document ID cannot be left empty') + + def __str__(self): + if self._aliases: + return "%s (%s)" % (self._id, ', '.join(self._aliases)) + return self._id + + +class CVRFTracking(object): + STATUSES = ('Draft', 'Interim', 'Final') + def __init__(self, _id, status, version, initial, current): + self._identification = _id + self._status = status + self._version = version + self._history = [] + self._initialDate = initial + self._currentDate = current + self._generator = None + + def addRevision(self, revision): + self._history.append(revision) + + def setGenerator(self, generator): + self._generator = generator + + def getId(self): + return self._identification.getId() + + def validate(self): + if self._identification is None: + raise ValidationError('Document Tracking needs to have an Identification') + self._identification.validate() + if not self._status: + raise ValidationError('Document status must be set') + if self._status not in self.STATUSES: + raise ValidationError('Document Status must be one of %s' % ', '.join(self.STATUSES)) + if not self._version: + raise ValidationError('Document Version must be set') + if len(self._version) > 4: + raise ValidationError('Document Version must be comprised between `nn` and `nn.nn.nn.nn`') + if not self._history: + raise ValidationError('Document must have at least a revision') + if not self._initialDate: + raise ValidationError('Document must have an initial Release date set') + prev_date = self._initialDate + if self._history[0]._date < self._initialDate: + # Documents could have revisions before being released + prev_date = self._history[0]._date + prev = () + for revision in self._history: + revision.validate() + if revision._number <= prev: + raise ValidationError('Revision numbers must always be increasing') + if revision._date < prev_date: + raise ValidationError('Revision dates must always be increasing') + prev = revision._number + prev_date = revision._date + if not self._currentDate: + raise ValidationError('Document must have a Current Release Date set') + if self._currentDate != self._history[-1]._date: + raise ValidationError('Current Release Date must be the same as the Date from the last Revision') + if self._initialDate > self._currentDate: + raise ValidationError('Initial date must not be after current Date') + if self._version != self._history[-1]._number: + raise ValidationError('Document version must be the same as the number of the last Revision') + + def __str__(self): + s = "ID: %s" % self._identification + s += " Status: %s" % self._status + s += " v%s" % '.'.join('%d' % i for i in self._version) + s += " %d revisions" % len(self._history) + s += " Initial release: %s" % self._initialDate.isoformat() + return s + + +class CVRFRevision(object): + def __init__(self, number, date, description): + self._number = number + self._date = date + self._description = description + + def validate(self): + if not self._number: + raise ValidationError('A Revision must have a Number') + if not self._date: + raise ValidationError('A Revision must have a Date') + if not self._description: + raise ValidationError('A Revision must have a Description') + +class CVRFGenerator(object): + def __init__(self): + self._engine = None + self._date = None + + def setEngine(self, engine): + self._engine = engine + + def setDate(self, date): + self._date = date + + def validate(self): + if (not self._engine) and (not self._date): + raise ValidationError('The Generator must have at least an Engine or a Date') + + +class CVRFAggregateSeverity(object): + def __init__(self, severity): + self._severity = severity + self._namespace = None + + def setNamespace(self, namespace): + self._namespace = namespace + + +class CVRF(object): + def __init__(self, title, _type): + self._title = title + self._type = _type + self._publisher = None + self._tracking = None + self._notes = [] + self._distribution = None + self._aggregateseverity = None + self._references = [] + self._acknowledgments = [] + self._producttree = None + self._vulnerabilities = [] + + def setPublisher(self, publisher): + self._publisher = publisher + + def setTracking(self, tracking): + self._tracking = tracking + + def addNote(self, note): + self._notes.append(note) + + def setDistribution(self, distribution): + self._distribution = distribution + + def setAggregateSeverity(self, aggregateseverity): + self._aggregateseverity = aggregateseverity + + def addReference(self, ref): + self._references.append(ref) + + def addAcknowledgment(self, ack): + self._acknowledgments.append(ack) + + def createProductTree(self): + """ only done if the element is there """ + self._producttree = CVRFProductTree() + return self._producttree + + def addVulnerability(self, vuln): + self._vulnerabilities.append(vuln) + + def getProductForID(self, productid): + if self._producttree is None: + raise ValueError('No ProductTree') + return self._producttree.getProductForID(productid) + + def getGroupForID(self, groupid): + if self._producttree is None: + raise ValueError('No ProductTree') + return self._producttree.getGroupForID(groupid) + + def getHighestCVSS(self): + highestBaseScore = 0 + highest = None + for vulnerability in self._vulnerabilities: + for cvss in vulnerability._cvsss: + if cvss._basescore <= highestBaseScore: + continue + highestBaseScore = cvss._basescore + highest = cvss + return highest + + def getProductList(self, type_='Fixed'): + products = set() + if type_ == 'Fixed': + # First try through the Remediation + for vulnerability in self._vulnerabilities: + for remediation in vulnerability._remediations: + if remediation._type != 'Vendor Fix': + continue + for productid in remediation._productids: + products.add(productid) + for groupid in remediation._groupids: + for productid in self.getGroupForID(groupid)._productids: + products.add(productid) + if not products: + # If nothing there, try through the productstatuses + for vulnerability in self._vulnerabilities: + for status in vulnerability._productstatuses: + if status._type != type_: + continue + for productid in status._productids: + products.add(productid) + return set(self.getProductForID(p) for p in products) + + def mentionsProductId(self, productid): + # We first look at the ProductTree + ptree = self._producttree + for relation in ptree._relationships: + if productid == relation._productreference: + yield relation + elif productid == relation._relatestoproductreference: + yield relation + # Then go through the groups + for group in ptree._groups: + if productid in group._productids: + yield group + # Finally, go through all the Vulnerabilities + for vulnerability in self._vulnerabilities: + for item in vulnerability.mentionsProdId(productid): + yield item + + def isProductOrphan(self, productid): + """ Returns if a productid is mentioned nowhere in the document """ + for item in self.mentionsProductId(productid): + return True + return False + + def changeProductID(self, old, new): + for item in self.mentionsProductId(old): + if isinstance(item, CVRFRelationship): + if old == item._productreference: + item._productreference = new + elif old == item._relatestoproductreference: + item._relatestoproductreference = new + else: + item._productids.remove(old) + item._productids.append(new) + + def isGroupOrphan(self, groupid): + """ Returns if a group can be safely deleted """ + for vulnerability in self._vulnerabilities: + if vulnerability.isMentioningGroupId(groupid): + return False + return True + + def isProductTreeOrphan(self): + """ Difference with the previous method is that we don;t care about + inter-producttree references """ + for vulnerability in self._vulnerabilities: + for product in self._producttree._products: + if vulnerability.isMentioningProdId(product._productid): + return False + for group in self._producttree._groups: + if vulnerability.isMentioningGroupId(group._groupid): + return False + return True + + def getNote(self, ordinal): + for note in self._notes: + if note._ordinal == ordinal: + return note + return None + + def getDocId(self): + if self._tracking is not None: + return self._tracking.getId() + # Make up something ... + return self._title.lower() + + def validate(self): + if not self._title: + raise ValidationError('Document Title cannot be empty') + if not self._type: + raise ValidationError('Document Type cannot be empty') + if self._publisher is None: + raise ValidationError('Document Publisher needs to be set') + self._publisher.validate() + if self._tracking is None: + raise ValidationError('Document Tracking needs to be set') + self._tracking.validate() + ordinals = set() + for note in self._notes: + note.validate() + if note._ordinal in ordinals: + raise ValidationError('Document Note ordinal %d is issued twice' % note._ordinal) + ordinals.add(note._ordinal) + for reference in self._references: + reference.validate() + for acknowledgment in self._acknowledgments: + acknowledgment.validate() + productids = set() + groupids = set() + if self._producttree: + productids, groupids = self._producttree.validate() + ordinals = set() + for vulnerability in self._vulnerabilities: + vulnerability.validate(productids, groupids) + if vulnerability._ordinal in ordinals: + raise ValidationError('Vulnerability ordinal %d is issued twice' % vulnerability._ordinal) + ordinals.add(vulnerability._ordinal) + + def __str__(self): + s = [ + 'Title: %s' % self._title, + 'Type: %s' % self._type, + 'Publisher: %s' % self._publisher, + 'tracking: %s' % self._tracking, + '%d Notes: %s' % (len(self._notes), ', '.join( + str(n) for n in self._notes)) + ] + if self._distribution is not None: + s.append('Distribution: %s' % self._distribution) + s.extend([ + '%d Acknowledgments' % len(self._acknowledgments), + 'Products: %s' % self._producttree, + ]) + return '\n'.join(s) diff -r 3cab052872f4 -r 809db989cac5 farolluz/producttree.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/producttree.py Fri Oct 24 17:01:26 2014 +0200 @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +# +# Authors: +# Benoît Allard +# +# 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. + +"""\ +Product Tree Objects related to CVRF Documents +""" + +from .common import ValidationError + +class CVRFProductTree(object): + def __init__(self): + # All the branches, they can be order with their `parent` attribute + self._branches = [] + self._groups = [] + self._relationships = [] + self._products = [] + self._groups = [] + + def addProduct(self, product): + """ Add to the product list """ + self._products.append(product) + + def addRelationship(self, rel): + self._relationships.append(rel) + + def addGroup(self, group): + self._groups.append(group) + + def getProductForID(self, productid): + for product in self._products: + if product._productid == productid: + return product + raise KeyError(productid) + + def getGroupForID(self, groupid): + for group in self._groups: + if group._groupid == groupid: + return group + raise KeyError(groupid) + + def decomposeProduct(self, productid): + """ In case of product defined as a relationship (product X installed + on OS Y), this gives us the following tuple: (OS, product). """ + product = self.getProductForID(productid) + parent = product._parent + if parent is None: + return (None, None) + if not isinstance(parent, CVRFRelationship): + return (None, None) + relationtype = parent._relationtype.replace(' ', '').lower() + if relationtype not in ('defaultcomponentof', 'installedon'): + return (None, None) + return ( + self.getProductForID(parent._relatestoproductreference), + self.getProductForID(parent._productreference) + ) + + def getBranch(self, path): + if len(path) == 0: + return self + branches = self._branches + node = None + for idx in path: + node = branches[idx] + branches = node._childs + return node + + def getBranches(self): + for branch in self._branches: + yield branch + for sub_branch in branch.getBranches(): + yield sub_branch + + def getPath(self): + return () + + def getNameOfRelationship(self, relationship): + if relationship is None: + return '' + return ' '.join((self.getProductForID(relationship._productreference)._name, 'as', + relationship._relationtype.lower(), + self.getProductForID(relationship._relatestoproductreference)._name)) + + def getOrphanedBranches(self, product=None): + """ The branches that could accept `product` as Product Definition """ + white_list = [] + if product is not None: + white_list = [product._parent] + for branch in self.getBranches(): + if (branch in white_list) or branch.isOrphaned(): + yield branch + + def getNotTerminalBranches(self, b2=None): + """\ + The branches that could accept `b2` as new sub-branches + Note that b2 and all its sub-branches cannot be listed + """ + black_list = [] + if b2 is not None: + black_list = [b2] + list(b2.getBranches()) + for branch in self.getBranches(): + if branch in black_list: + continue + if branch._product is None: + yield branch + + def getOrphanedRelationships(self, product=None): + """ The relationships that need a product defninition """ + white_list = [] + if product is not None: + white_list = [product.getCurrentRelationship()] + for i, relationship in enumerate(self._relationships): + if (relationship in white_list) or relationship.isOrphaned(): + yield (i, relationship) + + def nbProducts(self): + """ Amount of 'raw' Products """ + return len([p for p in self._products if p._parent is self]) + + def validate(self): + for branch in self._branches: + branch.validate() + productids = set() + for product in self._products: + product.validate() + if product._productid in productids: + raise ValidationError('Each ProductID must be unique (%s)' % product._productid) + productids.add(product._productid) + for relationship in self._relationships: + relationship.validate() + for productid in (relationship._productreference, + relationship._relatestoproductreference): + if productid not in productids: + raise ValidationError('ProductID %s is unknown' % productid) + groupids = set() + for group in self._groups: + group.validate() + if group._groupid in groupids: + raise ValidationError('Duplicated GroupID: %s' % group._groupid) + groupids.add(group._groupid) + for productid in group._productids: + if productid not in productids: + raise ValidationError('ProductID %s is unknown' % productid) + return productids, groupids + + def __str__(self): + return 'Products: %s' % '\n'.join(str(p) for p in self._products) + + +class CVRFProductBranch(object): + TYPES = ('Vendor', 'Product Family', 'Product Name', 'Product Version', + 'Patch Level', 'Service Pack', 'Architecture', 'Language', + 'Legacy', 'Specification') + def __init__(self, _type, name, parentbranch): + self._type = _type + self._name = name + self._childs = [] + self._product = None + self.link(parentbranch) + + def getParent(self): + return self._parentbranch + + def getPath(self, string=False): + """ return the path to that branch element as a tuple """ + if self.isRoot(): + for i, b in enumerate(self._parentbranch._branches): + if b is self: + if string: + return '%d' % i + return (i, ) + else: + for i, b in enumerate(self._parentbranch._childs): + if b is self: + if string: + return '/'.join([self._parentbranch.getPath(string), '%d' % i]) + return self._parentbranch.getPath(string) + (i,) + if string: + return '' + return () + + def getTree(self): + """ this returns a list of tuples (type, name) leading to here""" + if self.isRoot(): + return [(self._type, self._name)] + return self._parentbranch.getTree() + [(self._type, self._name)] + + def getName(self): + return ' / '.join("%s: %s" % (type_, name) for type_, name in self.getTree()) + + def getParentPath(self): + """ return as string the path to the parent """ + return '/'.join('%s' % p for p in self.getPath()[:-1]) + + def isRoot(self): + return isinstance(self._parentbranch, CVRFProductTree) + + def isOrphaned(self): + """ Has no childs and no product """ + return len(self._childs) == 0 and (self._product is None) + + def getBranches(self): + for branch in self._childs: + yield branch + for sub_branch in branch.getBranches(): + yield sub_branch + + def unlink(self): + """ Unset our _parent, and remove us from the _parent._childs """ + if self.isRoot(): + self.getParent()._branches.remove(self) + else: + self.getParent()._childs.remove(self) + self._parentbranch = None + + def link(self, parent): + """ Actually, only set the parent """ + self._parentbranch = parent + if self.isRoot(): + parent._branches.append(self) + else: + parent._childs.append(self) + + + def validate(self): + if not self._type: + raise ValidationError('A Branch must have a Type') + if self._type not in self.TYPES: + raise ValidationError('A Branch Type must be one of %s' % ', '.join(self.TYPES)) + if not self._name: + raise ValidationError('A Branch must have a Name') + for branch in self._childs: + branch.validate() + if self.isOrphaned(): + raise ValidationError('A Branch must have at least a sub-product or sub-branches') + + def __str__(self): + return "%s: %s" % (self._type, self._name) + + +class CVRFFullProductName(object): + def __init__(self, productid, name, parent, cpe=None): + self._productid = productid + self._name = name + self._cpe = cpe + # Can be None (directly under the tree), a ProductBranch, or a + # Relationship + self.link(parent) + + def isRoot(self): + return isinstance(self._parent, CVRFProductTree) + + def isRelationship(self): + return isinstance(self._parent, CVRFRelationship) + + def getTree(self): + if not isinstance(self._parent, CVRFProductBranch): + return [] + return self._parent.getTree() + + def getParentPath(self): + if self.isRoot() or self.isRelationship(): + return '' + return self._parent.getPath(True) + + def getCurrentRelationship(self): + if self.isRelationship(): + return self._parent + return None + + def unlink(self): + """ Unset our _parent, and remove us from the _parent._childs + We are still in the product list. + """ + if not self.isRoot(): + self._parent._product = None + self._parent = None + + def link(self, parent): + self._parent = parent + if not self.isRoot(): + parent._product = self + + def validate(self): + if not self._productid: + raise ValidationError('A Product must have a ProductID') + if not self._name: + raise ValidationError('A Product must have a Name') + + def __str__(self): + return "%s (%s)" % (self._productid, self._name) + + +class CVRFRelationship(object): + TYPES = ('Default Component Of', 'Optional Component Of', + 'External Component Of', 'Installed On', 'Installed With') + def __init__(self, productref, reltype, relatestoproductref): + self._productreference = productref + self._relationtype = reltype + self._relatestoproductreference = relatestoproductref + self._product = None + + def getParent(self): + """ All parent element of a FullProductName should implement that + method """ + return None + + def isOrphaned(self): + return self._product is None + + def validate(self): + if not self._productreference: + raise ValidationError('A Relationship must have a Product Reference') + if not self._relationtype: + raise ValidationError('A Relationship must have a Relation Type') + if self._relationtype not in self.TYPES: + raise ValidationError('Relation Type must be one of %s' % ', '.join(self.TYPES)) + if not self._relatestoproductreference: + raise ValidationError('A Relationship must have a "Relates To product Reference"') + if self._productreference == self._relatestoproductreference: + raise ValidationError('A Relationship cannot reference twice the same Product') + + +class CVRFGroup(object): + def __init__(self, groupid): + self._groupid = groupid + self._description = None + self._productids = [] + + def setDescription(self, description): + self._description = description + + def addProductID(self, productid): + self._productids.append(productid) + + def getTitle(self): + if self._description: + return "%s (%d products)" % (self._description, len(self._productids)) + return "#%s (%d products)" % (self._groupid, len(self._productids)) + + def validate(self): + if not self._groupid: + raise ValidationError('A Group must have a GroupID') + if not self._productids or len(self._productids) < 2: + raise ValidationError('A Group must contain at least two products') + diff -r 3cab052872f4 -r 809db989cac5 farolluz/renderer.py --- a/farolluz/renderer.py Fri Oct 24 16:43:31 2014 +0200 +++ b/farolluz/renderer.py Fri Oct 24 17:01:26 2014 +0200 @@ -25,8 +25,6 @@ from __future__ import print_function import os -import sys -from datetime import datetime import jinja2 from .parsers import cvrf diff -r 3cab052872f4 -r 809db989cac5 farolluz/vulnerability.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/farolluz/vulnerability.py Fri Oct 24 17:01:26 2014 +0200 @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +# +# Authors: +# Benoît Allard +# +# 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. + +"""\ +Vulnerability Objects related to CVRF Documents +""" + +from .common import ValidationError +from .document import CVRFPublisher + + +class CVRFVulnerabilityID(object): + def __init__(self, systemname, value): + self._systemname = systemname + self._value = value + + def validate(self): + if not self._systemname: + raise ValidationError('A Vulnerability ID must have a System Name') + if not self._value: + raise ValidationError('A Vulnerability ID must have a value') + + +class CVRFVulnerability(object): + def __init__(self, ordinal): + self._ordinal = ordinal + self._title = None + self._id = None + self._notes = [] + self._discoverydate = None + self._releasedate = None + self._involvements = [] + self._cve = None + self._cwes = [] + self._productstatuses = [] + self._threats = [] + self._cvsss = [] + self._remediations = [] + self._references = [] + self._acknowledgments = [] + + def setTitle(self, title): + self._title = title + + def setID(self, _id): + self._id = _id + + def addNote(self, note): + self._notes.append(note) + + def setDiscoveryDate(self, date): + self._discoverydate = date + + def setReleaseDate(self, date): + self._releasedate = date + + def addInvolvement(self, involvement): + self._involvements.append(involvement) + + def setCVE(self, cve): + self._cve = cve + + def addCWE(self, cwe): + self._cwes.append(cwe) + + def addProductStatus(self, productstatus): + self._productstatuses.append(productstatus) + + def addThreat(self, threat): + self._threats.append(threat) + + def addCVSSSet(self, cvss_set): + self._cvsss.append(cvss_set) + + def addRemediation(self, remediation): + self._remediations.append(remediation) + + def addReference(self, ref): + self._references.append(ref) + + def addAcknowledgment(self, ack): + self._acknowledgments.append(ack) + + def getTitle(self): + """ return something that can be used as a title """ + if self._title: + if self._id: + return "%s (%s)" % (self._title, self._id._value) + return self._title + if self._id: + return self._id._value + return "#%d" % self._ordinal + + def getNote(self, ordinal): + for note in self._notes: + if note._ordinal == ordinal: + return note + return None + + def mentionsProdId(self, productid): + """ Returns in which sub element, self is mentioning the productid """ + for category in (self._productstatuses, self._threats, self._cvsss, self._remediations): + for subelem in category: + if productid in subelem._productids: + yield subelem + + def isMentioningProdId(self, productid): + """ Returns if self is mentioning the productid """ + for e in self.mentionsProdId(productid): + # We only need to know if the generator yield at least one elem. + return True + return False + + def mentionsGroupId(self, groupid): + for category in (self._threats, self._remediations): + for subelem in category: + if groupid in subelem._groupids: + yield subelem + + def isMentioningGroupId(self, groupids): + """ Make sure you call this with a list (not a generator or a tuple) + when wished """ + if not isinstance(groupids, list): + groupids = [groupids] + for groupid in groupids: + for _ in self.mentionsGroupId(groupid): + # We only need to know if the generator yield at least one elem. + return True + return False + + def validate(self, productids, groupids): + if not self._ordinal: + raise ValidationError('A Vulnerability must have an ordinal') + if self._id is not None: + self._id.validate() + ordinals = set() + for note in self._notes: + note.validate() + if note._ordinal in ordinals: + raise ValidationError('Vulnerability Note Ordinal %d duplicated' % note._ordinal) + ordinals.add(note._ordinal) + for involvement in self._involvements: + involvement.validate() + for cwe in self._cwes: + cwe.validate() + for status in self._productstatuses: + status.validate(productids) + pids = set() + for status in self._productstatuses: + for pid in status._productids: + if pid in pids: + raise ValidationError('ProductID %s mentionned in two different ProductStatuses for Vulnerability %d' % (pid, self._ordinal)) + pids.add(pid) + for threat in self._threats: + threat.validate(productids, groupids) + for cvss in self._cvsss: + cvss.validate(productids) + pids = set() + for cvss in self._cvsss: + for pid in (cvss._productids or productids): + if pid in pids: + raise ValidationError('ProductID %s mentionned in two different CVSS Score Sets for Vulnerability %d' % (pid, self._ordinal)) + pids.add(pid) + for remediation in self._remediations: + remediation.validate(productids, groupids) + for reference in self._references: + reference.validate() + for acknowledgment in self._acknowledgments: + acknowledgment.validate() + + +class CVRFInvolvement(object): + PARTIES = CVRFPublisher.TYPES + STATUSES = ('Open', 'Disputed', 'In Progress', 'Completed', + 'Contact Attempted', 'Not Contacted') + def __init__(self, party, status): + self._party = party + self._status = status + self._description = None + + def setDescription(self, description): + self._description = description + + def getTitle(self): + return "From %s: %s" % (self._party, self._status) + + def validate(self): + if not self._party: + raise ValidationError('An Involvement must have a Party') + if self._party not in self.PARTIES: + raise ValidationError("An Involvement's Party must be one of %s" % ', '.join(self.PARTIES)) + if not self._status: + raise ValidationError('An Involvement must have a Status') + if self._status not in self.STATUSES: + raise ValidationError("An Involvement's Status must be one of %s" % ', '.join(self.STATUSES)) + + +class CVRFCWE(object): + def __init__(self, _id, value): + self._id = _id + self._value = value + + def validate(self): + if not self._id: + raise ValidationError('A CWE must have an ID') + if not self._value: + raise ValidationError('A CWE must have a description') + + +class CVRFProductStatus(object): + TYPES = ('First Affected', 'Known Affected', 'Known Not Affected', + 'First Fixed', 'Fixed', 'Recommended', 'Last Affected') + NAME = "Product Status" + def __init__(self, _type): + self._type = _type + self._productids = [] + + def addProductID(self, productid): + self._productids.append(productid) + + def getTitle(self): + return "%s: %d products" % (self._type, len(self._productids)) + + def validate(self, productids): + if not self._type: + raise ValidationError('A Product Status must have a Type') + if self._type not in self.TYPES: + raise ValidationError("A Product Status' Type must be one of %s" % ', '.join(self.TYPES)) + if len(self._productids) < 1: + raise ValidationError('A Product Status must mention at least one Product') + for productid in self._productids: + if productid not in productids: + raise ValidationError('Unknown ProductID: %s' % productid) + + +class CVRFThreat(object): + TYPES = ('Impact', 'Exploit Status', 'Target Set') + NAME = "Threat" + def __init__(self, _type, description): + self._type = _type + self._description = description + self._date = None + self._productids = [] + self._groupids = [] + + def setDate(self, date): + self._date = date + + def addProductID(self, productid): + self._productids.append(productid) + + def addGroupID(self, groupid): + self._groupids.append(groupid) + + def getTitle(self): + return self._type + + def validate(self, productids, groupids): + if not self._type: + raise ValidationError('A Threat must have a Type') + if self._type not in self.TYPES: + raise ValidationError("A Threat's Type must be one of %s" % ', '.join(self.TYPES)) + if not self._description: + raise ValidationError('A Threat must have a Description') + for productid in self._productids: + if productid not in productids: + raise ValidationError('Unknown ProductID: %s' % productid) + for groupid in self._groupids: + if groupid not in groupids: + raise ValidationError('Unknown GroupID: %s' % groupid) + + +class CVRFCVSSSet(object): + # To determine the base Score + VALUES = {'AV': {'L':0.395, 'A':0.646, 'N':1.0}, + 'AC': {'H':0.35, 'M':0.61 ,'L':0.71}, + 'Au': {'M':0.45, 'S':0.56, 'N':0.704}, + 'C': {'N':0.0, 'P':0.275, 'C':0.66}, + 'I': {'N':0.0, 'P':0.275, 'C':0.66}, + 'A': {'N':0.0, 'P':0.275, 'C':0.66}} + NAME = "CVSS Score Set" + def __init__(self, basescore): + self._basescore = basescore + self._temporalscore = None + self._environmentalscore = None + self._vector = None + self.vector = None + self._productids = [] + + def setTemporalScore(self, tempscore): + self._temporalscore = tempscore + + def setEnvironmentalScore(self, envscore): + self._environmentalscore = envscore + + def setVector(self, vector): + self._vector = vector + if vector is None: + self.vector = vector + return + try: + self.vector = {} + for component in vector[:26].split('/'): + name, value = component.split(':') + self.vector[name] = self.VALUES[name][value] + except (KeyError, ValueError): + self.vector = None + + def addProductID(self, productid): + self._productids.append(productid) + + def baseScore(self): + v = self.vector # make an alias for shorter lines + exploitability = 20 * v['AV'] * v['AC'] * v['Au'] + impact = 10.41 * (1 - (1 - v['C']) * (1 - v['I']) * (1 - v['A'])) + def f(i): return 0 if i == 0 else 1.176 + return ((0.6 * impact) + (0.4 * exploitability) - 1.5) * f(impact) + + def validate(self, productids): + if not self._basescore: + raise ValidationError('A CVSS Score Set must have a Base Score') + if self._vector and not self.vector: + raise ValidationError('Syntax Error in CVSS Vector') + if self.vector and (abs(self._basescore - self.baseScore()) >= 0.05): + raise ValidationError('Inconsistency in CVSS Score Set between Vector (%f) and Base Score (%f)' % (self.baseScore(), self._basescore)) + for productid in self._productids: + if productid not in productids: + raise ValidationError('Unknown ProductID: %s' % productid) + + +class CVRFRemediation(object): + TYPES = ('Workaround', 'Mitigation', 'Vendor Fix', 'None Available', + 'Will Not Fix') + NAME = "Remediation" + def __init__(self, _type, description): + self._type = _type + self._description = description + self._date = None + self._entitlement = None + self._url = None + self._productids = [] + self._groupids = [] + + def setDate(self, date): + self._date = date + + def setEntitlement(self, entitlement): + self._entitlement = entitlement + + def setURL(self, url): + self._url = url + + def addProductID(self, productid): + self._productids.append(productid) + + def addGroupID(self, groupid): + self._groupids.append(groupid) + + def getTitle(self): + return self._type + + def validate(self, productids, groupids): + if not self._type: + raise ValidationError('A Remediation must have a Type') + if self._type not in self.TYPES: + raise ValidationError("A Remediation's Type must be one of %s" % ', '.join(self.TYPES)) + if not self._description: + raise ValidationError('A Remediation must have a Description') + for productid in self._productids: + if productid not in productids: + raise ValidationError('Unknown ProductID: %s' % productid) + for groupid in self._groupids: + if groupid not in groupids: + raise ValidationError('Unknown GroupID: %s' % groupid) +