Source code for cpe.cpe

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
This file is part of cpe package.

This module contains the common characteristics of
any type of CPE Name, associated with a version of Common Platform
Enumeration (CPE) specification.

Copyright (C) 2013  Alejandro Galindo García, Roberto Abdelkader Martínez Pérez

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

For any problems using the cpe package, or general questions and
feedback about it, please contact:

- Alejandro Galindo García: galindo.garcia.alejandro@gmail.com
- Roberto Abdelkader Martínez Pérez: robertomartinezp@gmail.com
"""
from collections import OrderedDict

from comp.cpecomp import CPEComponent
from comp.cpecomp2_3_uri import CPEComponent2_3_URI
from comp.cpecomp2_3_wfn import CPEComponent2_3_WFN
from comp.cpecomp2_3_fs import CPEComponent2_3_FS
from comp.cpecomp2_3_uri_edpacked import CPEComponent2_3_URI_edpacked
from comp.cpecomp_logical import CPEComponentLogical
from comp.cpecomp_empty import CPEComponentEmpty
from comp.cpecomp_anyvalue import CPEComponentAnyValue
from comp.cpecomp_undefined import CPEComponentUndefined
from comp.cpecomp_notapplicable import CPEComponentNotApplicable


[docs]class CPE(dict): """ Represents a generic CPE Name compatible with all versions of CPE specification. Parts of CPE are stored in a dictionary. CPE structure (dictionary): └─ part {hw, os, sw, undefined} └─ element list (list) └─ component list (dictionary) """ ############### # CONSTANTS # ############### # Constants used to paint the representation of CPE Name _PREFIX_ELEM = " [" _PREFIX_ELEMENTS = " [" _SUFFIX_ELEM = " ]" _SUFFIX_ELEMENTS = " ]" # Type of systems included in CPE specification #: Type of system "application" KEY_APP = "app" #: Type of system "hardware" KEY_HW = "hw" #: Type of system "operating system" KEY_OS = "os" #: Undefined type of system KEY_UNDEFINED = "undef" #: List of keys associated with the types of system #: included in CPE specification CPE_PART_KEYS = (KEY_HW, KEY_OS, KEY_APP, KEY_UNDEFINED) # Constants of possible versions of CPE specification #: Version 1.1 of CPE specification VERSION_1_1 = "1.1" #: Version 2.2 of CPE specification VERSION_2_2 = "2.2" #: Version 2.3 of CPE specification VERSION_2_3 = "2.3" #: Version not set VERSION_UNDEFINED = "undefined" #: Version of CPE Name VERSION = VERSION_UNDEFINED ############### # VARIABLES # ############### #: Dictionary with the relation between the values of "part" #: component and the part name in internal structure of CPE Name _system_and_parts = OrderedDict(( (CPEComponent.VALUE_PART_HW, KEY_HW), (CPEComponent.VALUE_PART_OS, KEY_OS), (CPEComponent.VALUE_PART_APP, KEY_APP), (CPEComponent.VALUE_PART_UNDEFINED, KEY_UNDEFINED))) ################### # CLASS METHODS # ################### @classmethod def _trim(cls, s): """ Remove trailing colons from the URI back to the first non-colon. :param string s: input URI string :returns: URI string with trailing colons removed :rtype: string TEST: trailing colons necessary >>> s = '1:2::::' >>> CPE._trim(s) '1:2' TEST: trailing colons not necessary >>> s = '1:2:3:4:5:6' >>> CPE._trim(s) '1:2:3:4:5:6' """ reverse = s[::-1] idx = 0 for i in range(0, len(reverse)): if reverse[i] == ":": idx += 1 else: break # Return the substring after all trailing colons, # reversed back to its original character order. new_s = reverse[idx: len(reverse)] return new_s[::-1] #################### # OBJECT METHODS # ####################
[docs] def __eq__(self, other): """ Returns True if other (first element of operation) and self (second element of operation) are equal CPE Names, false otherwise. :param CPE other: CPE Name to compare :returns: True if other == self, False otherwise :rtype: boolean """ for part in CPE.CPE_PART_KEYS: elements_self = self.get(part) elements_other = other.get(part) len_self = len(elements_self) if len_self != len(elements_other): return False for i in range(0, len(elements_self)): elem_self = elements_self[i] elem_other = elements_other[i] for ck in CPEComponent.CPE_COMP_KEYS_EXTENDED: if (elem_self[ck] != elem_other[ck]): return False return True
[docs] def __getitem__(self, i): """ Returns the i'th component name of CPE Name. :param int i: component index to find :returns: component string found :rtype: CPEComponent :exception: IndexError - index not found in CPE Name TEST: good index >>> str = 'cpe:///sun_microsystem:sun@os:5.9:#update' >>> c = CPE(str) >>> c[0] CPEComponent1_1(sun_microsystem) """ count = 0 errmsg = "Component index of CPE Name out of range" for pk in CPE.CPE_PART_KEYS: elements = self.get(pk) for elem in elements: for ck in CPEComponent.CPE_COMP_KEYS_EXTENDED: comp = elem.get(ck) if (count == i): if not isinstance(comp, CPEComponentUndefined): return comp else: raise IndexError(errmsg) else: count += 1 raise IndexError(errmsg)
[docs] def __init__(self, cpe_str, *args, **kwargs): """ Store the CPE Name. :param string cpe_str: CPE Name :returns: None """ # The original CPE Name as string self.cpe_str = cpe_str # Store CPE Name in lower-case letters: # CPE Names are case-insensitive. # To reduce potential for confusion, # all CPE Names should be written in lowercase. self._str = cpe_str.lower() # Check if CPE Name is correct self._parse()
[docs] def __len__(self): """ Returns the number of components of CPE Name. :returns: count of components of CPE Name :rtype: int TEST: a CPE Name with two parts (hw and os) and some elements empty and with values >>> str = "cpe:/cisco::3825/cisco:ios:12.3" >>> c = CPE(str) >>> len(c) 6 """ count = 0 for part in CPE.CPE_PART_KEYS: elements = self.get(part) for elem in elements: for ck in CPEComponent.CPE_COMP_KEYS_EXTENDED: comp = elem.get(ck) if not isinstance(comp, CPEComponentUndefined): count += 1 return count
[docs] def __new__(cls, cpe_str, version=None, *args, **kwargs): """ Generator of CPE Names. :param string cpe_str: CPE Name string :param string version: version of CPE specification of CPE Name :returns: CPE object with version of CPE detected correctly :rtype: CPE :exception: NotImplementedError - incorrect CPE Name or version of CPE not implemented This class implements the factory pattern, that is, this class centralizes the creation of objects of a particular CPE version, hiding the user the requested object instance. """ from cpe1_1 import CPE1_1 from cpe2_2 import CPE2_2 from cpe2_3 import CPE2_3 # List of implemented versions of CPE Names # # Note: Order matters here, because some regexp can parse # multiple versions at once. _CPE_VERSIONS = OrderedDict(( (CPE.VERSION_2_3, CPE2_3), (CPE.VERSION_2_2, CPE2_2), (CPE.VERSION_1_1, CPE1_1),)) errmsg = 'Version of CPE not implemented' if version is None: # Detect CPE version of input CPE Name for v in _CPE_VERSIONS: try: # Validate CPE Name c = _CPE_VERSIONS[v](cpe_str) except ValueError: # Test another version continue except NotImplementedError: # Test another version continue else: # Version detected return c raise NotImplementedError(errmsg) elif version in _CPE_VERSIONS: # Correct input version, validate CPE Name return _CPE_VERSIONS[version](cpe_str) else: # Invalid CPE version raise NotImplementedError(errmsg)
[docs] def __repr__(self): """ Returns a unambiguous representation of CPE Name. :returns: Representation of CPE Name as string :rtype: string """ txtParts = [] for pk in CPE.CPE_PART_KEYS: txtParts.append(pk) txtElements = [] txtElements.append(CPE._PREFIX_ELEMENTS) elements = self.get(pk) for elem in elements: txtElem = [] txtElem.append(CPE._PREFIX_ELEM) for i in range(0, len(CPEComponent.CPE_COMP_KEYS_EXTENDED)): txtComp = [] ck = CPEComponent.ordered_comp_parts.get(i) comp = elem.get(ck) if isinstance(comp, CPEComponentLogical): value = comp.__str__() else: value = comp.get_value() txtComp.append(" ") txtComp.append(ck) txtComp.append(" = ") txtComp.append(value) txtElem.append("".join(txtComp)) if len(txtElem) == 1: # There are no components txtElem = [] txtElem.append(" []") else: txtElem.append(CPE._SUFFIX_ELEM) txtElements.append("\n".join(txtElem)) if len(txtElements) == 1: # There are no elements txtElements = [] txtElements.append(" []") else: txtElements.append(CPE._SUFFIX_ELEMENTS) txtParts.append("\n".join(txtElements)) return "\n".join(txtParts)
[docs] def __str__(self): """ Returns a human-readable representation of CPE Name. :returns: Representation of CPE Name as string :rtype: string """ return "CPE v{0}: {1}".format(self.VERSION, self.cpe_str)
def _create_cpe_parts(self, system, components): """ Create the structure to store the input type of system associated with components of CPE Name (hardware, operating system and software). :param string system: type of system associated with CPE Name :param dict components: CPE Name components to store :returns: None :exception: KeyError - incorrect system """ if system not in CPEComponent.SYSTEM_VALUES: errmsg = "Key '{0}' is not exist".format(system) raise ValueError(errmsg) elements = [] elements.append(components) pk = CPE._system_and_parts[system] self[pk] = elements def _get_attribute_components(self, att): """ Returns the component list of input attribute. :param string att: Attribute name to get :returns: List of Component objects of the attribute in CPE Name :rtype: list :exception: ValueError - invalid attribute name """ lc = [] if not CPEComponent.is_valid_attribute(att): errmsg = "Invalid attribute name '{0}' is not exist".format(att) raise ValueError(errmsg) for pk in CPE.CPE_PART_KEYS: elements = self.get(pk) for elem in elements: lc.append(elem.get(att)) return lc def _pack_edition(self): """ Pack the values of the five arguments into the simple edition component. If all the values are blank, just return a blank. :returns: "edition", "sw_edition", "target_sw", "target_hw" and "other" attributes packed in a only value :rtype: string :exception: TypeError - incompatible version with pack operation """ COMP_KEYS = (CPEComponent.ATT_EDITION, CPEComponent.ATT_SW_EDITION, CPEComponent.ATT_TARGET_SW, CPEComponent.ATT_TARGET_HW, CPEComponent.ATT_OTHER) separator = CPEComponent2_3_URI_edpacked.SEPARATOR_COMP packed_ed = [] packed_ed.append(separator) for ck in COMP_KEYS: lc = self._get_attribute_components(ck) if len(lc) > 1: # Incompatible version 1.1, there are two or more elements # in CPE Name errmsg = "Incompatible version {0} with URI".format( self.VERSION) raise TypeError(errmsg) comp = lc[0] if (isinstance(comp, CPEComponentUndefined) or isinstance(comp, CPEComponentEmpty) or isinstance(comp, CPEComponentAnyValue)): value = "" elif (isinstance(comp, CPEComponentNotApplicable)): value = CPEComponent2_3_URI.VALUE_NA else: # Component has some value; transform this original value # in URI value value = comp.as_uri_2_3() # Save the value of edition attribute if ck == CPEComponent.ATT_EDITION: ed = value # Packed the value of component packed_ed.append(value) packed_ed.append(separator) # Del the last separator packed_ed_str = "".join(packed_ed[:-1]) only_ed = [] only_ed.append(separator) only_ed.append(ed) only_ed.append(separator) only_ed.append(separator) only_ed.append(separator) only_ed.append(separator) only_ed_str = "".join(only_ed) if (packed_ed_str == only_ed_str): # All the extended attributes are blank, # so don't do any packing, just return ed return ed else: # Otherwise, pack the five values into a simple string # prefixed and internally delimited with the tilde return packed_ed_str
[docs] def as_dict(self): """ Returns the CPE Name dict as string. :returns: CPE Name dict as string :rtype: string """ return super(CPE, self).__str__()
[docs] def as_uri_2_3(self): """ Returns the CPE Name as URI string of version 2.3. :returns: CPE Name as URI string of version 2.3 :rtype: string :exception: TypeError - incompatible version """ uri = [] uri.append("cpe:/") ordered_comp_parts = { 0: CPEComponent.ATT_PART, 1: CPEComponent.ATT_VENDOR, 2: CPEComponent.ATT_PRODUCT, 3: CPEComponent.ATT_VERSION, 4: CPEComponent.ATT_UPDATE, 5: CPEComponent.ATT_EDITION, 6: CPEComponent.ATT_LANGUAGE} # Indicates if the previous component must be set depending on the # value of current component set_prev_comp = False prev_comp_list = [] for i in range(0, len(ordered_comp_parts)): ck = ordered_comp_parts[i] lc = self._get_attribute_components(ck) if len(lc) > 1: # Incompatible version 1.1, there are two or more elements # in CPE Name errmsg = "Incompatible version {0} with URI".format( self.VERSION) raise TypeError(errmsg) if ck == CPEComponent.ATT_EDITION: # Call the pack() helper function to compute the proper # binding for the edition element v = self._pack_edition() else: comp = lc[0] if (isinstance(comp, CPEComponentEmpty) or isinstance(comp, CPEComponentAnyValue)): # Logical value any v = CPEComponent2_3_URI.VALUE_ANY elif isinstance(comp, CPEComponentNotApplicable): # Logical value not applicable v = CPEComponent2_3_URI.VALUE_NA elif isinstance(comp, CPEComponentUndefined): set_prev_comp = True prev_comp_list.append(CPEComponent2_3_URI.VALUE_ANY) continue else: # Get the value of component encoded in URI v = comp.as_uri_2_3() # Append v to the URI and add a separator uri.append(v) uri.append(CPEComponent2_3_URI.SEPARATOR_COMP) if set_prev_comp: # Set the previous attribute as logical value any v = CPEComponent2_3_URI.VALUE_ANY pos_ini = len(uri) - len(prev_comp_list) - 1 increment = 2 # Count of inserted values for p, val in enumerate(prev_comp_list): pos = pos_ini + (p * increment) uri.insert(pos, v) uri.insert(pos + 1, CPEComponent2_3_URI.SEPARATOR_COMP) set_prev_comp = False prev_comp_list = [] # Return the URI string, with trailing separator trimmed return CPE._trim("".join(uri[:-1]))
[docs] def as_wfn(self): """ Returns the CPE Name as Well-Formed Name string of version 2.3. :return: CPE Name as WFN string :rtype: string :exception: TypeError - incompatible version """ from cpe2_3_wfn import CPE2_3_WFN wfn = [] wfn.append(CPE2_3_WFN.CPE_PREFIX) for i in range(0, len(CPEComponent.ordered_comp_parts)): ck = CPEComponent.ordered_comp_parts[i] lc = self._get_attribute_components(ck) if len(lc) > 1: # Incompatible version 1.1, there are two or more elements # in CPE Name errmsg = "Incompatible version {0} with WFN".format( self.VERSION) raise TypeError(errmsg) else: comp = lc[0] v = [] v.append(ck) v.append("=") if isinstance(comp, CPEComponentAnyValue): # Logical value any v.append(CPEComponent2_3_WFN.VALUE_ANY) elif isinstance(comp, CPEComponentNotApplicable): # Logical value not applicable v.append(CPEComponent2_3_WFN.VALUE_NA) elif (isinstance(comp, CPEComponentUndefined) or isinstance(comp, CPEComponentEmpty)): # Do not set the attribute continue else: # Get the simple value of WFN of component v.append('"') v.append(comp.as_wfn()) v.append('"') # Append v to the WFN and add a separator wfn.append("".join(v)) wfn.append(CPEComponent2_3_WFN.SEPARATOR_COMP) # Del the last separator wfn = wfn[:-1] # Return the WFN string wfn.append(CPE2_3_WFN.CPE_SUFFIX) return "".join(wfn)
[docs] def as_fs(self): """ Returns the CPE Name as formatted string of version 2.3. :returns: CPE Name as formatted string :rtype: string :exception: TypeError - incompatible version """ fs = [] fs.append("cpe:2.3:") for i in range(0, len(CPEComponent.ordered_comp_parts)): ck = CPEComponent.ordered_comp_parts[i] lc = self._get_attribute_components(ck) if len(lc) > 1: # Incompatible version 1.1, there are two or more elements # in CPE Name errmsg = "Incompatible version {0} with formatted string".format( self.VERSION) raise TypeError(errmsg) else: comp = lc[0] if (isinstance(comp, CPEComponentUndefined) or isinstance(comp, CPEComponentEmpty) or isinstance(comp, CPEComponentAnyValue)): # Logical value any v = CPEComponent2_3_FS.VALUE_ANY elif isinstance(comp, CPEComponentNotApplicable): # Logical value not applicable v = CPEComponent2_3_FS.VALUE_NA else: # Get the value of component encoded in formatted string v = comp.as_fs() # Append v to the formatted string then add a separator. fs.append(v) fs.append(CPEComponent2_3_FS.SEPARATOR_COMP) # Return the formatted string return CPE._trim("".join(fs[:-1]))
[docs] def get_edition(self): """ Returns the edition of product of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of edition attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_EDITION)
[docs] def get_language(self): """ Returns the internationalization information of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of language attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_LANGUAGE)
[docs] def get_other(self): """ Returns the other information part of CPE Name. :returns: Value of other attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_OTHER)
[docs] def get_part(self): """ Returns the part component of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of part attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_PART)
[docs] def get_product(self): """ Returns the product name of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of product attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_PRODUCT)
[docs] def get_software_edition(self): """ Returns the software edition of CPE Name. :returns: Value of sw_edition attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_SW_EDITION)
[docs] def get_target_hardware(self): """ Returns the arquitecture of CPE Name. :returns: Value of target_hw attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_TARGET_HW)
[docs] def get_target_software(self): """ Returns the software computing environment of CPE Name within which the product operates. :returns: Value of target_sw attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_TARGET_SW)
[docs] def get_update(self): """ Returns the update or service pack information of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of update attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_UPDATE)
[docs] def get_vendor(self): """ Returns the vendor name of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of vendor attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_VENDOR)
[docs] def get_version(self): """ Returns the version of product of CPE Name as a list. According to the CPE version, this list can contains one or more items. :returns: Value of version attribute as string list. :rtype: list """ return self.get_attribute_values(CPEComponent.ATT_VERSION)
[docs] def is_application(self): """ Returns True if CPE Name corresponds to application elem. :returns: True if CPE Name corresponds to application elem, False otherwise. :rtype: boolean """ elements = self.get(CPE.KEY_APP) return len(elements) > 0
[docs] def is_hardware(self): """ Returns True if CPE Name corresponds to hardware elem. :returns: True if CPE Name corresponds to hardware elem, False otherwise. :rtype: boolean """ elements = self.get(CPE.KEY_HW) return len(elements) > 0
[docs] def is_operating_system(self): """ Returns True if CPE Name corresponds to operating system elem. :returns: True if CPE Name corresponds to operating system elem, False otherwise. :rtype: boolean """ elements = self.get(CPE.KEY_OS) return len(elements) > 0
if __name__ == "__main__": import doctest doctest.testmod() doctest.testfile('tests/testfile_cpe.txt')