changeset 18:8a89b7a591e6

merged
author Benoît Allard <benoit.allard@greenbone.net>
date Tue, 14 Oct 2014 16:49:36 +0200
parents 8e23ba7d4167 (current diff) 90852c11fabd (diff)
children 4b53e7bcff0d
files
diffstat 4 files changed, 254 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES	Wed Sep 24 17:47:14 2014 +0200
+++ b/CHANGES	Tue Oct 14 16:49:36 2014 +0200
@@ -1,3 +1,22 @@
+FarolLuz 0.1.1 (????-??-??)
+===========================
+
+This is the first patch release of FarolLuz 0.1
+
+This release add support for reading / writing incomplete CVRF documents.
+
+Main changes since FarolLuz 0.1:
+--------------------------------
+* Allow writing of incomplete CVRF documents.
+* Allow parsing of incomplete CVRF documents.
+* Add a method to extract a document ID.
+* Add methods to extract Product references in a Document.
+* Add method to get a Vulnerability Note per ordinal.
+* Fix issue where Acknowledgment could only have one Name and Organization.
+* Complete the CVRF template with missing elements
+* Improve validation.
+
+
 FarolLuz 0.1 (2014-09-23)
 =========================
 
--- a/farolluz/cvrf.py	Wed Sep 24 17:47:14 2014 +0200
+++ b/farolluz/cvrf.py	Tue Oct 14 16:49:36 2014 +0200
@@ -65,6 +65,9 @@
     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')
@@ -92,6 +95,9 @@
     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')
@@ -225,18 +231,19 @@
 
 
 class CVRFAcknowledgment(object):
