|
28 | 28 | from cyclonedx.validation.xml import XmlValidator |
29 | 29 |
|
30 | 30 | """ |
31 | | -This example demonstrates how to validate CycloneDX SBOMs (both JSON and XML). |
32 | | -Validation is built upon `jsonschema` for JSON and `lxml` for XML. |
33 | | -To use validation, ensure you have installed the library with the validation extra: |
34 | | - pip install cyclonedx-python-lib[validation] |
35 | | - or |
36 | | - pip install cyclonedx-python-lib[json-validation,xml-validation] |
| 31 | +This example demonstrates how to validate CycloneDX documents (both JSON and XML). |
| 32 | +Make sure to have the needed dependencies installed - install the library's extra 'validation' for that. |
37 | 33 | """ |
38 | 34 |
|
39 | 35 | # region Sample SBOMs |
|
100 | 96 | validation_errors = json_validator.validate_str(INVALID_JSON_SBOM) |
101 | 97 | if validation_errors: |
102 | 98 | print('Validation failed as expected.') |
103 | | - print(f'Error Message: {validation_errors.message}') |
| 99 | + print(f'Error Message: {validation_errors.data.message}') |
104 | 100 | print(f'JSON Path: {validation_errors.data.json_path}') |
105 | 101 | print(f'Invalid Data: {validation_errors.data.instance}') |
106 | 102 | except MissingOptionalDependencyException as error: |
|
119 | 115 | xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_5) |
120 | 116 |
|
121 | 117 | try: |
122 | | - validation_errors = xml_validator.validate_str(XML_SBOM) |
123 | | - if validation_errors: |
| 118 | + xml_validation_errors = xml_validator.validate_str(XML_SBOM) |
| 119 | + if xml_validation_errors: |
124 | 120 | print('XML SBOM is invalid!', file=sys.stderr) |
125 | 121 | else: |
126 | 122 | print('XML SBOM is valid') |
|
138 | 134 | print('--- Dynamic Validation ---') |
139 | 135 |
|
140 | 136 |
|
141 | | -def validate_sbom(raw_data: str) -> bool: |
142 | | - """Validate an SBOM by detecting its format and version.""" |
143 | | - |
144 | | - # 1. Attempt to detect JSON and its version |
| 137 | +def _detect_json_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: |
| 138 | + """Detect JSON format and extract schema version.""" |
145 | 139 | try: |
146 | 140 | data = json.loads(raw_data) |
147 | | - input_format = OutputFormat.JSON |
148 | 141 | spec_version_str = data.get('specVersion') |
149 | 142 | if not spec_version_str: |
150 | 143 | print('Error: Missing specVersion in JSON SBOM', file=sys.stderr) |
151 | | - return False |
| 144 | + return None |
152 | 145 | schema_version = SchemaVersion.from_version(spec_version_str) |
| 146 | + return (OutputFormat.JSON, schema_version) |
153 | 147 | except (json.JSONDecodeError, ValueError): |
154 | | - # 2. Attempt to detect XML and its version |
155 | | - try: |
156 | | - from lxml import etree |
157 | | - xml_tree = etree.fromstring(raw_data.encode('utf-8')) |
158 | | - output_format = OutputFormat.XML |
159 | | - # Extract version from CycloneDX namespace |
160 | | - schema_version = SchemaVersion.V1_5 # Default |
161 | | - for ns in xml_tree.nsmap.values(): |
162 | | - if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): |
163 | | - try: |
164 | | - schema_version = SchemaVersion.from_version(ns.split('/')[-1]) |
165 | | - break |
166 | | - except ValueError: |
167 | | - pass |
168 | | - except (ImportError, etree.XMLSyntaxError): |
169 | | - print('Error: Unknown or malformed SBOM format', file=sys.stderr) |
170 | | - return False |
171 | | - except Exception as e: |
172 | | - print(f'Error: Format detection failed: {e}', file=sys.stderr) |
173 | | - return False |
| 148 | + return None |
| 149 | + |
| 150 | + |
| 151 | +def _detect_xml_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: |
| 152 | + """Detect XML format and extract schema version.""" |
| 153 | + try: |
| 154 | + from lxml import etree # type: ignore[import-untyped] |
| 155 | + xml_tree = etree.fromstring(raw_data.encode('utf-8')) |
| 156 | + # Extract version from CycloneDX namespace |
| 157 | + schema_version = SchemaVersion.V1_5 # Default |
| 158 | + for ns in xml_tree.nsmap.values(): |
| 159 | + if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): |
| 160 | + try: |
| 161 | + schema_version = SchemaVersion.from_version(ns.split('/')[-1]) |
| 162 | + break |
| 163 | + except ValueError: |
| 164 | + pass |
| 165 | + return (OutputFormat.XML, schema_version) |
| 166 | + except (ImportError, etree.XMLSyntaxError): |
| 167 | + print('Error: Unknown or malformed SBOM format', file=sys.stderr) |
| 168 | + return None |
| 169 | + except Exception as e: |
| 170 | + print(f'Error: Format detection failed: {e}', file=sys.stderr) |
| 171 | + return None |
| 172 | + |
| 173 | + |
| 174 | +def validate_sbom(raw_data: str) -> bool: |
| 175 | + """Validate an SBOM by detecting its format and version.""" |
| 176 | + # Detect format and version |
| 177 | + format_info = _detect_json_format(raw_data) or _detect_xml_format(raw_data) |
| 178 | + if not format_info: |
| 179 | + return False |
| 180 | + |
| 181 | + input_format, schema_version = format_info |
174 | 182 |
|
175 | | - # 3. Perform validation using the detected format and version |
| 183 | + # Perform validation |
176 | 184 | try: |
177 | | - validator = make_schemabased_validator(output_format, schema_version) |
| 185 | + validator = make_schemabased_validator(input_format, schema_version) |
178 | 186 | errors = validator.validate_str(raw_data) |
179 | 187 |
|
180 | 188 | if errors: |
181 | | - print(f'Validation failed for {output_format.name} version {schema_version.to_version()}', file=sys.stderr) |
| 189 | + print(f'Validation failed for {input_format.name} version {schema_version.to_version()}', file=sys.stderr) |
182 | 190 | print(f'Reason: {errors}', file=sys.stderr) |
183 | 191 | return False |
184 | 192 |
|
185 | | - print(f'Successfully validated {output_format.name} SBOM (Version {schema_version.to_version()})') |
| 193 | + print(f'Successfully validated {input_format.name} SBOM (Version {schema_version.to_version()})') |
186 | 194 | return True |
187 | 195 |
|
188 | 196 | except MissingOptionalDependencyException as error: |
|
0 commit comments