diff --git a/pontos/cpe/_cpe.py b/pontos/cpe/_cpe.py index 714b90ce5..f870d2d60 100644 --- a/pontos/cpe/_cpe.py +++ b/pontos/cpe/_cpe.py @@ -253,7 +253,10 @@ def bind_value_for_uri(value: Optional[str]) -> str: return "" if value == NA: return value - return _transform_for_uri(value) + try: + return _transform_for_uri(value) + except Exception as e: + raise CPEParsingError(f"Can't bind '{value}' for URI") from e def unbind_value_uri(value: Optional[str]) -> Optional[str]: @@ -318,6 +321,33 @@ def unbind_value_uri(value: Optional[str]) -> Optional[str]: return result +def split_cpe(cpe: str) -> list[str]: + """ + Split a CPE into its parts + """ + if "\\:" in cpe: + # houston we have a problem + # the cpe string contains an escaped colon (:) + parts = [] + index = 0 + start_index = 0 + stripped_cpe = cpe + while index < len(cpe): + if index > 0 and cpe[index] == ":" and cpe[index - 1] != "\\": + part = cpe[start_index:index] + parts.append(part) + start_index = index + 1 + stripped_cpe = cpe[start_index:] + index += 1 + + if stripped_cpe: + parts.append(stripped_cpe) + else: + parts = cpe.split(":") + + return parts + + @dataclass(frozen=True) # should require keywords only with Python >= 3.10 class CPE: """ @@ -389,7 +419,7 @@ def from_string(cpe: str) -> "CPE": Create a new CPE from a string """ cleaned_cpe = cpe.strip().lower() - parts = cpe.split(":") + parts = split_cpe(cleaned_cpe) if is_uri_binding(cleaned_cpe): values: dict[str, Optional[str]] = dict( diff --git a/tests/cpe/test_cpe.py b/tests/cpe/test_cpe.py index ce23f28c4..d2fca5e20 100644 --- a/tests/cpe/test_cpe.py +++ b/tests/cpe/test_cpe.py @@ -7,6 +7,58 @@ import unittest from pontos.cpe import ANY, CPE, NA, CPEParsingError, Part +from pontos.cpe._cpe import split_cpe + + +class SplitCpeTestCase(unittest.TestCase): + def test_split_uri_cpe(self): + parts = split_cpe("cpe:/o:microsoft:windows_xp:::pro") + + self.assertEqual(len(parts), 7) + self.assertEqual(parts[0], "cpe") + self.assertEqual(parts[1], "/o") + self.assertEqual(parts[2], "microsoft") + self.assertEqual(parts[3], "windows_xp") + self.assertEqual(parts[4], "") + self.assertEqual(parts[5], "") + self.assertEqual(parts[6], "pro") + + def test_split_formatted_cpe(self): + parts = split_cpe( + "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*" + ) + + self.assertEqual(len(parts), 13) + self.assertEqual(parts[0], "cpe") + self.assertEqual(parts[1], "2.3") + self.assertEqual(parts[2], "a") + self.assertEqual(parts[3], "microsoft") + self.assertEqual(parts[4], "internet_explorer") + self.assertEqual(parts[5], "8.0.6001") + self.assertEqual(parts[6], "beta") + self.assertEqual(parts[7], "*") + self.assertEqual(parts[8], "*") + self.assertEqual(parts[9], "*") + self.assertEqual(parts[10], "*") + self.assertEqual(parts[11], "*") + self.assertEqual(parts[12], "*") + + parts = split_cpe("cpe:2.3:a:foo:bar\:mumble:1.0:*:*:*:*:*:*:*") + + self.assertEqual(len(parts), 13) + self.assertEqual(parts[0], "cpe") + self.assertEqual(parts[1], "2.3") + self.assertEqual(parts[2], "a") + self.assertEqual(parts[3], "foo") + self.assertEqual(parts[4], "bar\\:mumble") + self.assertEqual(parts[5], "1.0") + self.assertEqual(parts[6], "*") + self.assertEqual(parts[7], "*") + self.assertEqual(parts[8], "*") + self.assertEqual(parts[9], "*") + self.assertEqual(parts[10], "*") + self.assertEqual(parts[11], "*") + self.assertEqual(parts[12], "*") class CPETestCase(unittest.TestCase): @@ -390,10 +442,10 @@ def test_formatted_unbind_examples(self): self.assertEqual(cpe.part, Part.APPLICATION) self.assertEqual(cpe.vendor, "microsoft") self.assertEqual(cpe.product, "internet_explorer") - # self.assertEqual(cpe.version, "8\.0\.6001") + self.assertEqual(cpe.version, "8\.0\.6001") self.assertEqual(cpe.update, "beta") - self.assertEqual(cpe.language, ANY) self.assertEqual(cpe.edition, ANY) + self.assertEqual(cpe.language, ANY) self.assertEqual(cpe.sw_edition, ANY) self.assertEqual(cpe.target_sw, ANY) self.assertEqual(cpe.target_hw, ANY) @@ -463,14 +515,34 @@ def test_formatted_unbind_examples(self): self.assertEqual(cpe.target_hw, "80gb") self.assertEqual(cpe.other, ANY) + cpe = CPE.from_string("cpe:2.3:a:foo:bar\:mumble:1.0:*:*:*:*:*:*:*") + self.assertFalse(cpe.is_uri_binding()) + self.assertTrue(cpe.is_formatted_string_binding()) + self.assertEqual(cpe.part, Part.APPLICATION) + self.assertEqual(cpe.vendor, "foo") + self.assertEqual(cpe.product, "bar\:mumble") + self.assertEqual(cpe.version, "1\.0") + self.assertEqual(cpe.update, ANY) + self.assertEqual(cpe.edition, ANY) + self.assertEqual(cpe.language, ANY) + self.assertEqual(cpe.sw_edition, ANY) + self.assertEqual(cpe.target_sw, ANY) + self.assertEqual(cpe.target_hw, ANY) + self.assertEqual(cpe.other, ANY) + def test_as_uri_binding(self): - cpe_string = "cpe:2.3:a:microsoft:internet_explorer:8\\.*:sp?" - cpe = CPE.from_string(cpe_string) + cpe = CPE.from_string("cpe:2.3:a:microsoft:internet_explorer:8\\.*:sp?") self.assertEqual( cpe.as_uri_binding(), "cpe:/a:microsoft:internet_explorer:8.%02:sp%01", ) + cpe = CPE.from_string("cpe:2.3:a:cgiirc:cgi\:irc:0.5.7:*:*:*:*:*:*:*") + self.assertEqual( + cpe.as_uri_binding(), + "cpe:/a:cgiirc:cgi%3airc:0.5.7", + ) + def test_as_uri_binding_with_edition(self): cpe_string = "cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win2003:x64" cpe = CPE.from_string(cpe_string)