Skip to content

Commit

Permalink
Add: Allow parsing CPEs with parts containing a colon
Browse files Browse the repository at this point in the history
In the real world CPEs contain escaped colons in the vendor or product
parts exist. Therefore allow to parse these CPE strings. Currently
parsing a CPE string like `cpe:2.3:a:foo\\:bar:...` wouldn't work. But
as far as I can see this is neither a valid CPE nor a real world
scenario.
  • Loading branch information
bjoernricks committed Nov 3, 2023
1 parent 9fb52ca commit 872f7cd
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 5 deletions.
29 changes: 28 additions & 1 deletion pontos/cpe/_cpe.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,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:
"""
Expand Down Expand Up @@ -392,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(
Expand Down
80 changes: 76 additions & 4 deletions tests/cpe/test_cpe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 872f7cd

Please sign in to comment.