-    def __init__(self, name=None, organization=None, description=None,
+    def __init__(self, names=[], organizations=[], description=None,
                  url=None):
-        self._name = name
-        self._organization = organization
+        self._names = names
+        self._organizations = organizations
         self._description = description
         self._url = url
 
     def getTitle(self):
-        return "%s - %s" % (self._name, self._organization)
+        return "%s - %s" % (', '.join(self._names),
+                            ', '.join(self._organizations))
 
     def validate(self):
-        if (not self._name) and (not self._organization) and (not self._description):
+        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')
 
 
@@ -249,18 +256,9 @@
         self._products = []
         self._groups = []
 
-    def addBranch(self, branch):
-        parent = self.getBranch(branch.getParent().getPath())
-        if parent is self:
-            self._branches.append(branch)
-        else:
-            parent._childs.append(branch)
-
     def addProduct(self, product):
-        if product not in self._products:
-            self._products.append(product)
-        if product._parent is not self:
-            product._parent._product = product
+        """ Add to the product list """
+        self._products.append(product)
 
     def addRelationship(self, rel):
         self._relationships.append(rel)
@@ -396,9 +394,9 @@
     def __init__(self, _type, name, parentbranch):
         self._type = _type
         self._name = name
-        self._parentbranch = parentbranch
         self._childs = []
         self._product = None
+        self.link(parentbranch)
 
     def getParent(self):
         return self._parentbranch
@@ -455,6 +453,15 @@
             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')
@@ -475,10 +482,10 @@
     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._parent = parent
-        self._cpe = cpe
+        self.link(parent)
 
     def isRoot(self):
         return isinstance(self._parent, CVRFProductTree)
@@ -502,13 +509,18 @@
         return None
 
     def unlink(self):
-        """ Unset our _parent, and remove us from the _parent._childs """
-        if self.isRoot():
-            self._parent._products.remove(self)
-        else:
+        """ 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')
@@ -655,6 +667,46 @@
             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:
+            print "testing GroupId: ", groupid
+            for _ in self.mentionsGroupId(groupid):
+                # We only need to know if the generator yield at least one elem.
+                print 'True'
+                return True
+        print 'False'
+        return False
+
     def validate(self, productids, groupids):
         if not self._ordinal:
             raise ValidationError('A Vulnerability must have an ordinal')
@@ -672,10 +724,22 @@
             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:
@@ -684,7 +748,6 @@
             acknowledgment.validate()
 
 
-
 class CVRFInvolvement(object):
     PARTIES = CVRFPublisher.TYPES
     STATUSES = ('Open', 'Disputed', 'In Progress', 'Completed',
@@ -726,6 +789,7 @@
 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 = []
@@ -750,6 +814,7 @@
 
 class CVRFThreat(object):
     TYPES = ('Impact', 'Exploit Status', 'Target Set')
+    NAME = "Threat"
     def __init__(self, _type, description):
         self._type = _type
         self._description = description
@@ -792,6 +857,7 @@
               '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
@@ -844,6 +910,7 @@
 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
@@ -972,12 +1039,39 @@
                         products.add(productid)
         return set(self.getProductForID(p) for p in products)
 
+    def isProductOrphan(self, productid):
+        """ Returns if a productid is mentionned nowhere in the document """
+        # We first look at the ProductTree
+        ptree = self._producttree
+        for relation in ptree._relationships:
+            if productid == relation._productreference:
+                return False
+            if productid == relation._relatestoproductreference:
+                return False
+        groupids = [g._groupid for g in ptree._groups if productid in g._productids]
+        if len(groupids) > 0:
+            return False
+        # Go through all the Vulnerabilities
+        for vulnerability in self._vulnerabilities:
+            if vulnerability.isMentioningProdId(productid):
+                return False
+            for groupid in groupids:
+                if vulnerability.isMentioningGroupId(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')
--- a/farolluz/parsers/cvrf.py	Wed Sep 24 17:47:14 2014 +0200
+++ b/farolluz/parsers/cvrf.py	Tue Oct 14 16:49:36 2014 +0200
@@ -94,9 +94,14 @@
 
 
 def parseAcknowledgment(elem, ns='cvrf'):
+    names = []
+    for cvrfname in elem.findall(UN(ns, 'Name')):
+        names.append(cvrfname.text.strip())
+    orgs = []
+    for cvrforg in elem.findall(UN(ns, 'Organization')):
+        orgs.append(cvrforg.text.strip())
     return CVRFAcknowledgment(
-        elem.findtext(UN(ns, 'Name')),
-        elem.findtext(UN(ns, 'Organization')),
+        names, orgs,
         elem.findtext(UN(ns, 'Description')),
         elem.findtext(UN(ns, 'URL')),
     )
@@ -247,47 +252,51 @@
         cvrfdoc.findtext(UN('cvrf', 'DocumentTitle')).strip(),
         cvrfdoc.findtext(UN('cvrf', 'DocumentType')).strip()
     )
+
     cvrfpub = cvrfdoc.find(UN('cvrf', 'DocumentPublisher'))
-    pub = CVRFPublisher(cvrfpub.attrib['Type'], cvrfpub.attrib.get('VendorID'))
-    doc.setPublisher(pub)
-    contact = cvrfpub.find(UN('cvrf', 'ContactDetails'))
-    if contact is not None:
-        pub.setContact(contact.text.strip())
-    authority = cvrfpub.find(UN('cvrf', 'IssuingAuthority'))
-    if authority is not None:
-        pub.setAuthority(authority.text.strip())
+    if cvrfpub is not None:
+        pub = CVRFPublisher(cvrfpub.attrib['Type'], cvrfpub.attrib.get('VendorID'))
+        doc.setPublisher(pub)
+        contact = cvrfpub.find(UN('cvrf', 'ContactDetails'))
+        if contact is not None:
+            pub.setContact(contact.text.strip())
+        authority = cvrfpub.find(UN('cvrf', 'IssuingAuthority'))
+        if authority is not None:
+            pub.setAuthority(authority.text.strip())
+
     cvrftracking = cvrfdoc.find(UN('cvrf', 'DocumentTracking'))
-    identification = CVRFTrackingID(
-        cvrftracking.findtext('/'.join([UN('cvrf', 'Identification'), UN('cvrf', 'ID')])).strip()
-    )
-    for cvrfalias in cvrftracking.findall('/'.join([UN('cvrf', 'Identification'), UN('cvrf', 'Alias')])):
-        identification.addAlias(cvrfalias.text.strip())
-    tracking = CVRFTracking(
-        identification,
-        cvrftracking.findtext(UN('cvrf', 'Status')).strip(),
-        parseVersion(cvrftracking.findtext(UN('cvrf', 'Version')).strip()),
-        parseDate(cvrftracking.findtext(UN('cvrf', 'InitialReleaseDate')).strip()),
-        parseDate(cvrftracking.findtext(UN('cvrf', 'CurrentReleaseDate')).strip())
-    )
-    doc.setTracking(tracking)
-    for cvrfrev in cvrftracking.findall('/'.join([UN('cvrf', 'RevisionHistory'), UN('cvrf', 'Revision')])):
-        rev = CVRFRevision(
-            parseVersion(cvrfrev.findtext(UN('cvrf', 'Number')).strip()),
-            parseDate(cvrfrev.findtext(UN('cvrf', 'Date')).strip()),
-            cvrfrev.findtext(UN('cvrf', 'Description')).strip(),
+    if cvrftracking is not None:
+        identification = CVRFTrackingID(
+            cvrftracking.findtext('/'.join([UN('cvrf', 'Identification'), UN('cvrf', 'ID')])).strip()
         )
-        tracking.addRevision(rev)
+        for cvrfalias in cvrftracking.findall('/'.join([UN('cvrf', 'Identification'), UN('cvrf', 'Alias')])):
+            identification.addAlias(cvrfalias.text.strip())
+        tracking = CVRFTracking(
+            identification,
+            cvrftracking.findtext(UN('cvrf', 'Status')).strip(),
+            parseVersion(cvrftracking.findtext(UN('cvrf', 'Version')).strip()),
+            parseDate(cvrftracking.findtext(UN('cvrf', 'InitialReleaseDate')).strip()),
+            parseDate(cvrftracking.findtext(UN('cvrf', 'CurrentReleaseDate')).strip())
+        )
+        doc.setTracking(tracking)
+        for cvrfrev in cvrftracking.findall('/'.join([UN('cvrf', 'RevisionHistory'), UN('cvrf', 'Revision')])):
+            rev = CVRFRevision(
+                parseVersion(cvrfrev.findtext(UN('cvrf', 'Number')).strip()),
+                parseDate(cvrfrev.findtext(UN('cvrf', 'Date')).strip()),
+                cvrfrev.findtext(UN('cvrf', 'Description')).strip(),
+            )
+            tracking.addRevision(rev)
 
-    xmlgenerator = cvrftracking.find(UN('cvrf', 'Generator'))
-    if xmlgenerator is not None:
-        generator = CVRFGenerator()
-        xmlengine = xmlgenerator.findtext(UN('cvrf', 'Engine'))
-        if xmlengine is not None:
-            generator.setEngine(xmlengine.strip())
-        xmldate = xmlgenerator.findtext(UN('cvrf', 'Date'))
-        if xmldate is not None:
-            generator.setDate(parseDate(xmldate.strip()))
-        tracking.setGenerator(generator)
+        xmlgenerator = cvrftracking.find(UN('cvrf', 'Generator'))
+        if xmlgenerator is not None:
+            generator = CVRFGenerator()
+            xmlengine = xmlgenerator.findtext(UN('cvrf', 'Engine'))
+            if xmlengine is not None:
+                generator.setEngine(xmlengine.strip())
+            xmldate = xmlgenerator.findtext(UN('cvrf', 'Date'))
+            if xmldate is not None:
+                generator.setDate(parseDate(xmldate.strip()))
+            tracking.setGenerator(generator)
 
     for cvrfnote in cvrfdoc.findall('/'.join([UN('cvrf', 'DocumentNotes'), UN('cvrf', 'Note')])):
         doc.addNote(parseNote(cvrfnote))
@@ -315,8 +324,8 @@
     cvrfptree = cvrfdoc.find(UN('prod', 'ProductTree'))
     if cvrfptree is not None:
         producttree = doc.createProductTree()
-        for branch in parseProdBranch(cvrfptree, producttree):
-            producttree.addBranch(branch)
+        # We need to exhaust our generator ...
+        for _ in parseProdBranch(cvrfptree, producttree): pass
 
         for product in cvrfptree.findall(UN('prod', 'FullProductName')):
             producttree.addProduct(parseFullProductName(product, producttree))
--- a/farolluz/templates/cvrf.j2	Wed Sep 24 17:47:14 2014 +0200
+++ b/farolluz/templates/cvrf.j2	Tue Oct 14 16:49:36 2014 +0200
@@ -25,7 +25,7 @@
 
 <?xml version="1.0" encoding="utf-8"?>
 
-{#- Some macros for producttree generation #}
+{#- A macro for producttree generation #}
 {%- macro FullProductNames(producttree, parent) %}
   {%- for product in producttree._products %}
     {%- if product._parent is sameas parent %}
@@ -36,15 +36,41 @@
   {%- endfor %}
 {%- endmacro %}
 
+{#- Some macros about more generic types #}
 {%- macro Note(note) -%}
   <Note{{ {'Type': note._type, 'Ordinal': note._ordinal, 'Title': note._title, 'Audience': note._audience} | xmlattr }}>
     {{- note._note | escape -}}
   </Note>
-{%- endmacro %}
+{%- endmacro -%}
+
+{%- macro Reference(reference) -%}
+  <Reference{{ {'Type': reference._type} | xmlattr }}>
+    <URL>{{ reference._url }}</URL>
+    <Description>{{ reference._description }}</Description>
+  </Reference>
+{%- endmacro -%}
+
+{%- macro Acknowledgment(acknowledgment) -%}
+  <Acknowledgment>
+    {%- for name in acknowledgment._names %}
+    <Name>{{ name }}</Name>
+    {%- endfor %}
+    {%- for organization in acknowledgment._organizations %}
+    <Organization>{{ organization }}</Organization>
+    {%- endfor %}
+    {%- if acknowledgment._description %}
+    <Description>{{ acknowledgment._description }}</Description>
+    {%- endif %}
+    {%- if acknowledgment._url %}
+    <URL>{{ acknowledgment._url }}</URL>
+    {%- endif %}
+  </Acknowledgment>
+{%- endmacro -%}
+
 <cvrfdoc xmlns="http://www.icasi.org/CVRF/schema/cvrf/1.1">
   <DocumentTitle>{{ cvrf._title }}</DocumentTitle>
   <DocumentType>{{ cvrf._type }}</DocumentType>
-  {%- with publisher = cvrf._publisher %}
+  {%- with publisher = cvrf._publisher %}{% if publisher %}
   <DocumentPublisher{{ {'Type': publisher._type, 'VendorID': publisher._vendorid} | xmlattr }}>
     {%- if publisher._contact %}
     <ContactDetails>{{ publisher._contact }}</ContactDetails>
@@ -53,8 +79,8 @@
     <IssuingAuthority>{{ publisher._authority }}</IssuingAuthority>
     {%- endif %}
   </DocumentPublisher>
-  {%- endwith %}
-  {%- with tracking = cvrf._tracking %}
+  {%- endif %}{% endwith %}
+  {%- with tracking = cvrf._tracking %}{% if tracking %}
   <DocumentTracking>
     <Identification>
       <ID>{{ tracking._identification._id }}</ID>
@@ -88,7 +114,7 @@
     </Generator>
     {%- endif %}
   </DocumentTracking>
-  {%- endwith %}
+  {%- endif %}{% endwith %}
   {%- if cvrf._notes %}
   <DocumentNotes>
     {%- for note in cvrf._notes %}
@@ -107,30 +133,14 @@
   {%- if cvrf._references %}
   <DocumentReferences>
     {%- for reference in cvrf._references %}
-    <Reference{{ {'Type': reference._type} | xmlattr }}>
-      <URL>{{ reference._url }}</URL>
-      <Description>{{ reference._description }}</Description>
-    </Reference>
+    {{ Reference(reference) }}
     {%- endfor %}
   </DocumentReferences>
   {%- endif %}
   {%- if cvrf._acknowledgments %}
   <Acknowledgments>
     {%- for acknowledgment in cvrf._acknowledgments %}
-    <Acknowledgment>
-      {%- if acknowledgment._name %}
-      <Name>{{ acknowledgment._name }}</Name>
-      {%- endif %}
-      {%- if acknowledgment._organization %}
-      <Organization>{{ acknowledgment._organization }}</Organization>
-      {%- endif %}
-      {%- if acknowledgment._description %}
-      <Description>{{ acknowledgment._description }}</Description>
-      {%- endif %}
-      {%- if acknowledgment._url %}
-      <URL>{{ acknowledgment._url }}</URL>
-      {%- endif %}
-    </Acknowledgment>
+    {{ Acknowledgment(acknowledgment) }}
     {%- endfor %}
   </Acknowledgments>
   {%- endif %}
@@ -143,8 +153,8 @@
       {{- FullProductNames(producttree, branch) }}
     </Branch>
     {%- endfor %}
-    {{ FullProductNames(producttree, producttree) }}
-    {%- for relationship in producttree._relationships -%}
+    {{- FullProductNames(producttree, producttree) }}
+    {%- for relationship in producttree._relationships %}
     <Relationship{{ {'ProductReference': relationship._productreference, 'RelationType': relationship._relationtype, 'RelatesToProductReference': relationship._relatestoproductreference} | xmlattr }}>
       {{- FullProductNames(producttree, relationship) }}
     </Relationship>
@@ -152,7 +162,7 @@
     {%- if producttree._groups %}
     <ProductGroups>
       {%- for group in producttree._groups %}
-      <Group{{ {'GroupID': group._id} | xmlattr }}>
+      <Group{{ {'GroupID': group._groupid} | xmlattr }}>
         {%- if group._description %}
         <Description>{{ group._description }}</Description>
         {%- endif %}
@@ -215,6 +225,21 @@
       {%- endfor %}
     </ProductStatuses>
     {%- endif %}
+    {%- if vulnerability._threats %}
+    <Threats>
+      {%- for threat in vulnerability._threats %}
+      <Threat Type="{{ threat._type }}"{{ ' Date="%s"' % threat._date.isoformat() if threat._date }}>
+        <Description>{{ threat._description }}</Description>
+        {%- for productid in threat._productids %}
+        <ProductID>{{ productid }}</ProductID>
+        {%- endfor %}
+        {%- for groupid in threat._groupids %}
+        <GroupID>{{ groupid }}</GroupID>
+        {%- endfor %}
+      </Threat>
+      {%- endfor %}
+    </Threats>
+    {%- endif %}
     {%- if vulnerability._cvsss %}
     <CVSSScoreSets>
       {%- for cvss in vulnerability._cvsss %}
@@ -230,7 +255,7 @@
         <Vector>{{ cvss._vector }}</Vector>
         {%- endif %}
         {%- for productid in cvss._productids %}
-        <ProductID>{{productid}}</ProductID>
+        <ProductID>{{ productid }}</ProductID>
         {%- endfor %}
       </ScoreSet>
       {%- endfor %}
@@ -257,6 +282,20 @@
       {%- endfor %}
     </Remediations>
     {%- endif %}
+    {%- if vulnerability._references %}
+    <References>
+      {%- for reference in vulnerability._references %}
+      {{ Reference(reference) }}
+      {%- endfor %}
+    </References>
+    {%- endif %}
+    {%- if vulnerability._acknowledgments %}
+    <Acknowledgments>
+      {%- for acknowledgment in vulnerability._acknowledgments %}
+      {{ Acknowledgment(acknowledgment) }}
+      {%- endfor %}
+    </Acknowledgments>
+    {%- endif %}
   </Vulnerability>
   {%- endfor %}
 </cvrfdoc>
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)