forked from ossf/cve-bin-tool
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsbom_detection.py
More file actions
147 lines (123 loc) · 5.22 KB
/
Copy pathsbom_detection.py
File metadata and controls
147 lines (123 loc) · 5.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# Copyright (C) 2024 Intel Corporation
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from typing import Optional
import defusedxml.ElementTree as ET
from cve_bin_tool.log import LOGGER
from cve_bin_tool.validator import validate_cyclonedx
def sbom_detection(file_path: str) -> Optional[str]:
"""
Identifies SBOM type with content validation and extension checks.
Returns 'spdx', 'cyclonedx', 'swid', or None if the SBOM type cannot be determined.
Args:
file_path (str): Path to the SBOM file.
Returns:
Optional[str]: The detected SBOM type or None if detection fails.
"""
try:
# Check for CycloneDX JSON format
if file_path.lower().endswith(".json"):
try:
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
# Check for CycloneDX-specific structure
if isinstance(data, dict):
if (
data.get("bomFormat") == "CycloneDX"
and "components" in data
):
return "cyclonedx"
# Fallback: Check for required fields
if "specVersion" in data.get(
"bom", {}
) and "version" in data.get("bom", {}):
LOGGER.warning(
f"Possible CycloneDX SBOM with non-standard structure: {file_path}"
)
return "cyclonedx"
except (json.JSONDecodeError, UnicodeDecodeError) as e:
LOGGER.debug(f"JSON parsing error for {file_path}: {str(e)}")
pass # Not JSON, continue with other checks
# Check XML-based formats with namespace validation
if file_path.endswith(".xml"):
try:
tree = ET.parse(file_path)
root = tree.getroot()
namespace = (
root.tag.split("}", 1)[0].strip("{") if "}" in root.tag else ""
)
# Check CycloneDX namespace
if "cyclonedx.org" in namespace and validate_cyclonedx(file_path):
return "cyclonedx"
# Check SWID by root tag and namespace
elif root.tag.endswith("SoftwareIdentity") and "iso/19770" in namespace:
return "swid"
except ET.ParseError as e:
LOGGER.debug(f"XML parsing error for {file_path}: {str(e)}")
return None
# SPDX detection (case-insensitive and path check)
if any(
ext in file_path.lower()
for ext in [".spdx", ".spdx.json", ".spdx.xml", ".spdx.yml", ".spdx.yaml"]
):
return "spdx"
except Exception as e:
LOGGER.error(f"SBOM detection failed for {file_path}: {str(e)}")
return None
def detect_sbom_type_from_content(file_path: str) -> Optional[str]:
"""
Detects SBOM type by analyzing file content without relying on file extensions.
This is a fallback method if the primary detection fails.
Args:
file_path (str): Path to the SBOM file.
Returns:
Optional[str]: The detected SBOM type or None if detection fails.
"""
try:
with open(file_path, "rb") as f:
content = f.read(1024) # Read first 1KB for analysis
# Check for JSON content
if content.startswith(b"{"):
try:
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and data.get("bomFormat") == "CycloneDX":
return "cyclonedx"
elif "SPDXID" in data or "spdxVersion" in data:
return "spdx"
except json.JSONDecodeError:
pass
# Check for XML content
if content.startswith(b"<?xml"):
try:
tree = ET.parse(file_path)
root = tree.getroot()
namespace = (
root.tag.split("}", 1)[0].strip("{") if "}" in root.tag else ""
)
if "cyclonedx.org" in namespace:
return "cyclonedx"
elif "iso/19770" in namespace:
return "swid"
elif "spdx.org" in namespace:
return "spdx"
except ET.ParseError:
pass
except Exception as e:
LOGGER.debug(f"Content-based SBOM detection failed for {file_path}: {str(e)}")
return None
def detect_sbom(file_path: str) -> Optional[str]:
"""
Detects SBOM type using both file extension and content-based methods.
This is the main function to be used for SBOM detection.
Args:
file_path (str): Path to the SBOM file.
Returns:
Optional[str]: The detected SBOM type or None if detection fails.
"""
# First, try detection based on file extension and content
sbom_type = sbom_detection(file_path)
if sbom_type:
return sbom_type
# If primary detection fails, try content-based detection as a fallback
return detect_sbom_type_from_content(file_path)