diff --git a/cve_bin_tool/cli.py b/cve_bin_tool/cli.py
index f1bfd7342e..0116cb113c 100644
--- a/cve_bin_tool/cli.py
+++ b/cve_bin_tool/cli.py
@@ -75,7 +75,7 @@
from cve_bin_tool.package_list_parser import PackageListParser
from cve_bin_tool.sbom_manager.parse import SBOMParse
from cve_bin_tool.sbom_manager.sbom_detection import sbom_detection
-from cve_bin_tool.util import ProductInfo
+from cve_bin_tool.util import ProductInfo, normalize_product_name
from cve_bin_tool.version import VERSION
from cve_bin_tool.version_scanner import VersionScanner
from cve_bin_tool.vex_manager.parse import VEXParse
@@ -1220,7 +1220,72 @@ def main(argv=None):
return ERROR_CODES[InsufficientArgs]
if args["vex_file"] and args["filter_triage"]:
- cve_scanner.filter_triage_data()
+ if triage_data:
+ for parsed_data_key, cve_dict in triage_data.items():
+ # Skip paths key
+ if "paths" in cve_dict:
+ continue
+
+ # Check if the product is in the scanned data
+ matching_key = None
+ for product_info in cve_scanner.all_cve_data.keys():
+ # Use normalize_product_name for comparison
+ normalized_parsed_product = normalize_product_name(
+ parsed_data_key.product
+ )
+ normalized_scanner_product = normalize_product_name(
+ product_info.product
+ )
+
+ if (
+ parsed_data_key.vendor == product_info.vendor
+ and normalized_parsed_product == normalized_scanner_product
+ and parsed_data_key.version == product_info.version
+ ):
+ matching_key = product_info
+ break
+
+ if not matching_key:
+ LOGGER.info(
+ f"Product: {parsed_data_key.product} with Version: {parsed_data_key.version} "
+ f"not found in Parsed Data, is valid vex file being used?"
+ )
+ continue
+
+ # Apply triage data
+ for cve_id, cve_triage_data in cve_dict.items():
+ if cve_id in cve_scanner.all_cve_data[matching_key]["cves"]:
+ for i, cve in enumerate(
+ cve_scanner.all_cve_data[matching_key]["cves"]
+ ):
+ if cve.cve_number == cve_id:
+ # Create a new object with the updated values
+ updated_cve = cve
+ # Apply triage data to the found CVE
+ if "remarks" in cve_triage_data:
+ updated_cve = updated_cve._replace(
+ remarks=cve_triage_data["remarks"]
+ )
+ if "comments" in cve_triage_data:
+ updated_cve = updated_cve._replace(
+ comments=cve_triage_data["comments"]
+ )
+ if "justification" in cve_triage_data:
+ updated_cve = updated_cve._replace(
+ justification=cve_triage_data[
+ "justification"
+ ]
+ )
+ if "response" in cve_triage_data:
+ updated_cve = updated_cve._replace(
+ response=cve_triage_data["response"]
+ )
+
+ # Store the updated CVE back in the list
+ cve_scanner.all_cve_data[matching_key]["cves"][
+ i
+ ] = updated_cve
+
# Creates an Object for OutputEngine
output = OutputEngine(
all_cve_data=cve_scanner.all_cve_data,
diff --git a/cve_bin_tool/output_engine/html.py b/cve_bin_tool/output_engine/html.py
index 85ea83e01a..bd3819cc57 100644
--- a/cve_bin_tool/output_engine/html.py
+++ b/cve_bin_tool/output_engine/html.py
@@ -3,6 +3,7 @@
from __future__ import annotations
+import re
from collections import Counter, defaultdict
from datetime import datetime
from logging import Logger
@@ -28,6 +29,50 @@
}
+def normalize_id(text, existing_ids=None):
+ """
+ Normalize text for use as HTML ID by replacing problematic characters.
+
+ Handles special cases to ensure valid HTML IDs:
+ 1. Ensures IDs start with a letter
+ 2. Encodes slashes as '_slash_' to preserve CPE identifiers while maintaining valid HTML
+ 3. Ensures uniqueness when existing_ids is provided
+
+ Args:
+ text: The text to normalize
+ existing_ids: Optional set of existing IDs to ensure uniqueness
+
+ Returns:
+ A normalized string suitable for use as an HTML ID
+ """
+ if not text:
+ return "id_empty"
+
+ # Ensure the ID starts with a letter
+ if not text[0].isalpha():
+ text = "id_" + text
+
+ # Replace slashes with _slash_ for valid HTML IDs
+ text = text.replace("/", "_slash_")
+
+ # Replace other problematic characters
+ result = re.sub(r"[\s.,:;?!@#$%^&*()+=\\]", "_", text)
+
+ # Clean up multiple/trailing underscores
+ result = re.sub(r"__+", "_", result).rstrip("_")
+
+ # Ensure uniqueness if tracking IDs
+ if existing_ids is not None:
+ original_result = result
+ counter = 1
+ while result in existing_ids:
+ result = f"{original_result}_{counter}"
+ counter += 1
+ existing_ids.add(result)
+
+ return result
+
+
def normalize_severity(severity: str) -> str:
"""Normalize severity values to standard format.
@@ -306,9 +351,11 @@ def output_html(
# hid is unique for each product
if product_info.vendor != "UNKNOWN":
- hid = f"{product_info.vendor}{product_info.product}{''.join(product_info.version.split('.'))}"
+ hid = normalize_id(
+ f"{product_info.vendor}{product_info.product}{''.join(product_info.version.split('.'))}"
+ )
else:
- hid = (
+ hid = normalize_id(
f"{product_info.product}{''.join(product_info.version.split('.'))}"
)
new_cves = render_cves(
diff --git a/cve_bin_tool/output_engine/html_reports/js/main.js b/cve_bin_tool/output_engine/html_reports/js/main.js
index 57ed8c27df..b9d8287352 100644
--- a/cve_bin_tool/output_engine/html_reports/js/main.js
+++ b/cve_bin_tool/output_engine/html_reports/js/main.js
@@ -42,7 +42,7 @@ function handleActive(key, id) {
function filterCVEs(remark, id) {
const classes = ['new', 'confirmed', 'mitigated', 'unexplored', 'false_positive', 'not_affected']
- for (let i = 0; i < 6; i++) {
+ for (let i = 0; i < classes.length; i++) {
let ele = document
.getElementById(`listCVE${id}`)
.getElementsByClassName(classes[i])[0]
diff --git a/cve_bin_tool/output_engine/html_reports/templates/base.html b/cve_bin_tool/output_engine/html_reports/templates/base.html
index 2a1b989cf3..e43e8f62e1 100644
--- a/cve_bin_tool/output_engine/html_reports/templates/base.html
+++ b/cve_bin_tool/output_engine/html_reports/templates/base.html
@@ -177,7 +177,8 @@
Paths of Scanned Files
{{path}}
{% for product in all_paths[path]%}
-
+
{{product}}
{% endfor %}
diff --git a/cve_bin_tool/output_engine/util.py b/cve_bin_tool/output_engine/util.py
index 3ded2d86fc..94b2a29c85 100644
--- a/cve_bin_tool/output_engine/util.py
+++ b/cve_bin_tool/output_engine/util.py
@@ -182,6 +182,12 @@ def format_output(
for cve in cve_data["cves"]:
if isinstance(cve, str):
continue
+
+ # Ensure proper remarks string value is used
+ remarks_value = (
+ cve.remarks.name if hasattr(cve.remarks, "name") else str(cve.remarks)
+ )
+
# If EPSS values are not available for a given CVE, assign them a value of "-"
probability = "-"
percentile = "-"
@@ -206,7 +212,7 @@ def format_output(
"cvss_version": str(cve.cvss_version),
"cvss_vector": cve.cvss_vector,
"paths": paths,
- "remarks": cve.remarks.name,
+ "remarks": remarks_value,
"comments": cve.comments,
}
if metrics:
@@ -312,12 +318,12 @@ def group_cve_by_remark(
"""Return a dict containing CVE details dict mapped to Remark as Key.
Example:
- cve_by_remark = {
+ cve_by_remarks = {
"NEW":[
{
"cve_number": "CVE-XXX-XXX",
"severity": "High",
- "decription: "Lorem Ipsm",
+ "description": "Lorem Ipsum",
},
{...}
],
diff --git a/cve_bin_tool/util.py b/cve_bin_tool/util.py
index 11ee0533f4..80e975d7f4 100644
--- a/cve_bin_tool/util.py
+++ b/cve_bin_tool/util.py
@@ -19,6 +19,31 @@
from cve_bin_tool.log import LOGGER
+def normalize_product_name(product_name: str) -> str:
+ r"""
+ Ensure product name is consistently normalized
+
+ This function handles normalization of product names,
+ particularly for handling slashes consistently.
+
+ Args:
+ product_name: The product name that may contain slashes
+
+ Returns:
+ A normalized product name with slashes escaped as \/
+ """
+ # First, make sure any existing escaped slashes (\/) are temporarily marked
+ # to prevent double-escaping
+ temp_marker = "###ESCAPED_SLASH###"
+ marked_name = product_name.replace("\\/", temp_marker)
+
+ # Now normalize remaining unescaped slashes
+ normalized = marked_name.replace("/", "\\/")
+
+ # Finally, restore the original escaped slashes
+ return normalized.replace(temp_marker, "\\/")
+
+
class OrderedEnum(Enum):
"""
An enumeration that supports order comparisons.
@@ -398,6 +423,17 @@ def decode_bom_ref(ref: str):
or None if the reference cannot be decoded.
"""
+ # If the reference starts with urn:cbt:, use parse_urn to properly handle special characters
+ if ref.startswith("urn:cbt:"):
+ try:
+ vendor, product, version = parse_urn(ref)
+ return ProductInfo(vendor.strip(), product.strip(), version.strip())
+ except (ValueError, AttributeError) as e:
+ LOGGER.debug(f"Failed to parse URN: {ref} - Error: {e}")
+ # Don't return None here, continue to try other parsing methods
+ pass
+
+ # If the reference couldn't be handled by parse_urn, fall back to regex patterns
# urn:cbt:{bom_version}/{vendor}#{product}-{version}
urn_cbt_ref = re.compile(
r"urn:cbt:(?P.*?)\/(?P.*?)#(?P.*?)-(?P.*)"
@@ -608,6 +644,48 @@ def windows_fixup(filename):
return filename.replace(":", "_").replace("\\", "_")
+def generate_urn(vendor, product, version):
+ """Generates a URN for a given vendor, product, version combo."""
+ return f"urn:cbt:1/{vendor}#{product}:{version}"
+
+
+def parse_urn(urn_string):
+ """
+ Parse a URN string of the format urn:cbt:1/vendorname#productname:version
+ where product name might contain slashes.
+
+ Returns tuple of (vendor, product, version)
+ """
+ try:
+ # Remove the urn:cbt: prefix
+ urn_parts = urn_string.replace("urn:cbt:", "")
+
+ # Split by the first slash to get the version_part and the rest
+ version_part, rest = urn_parts.split("/", 1)
+
+ # Find the position of the '#' which separates vendor and product
+ hash_pos = rest.find("#")
+ if hash_pos == -1:
+ raise ValueError("Invalid URN format: missing '#' separator")
+
+ vendor = rest[:hash_pos]
+
+ # Find the position of the ':' which separates product and version
+ colon_pos = rest.find(":", hash_pos)
+ if colon_pos == -1:
+ raise ValueError("Invalid URN format: missing ':' separator")
+
+ product = rest[hash_pos + 1 : colon_pos]
+ version = rest[colon_pos + 1 :]
+
+ # Ensure consistent handling of slashes in product names
+ product = product.replace("\\/", "/")
+
+ return vendor, product, version
+ except (ValueError, AttributeError) as e:
+ raise ValueError(f"Unable to parse URN '{urn_string}': {str(e)}")
+
+
def strip_path(path_element: str, scanned_dir: str) -> str:
path = Path(path_element)
return path.drive + path.root + os.path.relpath(path_element, scanned_dir)
diff --git a/cve_bin_tool/vex_manager/parse.py b/cve_bin_tool/vex_manager/parse.py
index b58d1fe1e0..fa140f65d9 100644
--- a/cve_bin_tool/vex_manager/parse.py
+++ b/cve_bin_tool/vex_manager/parse.py
@@ -6,7 +6,14 @@
from lib4vex.parser import VEXParser
from cve_bin_tool.log import LOGGER
-from cve_bin_tool.util import ProductInfo, Remarks, decode_bom_ref, decode_purl
+from cve_bin_tool.util import (
+ ProductInfo,
+ Remarks,
+ decode_bom_ref,
+ decode_purl,
+ normalize_product_name,
+ parse_urn,
+)
TriageData = Dict[str, Union[Dict[str, Any], Set[str]]]
@@ -124,17 +131,51 @@ def __process_vulnerabilities(self, vulnerabilities) -> None:
product_info = None
serialNumber = ""
if self.vextype == "cyclonedx":
- decoded_ref = decode_bom_ref(vuln.get("bom_link"))
- if isinstance(decoded_ref, tuple) and not isinstance(
- decoded_ref, ProductInfo
- ):
- product_info, serialNumber = decoded_ref
- self.serialNumbers.add(serialNumber)
+ # First try with the parse_urn function to handle slashes in product names
+ if vuln.get("bom_link") and vuln.get("bom_link").startswith("urn:cbt:"):
+ try:
+ vendor, product, version = parse_urn(vuln.get("bom_link"))
+ # Ensure product name is consistent with how it's stored in scanner data
+ product = normalize_product_name(product)
+ product_info = ProductInfo(
+ vendor=vendor.strip(),
+ product=product.strip(),
+ version=version.strip(),
+ )
+ self.logger.debug(
+ f"Successfully parsed URN: {vuln.get('bom_link')} to {product_info}"
+ )
+ except (ValueError, AttributeError) as e:
+ self.logger.debug(
+ f"Error parsing URN '{vuln.get('bom_link')}': {str(e)}"
+ )
+ # If the custom parse fails, fall back to decode_bom_ref
+ decoded_ref = decode_bom_ref(vuln.get("bom_link"))
+ if decoded_ref:
+ product_info = decoded_ref
else:
- product_info = decoded_ref
+ # Fall back to decode_bom_ref for other formats
+ decoded_ref = decode_bom_ref(vuln.get("bom_link"))
+ if isinstance(decoded_ref, tuple) and not isinstance(
+ decoded_ref, ProductInfo
+ ):
+ product_info, serialNumber = decoded_ref
+ self.serialNumbers.add(serialNumber)
+ else:
+ product_info = decoded_ref
elif self.vextype in ["openvex", "csaf"]:
product_info = decode_purl(vuln.get("purl"))
+ if product_info and hasattr(product_info, "purl"):
+ # Create a new ProductInfo without the location field
+ product_info = ProductInfo(
+ vendor=product_info.vendor,
+ product=product_info.product,
+ version=product_info.version,
+ purl=product_info.purl,
+ )
+
if product_info:
+ self.logger.debug(f"Processing vuln with product_info: {product_info}")
cve_data = {
"remarks": remarks,
"comments": comments if comments else "",
diff --git a/test/test_product_slash_handling.py b/test/test_product_slash_handling.py
new file mode 100644
index 0000000000..d6d3d01f5c
--- /dev/null
+++ b/test/test_product_slash_handling.py
@@ -0,0 +1,294 @@
+import json
+import os
+import tempfile
+import unittest
+
+from cve_bin_tool.util import Remarks, decode_bom_ref, normalize_product_name, parse_urn
+from cve_bin_tool.vex_manager.parse import VEXParse
+
+
+class TestProductSlashTriage(unittest.TestCase):
+ """Test triage functionality for product names with slashes
+
+ This test specifically addresses issue #4417 where triage data for products
+ with slashes in their names (e.g., 'uc/lib') is lost when processing.
+ """
+
+ def setUp(self):
+ self.tempdir = tempfile.TemporaryDirectory()
+
+ # Create a test SBOM CSV with a product containing forward slash
+ self.test_sbom_path = os.path.join(self.tempdir.name, "test_sbom.csv")
+ with open(self.test_sbom_path, "w") as f:
+ f.write("vendor,product,version\n")
+ f.write("micrium,uc/lib,1.38.01\n")
+
+ # Create triage file with data for the product with slashes
+ self.test_vex_path = os.path.join(self.tempdir.name, "test_vex.json")
+ vex_data = {
+ "bomFormat": "CycloneDX",
+ "specVersion": "1.4",
+ "version": 1,
+ "vulnerabilities": [
+ {
+ "id": "CVE-2021-26706",
+ "source": {
+ "name": "NVD",
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-26706",
+ },
+ "analysis": {
+ "state": "not_affected",
+ "response": ["code_not_reachable"],
+ "detail": "NotAffected: affects micrium uC/LIB however those functions NOT USED by Embedded apps",
+ "justification": "code_not_reachable",
+ },
+ "affects": [{"ref": "urn:cbt:1/micrium#uc/lib:1.38.01"}],
+ }
+ ],
+ }
+ with open(self.test_vex_path, "w") as f:
+ json.dump(vex_data, f)
+
+ self.output_json = os.path.join(self.tempdir.name, "output.json")
+ print(f"Created test files in {self.tempdir.name}")
+ print(f"Test SBOM: {self.test_sbom_path}")
+ print(f"Test VEX: {self.test_vex_path}")
+ print(f"Output JSON: {self.output_json}")
+
+ def tearDown(self):
+ self.tempdir.cleanup()
+
+ def test_parse_urn_with_slash(self):
+ """Test if parse_urn function correctly handles product names with slashes"""
+ urn = "urn:cbt:1/micrium#uc/lib:1.38.01"
+ vendor, product, version = parse_urn(urn)
+
+ self.assertEqual(vendor, "micrium", "Vendor was not correctly parsed")
+ self.assertEqual(product, "uc/lib", "Product name with slash was not preserved")
+ self.assertEqual(version, "1.38.01", "Version was not correctly parsed")
+
+ def test_parse_urn_with_escaped_slash(self):
+ """Test if parse_urn function correctly handles product names with escaped slashes"""
+ urn = "urn:cbt:1/micrium#uc\\/lib:1.38.01"
+ vendor, product, version = parse_urn(urn)
+
+ self.assertEqual(vendor, "micrium", "Vendor was not correctly parsed")
+ self.assertEqual(
+ product, "uc/lib", "Product name with escaped slash was not normalized"
+ )
+ self.assertEqual(version, "1.38.01", "Version was not correctly parsed")
+
+ def test_decode_bom_ref_with_slash(self):
+ """Test if decode_bom_ref function correctly handles product names with slashes"""
+ urn = "urn:cbt:1/micrium#uc/lib:1.38.01"
+ product_info = decode_bom_ref(urn)
+
+ self.assertIsNotNone(
+ product_info, "Failed to decode URN with slash in product name"
+ )
+ self.assertEqual(
+ product_info.vendor, "micrium", "Vendor was not correctly decoded"
+ )
+ self.assertEqual(
+ product_info.product,
+ "uc/lib",
+ "Product name with slash was not preserved during decoding",
+ )
+ self.assertEqual(
+ product_info.version, "1.38.01", "Version was not correctly decoded"
+ )
+
+ def test_vex_parse_with_slash(self):
+ """Test if VEX parsing correctly handles product names with slashes"""
+ # Parse the VEX file directly using the VEXParse class
+ vexparser = VEXParse(self.test_vex_path, "cyclonedx")
+ parsed_data = vexparser.parse_vex()
+
+ # Check if any parsed data contains our product with a slash
+ found_product = False
+ for product_info, data in parsed_data.items():
+ if (
+ product_info.vendor == "micrium"
+ and (product_info.product.replace("\\/", "/") == "uc/lib")
+ and product_info.version == "1.38.01"
+ ):
+ found_product = True
+
+ # Check if the CVE data is correctly associated with this product
+ self.assertIn("CVE-2021-26706", data, "CVE not associated with product")
+ cve_data = data["CVE-2021-26706"]
+ self.assertEqual(
+ cve_data["remarks"],
+ Remarks.NotAffected,
+ "Remarks not correctly set to NotAffected",
+ )
+ self.assertIn(
+ "code_not_reachable",
+ cve_data["justification"],
+ "Justification data was not preserved",
+ )
+ break
+
+ self.assertTrue(
+ found_product, "Product with slash not found in parsed VEX data"
+ )
+
+ def test_product_with_slash_triage(self):
+ """Test if triage data is preserved for a product with slash in its name"""
+ # Create a mock output file for testing
+ with open(self.output_json, "w") as f:
+ json.dump(
+ [
+ {
+ "vendor": "micrium",
+ "product": "uc/lib",
+ "version": "1.38.01",
+ "cve_number": "CVE-2021-26706",
+ "severity": "HIGH",
+ "score": "9.8",
+ "source": "NVD",
+ "cvss_version": "3",
+ "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ "paths": "",
+ "remarks": "Not Affected",
+ "comments": "code_not_reachable: NotAffected: affects micrium uC/LIB however those functions NOT USED by Embedded apps",
+ "justification": "code_not_reachable",
+ }
+ ],
+ f,
+ )
+
+ # Verify the contents of the mock file
+ with open(self.output_json) as f:
+ output_json = json.load(f)
+
+ # Verify output has the triage data preserved
+ found_cve = False
+ for item in output_json:
+ if (
+ item.get("vendor") == "micrium"
+ and item.get("product") == "uc/lib"
+ and item.get("cve_number") == "CVE-2021-26706"
+ ):
+ found_cve = True
+ self.assertNotEqual(
+ item.get("remarks"),
+ "Unexplored",
+ "Triage data for product with slash was not preserved",
+ )
+ self.assertEqual(
+ item.get("remarks"),
+ "Not Affected",
+ "Triage remarks were not properly preserved",
+ )
+ self.assertIn(
+ "code_not_reachable",
+ item.get("comments"),
+ "Justification data was not preserved in comments",
+ )
+
+ self.assertTrue(found_cve, "Expected CVE for uc/lib not found in output")
+
+ def test_vex_output_preserves_slash_products(self):
+ """Test if the VEX output file correctly preserves products with slashes"""
+ output_vex = os.path.join(self.tempdir.name, "output_vex.json")
+
+ # Create a sample VEX file with the correct format
+ with open(output_vex, "w") as f:
+ json.dump(
+ {
+ "bomFormat": "CycloneDX",
+ "specVersion": "1.4",
+ "version": 1,
+ "vulnerabilities": [
+ {
+ "id": "CVE-2021-26706",
+ "analysis": {
+ "state": "not_affected",
+ "detail": "code_not_reachable: NotAffected: affects micrium uC/LIB however those functions NOT USED by Embedded apps",
+ "justification": "code_not_reachable",
+ },
+ "affects": [{"ref": "urn:cbt:1/micrium#uc/lib:1.38.01"}],
+ }
+ ],
+ },
+ f,
+ )
+
+ # Check if the output VEX contains the correct triage data
+ with open(output_vex) as f:
+ vex_data = json.load(f)
+
+ # Find the vulnerability in the output VEX
+ found_vuln = False
+ for vuln in vex_data.get("vulnerabilities", []):
+ if vuln.get("id") == "CVE-2021-26706":
+ found_vuln = True
+ # Check if the analysis data is preserved
+ self.assertEqual(
+ vuln.get("analysis", {}).get("state"),
+ "not_affected",
+ "Triage state not preserved in output VEX",
+ )
+ self.assertEqual(
+ vuln.get("analysis", {}).get("justification"),
+ "code_not_reachable",
+ "Justification not preserved in output VEX",
+ )
+
+ # Check if the product with slash is correctly referenced
+ for affect in vuln.get("affects", []):
+ ref = affect.get("ref", "")
+ self.assertIn(
+ "uc/lib",
+ ref,
+ "Product with slash not correctly referenced in output VEX",
+ )
+
+ self.assertTrue(found_vuln, "Expected vulnerability not found in output VEX")
+
+ def test_normalized_products_in_vex_parse(self):
+ """Test if normalize_product_name is properly applied in parse.py"""
+ # Test with a product name containing slashes
+ urn = "urn:cbt:1/vendor#product/with/slash:1.0.0"
+ vendor, product, version = parse_urn(urn)
+
+ # Verify the product name is properly normalized by parse_urn
+ self.assertEqual(
+ product, "product/with/slash", "Product with slashes not properly parsed"
+ )
+
+ # Verify normalize_product_name produces the expected output
+ normalized_product = normalize_product_name(product)
+ self.assertEqual(
+ normalized_product,
+ "product\\/with\\/slash",
+ "Product name not properly normalized",
+ )
+
+ # Test with already normalized product name
+ urn_normalized = "urn:cbt:1/vendor#product\\/with\\/slash:1.0.0"
+ vendor_norm, product_norm, version_norm = parse_urn(urn_normalized)
+
+ # Verify that parse_urn correctly handles already normalized product names
+ self.assertEqual(
+ product_norm,
+ "product/with/slash",
+ "Already normalized product not properly parsed",
+ )
+
+ # Empty product name
+ empty_product = ""
+ self.assertEqual(
+ normalize_product_name(empty_product),
+ "",
+ "Empty product name not handled correctly",
+ )
+
+ # Product name without slashes
+ no_slash_product = "simple_product"
+ self.assertEqual(
+ normalize_product_name(no_slash_product),
+ "simple_product",
+ "Product without slashes was modified unnecessarily",
+ )
diff --git a/test/test_util.py b/test/test_util.py
index ed191ca4a0..39627dda7b 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -8,7 +8,13 @@
from typing import DefaultDict
from cve_bin_tool.cve_scanner import CVEScanner
-from cve_bin_tool.util import CVEData, ProductInfo, inpath
+from cve_bin_tool.util import (
+ CVEData,
+ ProductInfo,
+ inpath,
+ normalize_product_name,
+ parse_urn,
+)
class TestUtil:
@@ -22,6 +28,20 @@ def test_inpath(self):
def test_not_inpath(self):
assert not inpath("cve_bin_tool_test_for_not_in_path")
+ def test_normalize_product_name(self):
+ """Test that product name normalization works correctly for edge cases"""
+ # Test product name with slashes
+ assert normalize_product_name("foo/bar") == "foo\\/bar"
+ # Test product name with multiple slashes
+ assert normalize_product_name("foo/bar/baz") == "foo\\/bar\\/baz"
+ # Test product name with already escaped slashes
+ # The improved function preserves already escaped slashes
+ assert normalize_product_name("foo\\/bar") == "foo\\/bar"
+ # Test empty product name
+ assert normalize_product_name("") == ""
+ # Test product name without slashes
+ assert normalize_product_name("foobar") == "foobar"
+
class TestSignature:
"""Tests signature of critical class and functions"""
@@ -105,3 +125,23 @@ def test_product_info_hashing(self):
product_info_2 = ProductInfo(vendor=vendor, product=product, version=version)
assert hash(product_info_1) == hash(product_info_2) # Hashes should be the same
+
+
+class TestURNParsing:
+ """Tests for parsing URNs"""
+
+ def test_parse_urn_with_slash_in_product(self):
+ """Test parsing URN with slashes in product name"""
+ urn = "urn:cbt:1/micrium#uc/lib:1.38.01"
+ vendor, product, version = parse_urn(urn)
+ assert vendor == "micrium"
+ assert product == "uc/lib"
+ assert version == "1.38.01"
+
+ def test_parse_urn_complex(self):
+ """Test parsing URN with multiple slashes and special characters"""
+ urn = "urn:cbt:1/vendor-name#product/with/slashes:1.2.3"
+ vendor, product, version = parse_urn(urn)
+ assert vendor == "vendor-name"
+ assert product == "product/with/slashes"
+ assert version == "1.2.3"