benoit@41: # -*- coding: utf-8 -*- benoit@41: # Description: benoit@41: # Methods for parsing CPEs benoit@41: # benoit@41: # Authors: benoit@41: # BenoƮt Allard benoit@41: # benoit@41: # Copyright: benoit@41: # Copyright (C) 2014 Greenbone Networks GmbH benoit@41: # benoit@41: # This program is free software; you can redistribute it and/or benoit@41: # modify it under the terms of the GNU General Public License benoit@41: # as published by the Free Software Foundation; either version 2 benoit@41: # of the License, or (at your option) any later version. benoit@41: # benoit@41: # This program is distributed in the hope that it will be useful, benoit@41: # but WITHOUT ANY WARRANTY; without even the implied warranty of benoit@41: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the benoit@41: # GNU General Public License for more details. benoit@41: # benoit@41: # You should have received a copy of the GNU General Public License benoit@41: # along with this program; if not, write to the Free Software benoit@41: # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. benoit@41: benoit@40: """\ benoit@40: a cpe class to ease the creation of a producttree based on cpe benoit@40: benoit@40: This is based on: benoit@40: benoit@40: NIST Interagency Report 7695 benoit@40: Common Platform Enumeration: Naming Specification Version 2.3 benoit@40: benoit@40: CPE is a trademark of The MITRE Corporation. benoit@40: benoit@40: """ benoit@40: benoit@40: import re benoit@40: benoit@40: PCT_MAP ={'!': "%21", '"': "%22", '#': "%23", '$': "%24", '%': "%25", '&': "%26", benoit@40: "'": "%27", '(': "%28", ')': "%29", '*': "%2a", '+': "%2b", ',': "%2c", benoit@40: '/': "%2f", ':': "%3a", ';': "%3b", '<': "%3c", "=": "%3d", '>': "%3e", benoit@40: '?': "%3f", '@': "%40", '[': "%5b", '\\': "%5c","]": "%5d", '^': "%5e", benoit@40: '`': "%60", '{': "%7b", '|': "%7c", '}': "%7d", "~": "%7e"} benoit@40: benoit@40: PCT_MAP_i = dict((v, k) for k, v in PCT_MAP.iteritems()) benoit@40: benoit@40: def pct_encode(c): benoit@40: """ Returns the right percent-encoding of c """ benoit@40: if c in "-.": benoit@40: return c benoit@40: return PCT_MAP[c] benoit@40: return {'!': "%21", '"': "%22", '#': "%23", '$': "%24", '%': "%25", '&': "%26", benoit@40: "'": "%27", '(': "%28", ')': "%29", '*': "%2a", '+': "%2b", ',': "%2c", benoit@40: "-": c, '.': c, '/': "%2f", ':': "%3a", ';': "%3b", '<': "%3c", benoit@40: "=": "%3d", '>': "%3e", '?': "%3f", '@': "%40", '[': "%5b", '\\': "%5c", benoit@40: "]": "%5d", '^': "%5e", '`': "%60", '{': "%7b", '|': "%7c", '}': "%7d", benoit@40: "~": "%7e"}[c] benoit@40: benoit@40: def decode(s): benoit@40: if s == '': benoit@40: return ANY benoit@40: if s == '-': benoit@40: return NA benoit@40: s = s.lower() benoit@40: res = "" benoit@40: idx = 0 benoit@40: embedded = False benoit@40: while idx < len(s): benoit@40: c = s[idx] benoit@40: if c in ".-~": benoit@40: res += "\\" + c benoit@40: embedded = True benoit@40: elif c != '%': benoit@40: res += c benoit@40: embedded = True benoit@40: else: benoit@40: form = s[idx:idx+3] benoit@40: if form == "%01": benoit@40: if (((idx == 0) or (idx == (len(s) - 3))) or benoit@40: ( not embedded and (s[idx-4:idx-1] == "%01")) or benoit@40: (embedded and (len(s) > idx + 6) and (s[idx+3:idx+6] == "%01"))): benoit@40: res += '?' benoit@40: else: benoit@40: raise ValueError benoit@40: elif form == "%02": benoit@40: if (idx == 0) or (idx == len(s) - 3): benoit@40: res += '*' benoit@40: else: benoit@40: raise ValueError benoit@40: else: benoit@40: res += '\\' + PCT_MAP_i[form] benoit@40: embedded = True benoit@40: idx += 2 benoit@40: idx += 1 benoit@40: return CPEAttribute(res) benoit@40: benoit@40: def unbind_value_fs(s): benoit@40: if s == '*': benoit@40: return ANY benoit@40: if s == '-': benoit@40: return NA benoit@40: res = "" benoit@40: idx = 0 benoit@40: embedded = False benoit@40: while idx < len(s): benoit@40: c = s[idx] benoit@40: if re.match("[a-zA-Z0-9_]", c) is not None: benoit@40: res += c benoit@40: embedded = True benoit@40: elif c == "\\": benoit@40: res += s[idx:idx+2] benoit@40: embedded = True benoit@40: idx += 1 benoit@40: elif c == "*": benoit@40: if (idx == 0) or (idx == (len(s) - 1)): benoit@40: res += c benoit@40: embedded = True benoit@40: else: benoit@40: raise ValueError benoit@40: elif c == "?": benoit@40: if (((idx == 0) or (idx == (len(s) - 1))) or benoit@40: (not embedded and (s[idx - 1] == "?")) or benoit@40: (embedded and (s[idx + 1] == "?"))): benoit@40: res += c benoit@40: embedded = False benoit@40: else: benoit@40: raise ValueError benoit@40: else: benoit@40: res += "\\" + c benoit@40: embedded = True benoit@40: idx += 1 benoit@40: return CPEAttribute(res) benoit@40: benoit@40: class CPEAttribute(object): benoit@40: """ We need a special class to deal with ANY / NA / "string" """ benoit@40: benoit@40: def __init__(self, value=None, any=False, na=False): benoit@40: self.any = any benoit@40: self.na = na benoit@40: self.value = value benoit@40: benoit@40: def bind_for_URI(self): benoit@40: # print self.any, self.na, self.value benoit@40: if self.any: benoit@40: return "" benoit@40: if self.na: benoit@40: return '-' benoit@40: return self.transform_for_uri() benoit@40: benoit@40: def transform_for_uri(self): benoit@40: res = "" benoit@40: idx = 0 benoit@40: while idx < len(self.value): benoit@40: c = self.value[idx] benoit@40: if re.match("[a-zA-Z0-9_]", c) is not None: benoit@40: res += c benoit@40: elif c == '\\': benoit@40: idx += 1 benoit@40: c = self.value[idx] benoit@40: res += pct_encode(c) benoit@40: elif c == '?': benoit@40: res += "%01" benoit@40: elif c == '*': benoit@40: res += "%02" benoit@40: idx += 1 benoit@40: return res benoit@40: benoit@40: def bind_for_fs(self): benoit@40: if self.any: benoit@40: return "*" benoit@40: if self.na: benoit@40: return "-" benoit@40: return self.process_quoted_chars() benoit@40: benoit@40: def process_quoted_chars(self): benoit@40: res = "" benoit@40: idx = 0 benoit@40: while idx < len(self.value): benoit@40: c = self.value[idx] benoit@40: if c != '\\': benoit@40: res += c benoit@40: else: benoit@40: idx += 1 benoit@40: c = self.value[idx] benoit@40: if c in ".-_": benoit@40: res += c benoit@40: else: benoit@40: res += '\\' + c benoit@40: idx += 1 benoit@40: return res benoit@40: benoit@40: ANY = CPEAttribute(any=True) benoit@40: NA = CPEAttribute(na=True) benoit@40: benoit@40: class CPE(object): benoit@40: benoit@40: def __init__(self, part=None, vendor=None, product=None, version=None, update=None, edition=None, language=None, sw_edition=None, target_sw=None, target_hw=None, other=None): benoit@40: self.part = part or CPEAttribute(any=True) benoit@40: self.vendor = vendor or CPEAttribute(any=True) benoit@40: self.product = product or CPEAttribute(any=True) benoit@40: self.version = version or CPEAttribute(any=True) benoit@40: self.update = update or CPEAttribute(any=True) benoit@40: self.edition = edition or CPEAttribute(any=True) benoit@40: self.language = language or CPEAttribute(any=True) benoit@40: # Extended attributes: benoit@40: self.sw_edition = sw_edition or CPEAttribute(any=True) benoit@40: self.target_sw = target_sw or CPEAttribute(any=True) benoit@40: self.target_hw = target_hw or CPEAttribute(any=True) benoit@40: self.other = other or CPEAttribute(any=True) benoit@40: benoit@40: def bind_to_URI(self): benoit@40: uri = 'cpe:/' benoit@40: uri += ':'.join(a.bind_for_URI() for a in (self.part, self.vendor, self.product, self.version, self.update)) benoit@40: # Special handling for edition benoit@40: ed = self.edition.bind_for_URI() benoit@40: sw_ed = self.sw_edition.bind_for_URI() benoit@40: t_sw = self.target_sw.bind_for_URI() benoit@40: t_hw = self.target_hw.bind_for_URI() benoit@40: oth = self.other.bind_for_URI() benoit@40: if sw_ed == "" and t_sw == "" and t_hw == "" and oth == "": benoit@40: uri += ":" + ed benoit@40: else: benoit@40: uri += ":~" + '~'.join([ed, sw_ed, t_sw, t_hw, oth]) benoit@40: uri += ':' + self.language.bind_for_URI() benoit@40: return uri.rstrip(':') benoit@40: benoit@40: def unbind_URI(self, uri): benoit@40: for idx, comp in enumerate(uri.split(':')): benoit@40: if idx == 0: benoit@40: continue benoit@40: elif idx == 1: benoit@40: self.part = decode(comp[1:]) benoit@40: elif idx == 2: benoit@40: self.vendor = decode(comp) benoit@40: elif idx == 3: benoit@40: self.product = decode(comp) benoit@40: elif idx == 4: benoit@40: self.version = decode(comp) benoit@40: elif idx == 5: benoit@40: self.update = decode(comp) benoit@40: elif idx == 6: benoit@40: if comp == "" or comp[0] != '~': benoit@40: self.edition = decode(comp) benoit@40: else: benoit@40: ed, sw_ed, t_sw, t_hw, oth = comp[1:].split('~') benoit@40: self.edition = decode(ed) benoit@40: self.sw_edition = decode(sw_ed) benoit@40: self.target_sw = decode(t_sw) benoit@40: self.target_hw = decode(t_hw) benoit@40: self.other = decode(oth) benoit@40: elif idx == 7: benoit@40: self.language = decode(comp) benoit@40: benoit@40: def bind_to_fs(self): benoit@40: fs = 'cpe:2.3:' benoit@40: fs += ':'.join(a.bind_for_fs() for a in (self.part, self.vendor, self.product, self.version, self.update, self.edition, self.language, self.sw_edition, self.target_sw, self.target_hw, self.other)) benoit@40: return fs benoit@40: benoit@40: def unbind_fs(self, fs): benoit@40: for idx, v in enumerate(fs.split(':')): benoit@40: v = unbind_value_fs(v) benoit@40: if idx == 2: benoit@40: self.part = v benoit@40: elif idx == 3: benoit@40: self.vendor = v benoit@40: elif idx == 4: benoit@40: self.product = v benoit@40: elif idx == 5: benoit@40: self.version = v benoit@40: elif idx == 6: benoit@40: self.update = v benoit@40: elif idx == 7: benoit@40: self.edition = v benoit@40: elif idx == 8: benoit@40: self.language = v benoit@40: elif idx == 9: benoit@40: self.sw_edition = v benoit@40: elif idx == 10: benoit@40: self.target_sw = v benoit@40: elif idx == 11: benoit@40: self.target_hw = v benoit@40: elif idx == 12: benoit@40: self.other = v benoit@41: benoit@41: def parse(s): benoit@41: cpe = CPE() benoit@41: if s[:5] == 'cpe:/': benoit@41: cpe.unbind_URI(s) benoit@41: elif s[:8] == 'cpe:2.3:': benoit@41: cpe.unbind_fs(s) benoit@41: else: benoit@41: raise ValueError(s) benoit@41: return cpe