Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/cyclonedx-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,33 @@ foreach ($component->licenses ?? [] as $licenseChoice) {
The same shape applies to `Service::$licenses` and
`ComponentEvidence::$licenses`.

### Vulnerability Analysis: [`VulnerabilityAnalysis`](../src/Entity/Vulnerability/VulnerabilityAnalysis.php)

A VEX document records its verdict in `vulnerabilities[].analysis`. The
`state`, `justification` and `response` fields are enums.

```php
use mteu\SbomParser\Entity\Vulnerability\ImpactAnalysisState;

foreach ($bom->vulnerabilities ?? [] as $vulnerability) {
$analysis = $vulnerability->analysis;

if ($analysis?->state === ImpactAnalysisState::NOT_AFFECTED) {
$analysis->justification; // ImpactAnalysisJustification, e.g. CODE_NOT_REACHABLE
}

$analysis?->response; // list<ImpactAnalysisResponse>, e.g. [UPDATE]
$analysis?->detail; // Free text written by a person: escape it before display
}
```

The vocabulary is the same in every supported spec version. A value outside
it fails the parse with an `SbomParseException` whose message names the path,
such as `vulnerabilities.2.analysis.state`.

A VEX document may carry no `components` at all. It parses like any other
document, and `Bom::hasComponents()` returns `false`.

## File Validation

The parser includes validation:
Expand Down
44 changes: 44 additions & 0 deletions src/Entity/Vulnerability/ImpactAnalysisJustification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

/*
* This file is part of the package "mteu/sbom-parser".
*
* Copyright (C) 2025 Martin Adler <mteu@mailbox.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace mteu\SbomParser\Entity\Vulnerability;

/**
* Impact analysis justification based on CycloneDX 1.4+ specification.
* The vocabulary is identical in every supported spec version.
*
* @author Martin Adler <mteu@mailbox.org>
* @license GPL-3.0-or-later
*/
enum ImpactAnalysisJustification: string
{
case CODE_NOT_PRESENT = 'code_not_present';
case CODE_NOT_REACHABLE = 'code_not_reachable';
case REQUIRES_CONFIGURATION = 'requires_configuration';
case REQUIRES_DEPENDENCY = 'requires_dependency';
case REQUIRES_ENVIRONMENT = 'requires_environment';
case PROTECTED_BY_COMPILER = 'protected_by_compiler';
case PROTECTED_AT_RUNTIME = 'protected_at_runtime';
case PROTECTED_AT_PERIMETER = 'protected_at_perimeter';
case PROTECTED_BY_MITIGATING_CONTROL = 'protected_by_mitigating_control';
}
40 changes: 40 additions & 0 deletions src/Entity/Vulnerability/ImpactAnalysisResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/*
* This file is part of the package "mteu/sbom-parser".
*
* Copyright (C) 2025 Martin Adler <mteu@mailbox.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace mteu\SbomParser\Entity\Vulnerability;

/**
* Impact analysis response based on CycloneDX 1.4+ specification.
* The vocabulary is identical in every supported spec version.
*
* @author Martin Adler <mteu@mailbox.org>
* @license GPL-3.0-or-later
*/
enum ImpactAnalysisResponse: string
{
case CAN_NOT_FIX = 'can_not_fix';
case WILL_NOT_FIX = 'will_not_fix';
case UPDATE = 'update';
case ROLLBACK = 'rollback';
case WORKAROUND_AVAILABLE = 'workaround_available';
}
41 changes: 41 additions & 0 deletions src/Entity/Vulnerability/ImpactAnalysisState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/*
* This file is part of the package "mteu/sbom-parser".
*
* Copyright (C) 2025 Martin Adler <mteu@mailbox.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace mteu\SbomParser\Entity\Vulnerability;

/**
* Impact analysis state based on CycloneDX 1.4+ specification.
* The vocabulary is identical in every supported spec version.
*
* @author Martin Adler <mteu@mailbox.org>
* @license GPL-3.0-or-later
*/
enum ImpactAnalysisState: string
{
case RESOLVED = 'resolved';
case RESOLVED_WITH_PEDIGREE = 'resolved_with_pedigree';
case EXPLOITABLE = 'exploitable';
case IN_TRIAGE = 'in_triage';
case FALSE_POSITIVE = 'false_positive';
case NOT_AFFECTED = 'not_affected';
}
6 changes: 3 additions & 3 deletions src/Entity/Vulnerability/VulnerabilityAnalysis.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
final readonly class VulnerabilityAnalysis
{
public function __construct(
public ?string $state = null,
public ?string $justification = null,
/** @var string[]|null */
public ?ImpactAnalysisState $state = null,
public ?ImpactAnalysisJustification $justification = null,
/** @var list<ImpactAnalysisResponse>|null */
public ?array $response = null,
public ?string $detail = null,
public ?\DateTimeImmutable $firstIssued = null,
Expand Down
2 changes: 2 additions & 0 deletions tests/Fixtures/sbom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `bom-1.4.json`, `bom-1.5.json`, `bom-1.6.json`, `bom-1.7.json` | Generated by `composer generate-sboms` from this package's own dependencies. Do not edit by hand; changes are overwritten on the next run. |
| `bom-1.6-custom.json` | Manually created fields the generator never emits, so parser tests can assert on schema-valid input. |
| `vex-1.6.json` | Written by `mteu/vex`'s `CycloneDxVexWriter`, re-indented with tabs. No `serialNumber`, one id on two components, a `mteu:vex:justification` property. |
| `vex-1.6-without-components.json` | Hand-written. A VEX document may carry vulnerabilities only. |

Every fixture must be registered in
`tests/Integration/SchemaComplianceTest::schemaFixtureProvider()`, so it is
Expand Down
13 changes: 13 additions & 0 deletions tests/Fixtures/sbom/vex-1.6-without-components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"vulnerabilities": [
{
"id": "CVE-2026-1234",
"analysis": {
"state": "in_triage"
}
}
]
}
187 changes: 187 additions & 0 deletions tests/Fixtures/sbom/vex-1.6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"timestamp": "2026-10-08T12:00:00Z"
},
"components": [
{
"type": "library",
"bom-ref": "pkg:composer/guzzlehttp/psr7@2.4.1",
"group": "guzzlehttp",
"name": "psr7",
"version": "2.4.1",
"purl": "pkg:composer/guzzlehttp/psr7@2.4.1"
},
{
"type": "library",
"bom-ref": "pkg:composer/symfony/http-kernel@5.4.19",
"group": "symfony",
"name": "http-kernel",
"version": "5.4.19",
"purl": "pkg:composer/symfony/http-kernel@5.4.19"
},
{
"type": "library",
"bom-ref": "pkg:composer/symfony/http-kernel@6.4.2",
"group": "symfony",
"name": "http-kernel",
"version": "6.4.2",
"purl": "pkg:composer/symfony/http-kernel@6.4.2"
},
{
"type": "library",
"bom-ref": "pkg:composer/twig/twig@3.8.0",
"group": "twig",
"name": "twig",
"version": "3.8.0",
"purl": "pkg:composer/twig/twig@3.8.0"
},
{
"type": "library",
"bom-ref": "pkg:npm/lodash@4.17.20",
"name": "lodash",
"version": "4.17.20",
"purl": "pkg:npm/lodash@4.17.20"
}
],
"vulnerabilities": [
{
"id": "CVE-2026-1234",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-1234"
},
"references": [
{
"id": "GHSA-jfh8-c2jp-5v3q",
"source": {
"name": "GitHub",
"url": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q"
}
}
],
"analysis": {
"state": "not_affected",
"justification": "code_not_reachable",
"detail": "Fragments are never rendered, so the vulnerable path is never called.",
"firstIssued": "2026-10-01T09:15:00Z",
"lastUpdated": "2026-10-01T09:15:00Z"
},
"affects": [
{
"ref": "pkg:composer/symfony/http-kernel@5.4.19"
}
]
},
{
"id": "CVE-2026-1234",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-1234"
},
"references": [
{
"id": "GHSA-jfh8-c2jp-5v3q",
"source": {
"name": "GitHub",
"url": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q"
}
}
],
"analysis": {
"state": "exploitable",
"response": [
"update"
],
"detail": "Fixed by the upgrade planned for the next release.",
"firstIssued": "2026-10-02T10:00:00Z",
"lastUpdated": "2026-10-02T10:00:00Z"
},
"affects": [
{
"ref": "pkg:composer/symfony/http-kernel@6.4.2"
}
]
},
{
"id": "CVE-2026-2345",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-2345"
},
"analysis": {
"state": "resolved",
"detail": "A Composer patch backports the upstream fix.",
"firstIssued": "2026-10-03T11:30:00Z",
"lastUpdated": "2026-10-03T11:30:00Z"
},
"affects": [
{
"ref": "pkg:composer/guzzlehttp/psr7@2.4.1"
}
]
},
{
"id": "CVE-2026-3456",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-3456"
},
"analysis": {
"state": "false_positive",
"detail": "The advisory range is wrong for this release.",
"firstIssued": "2026-10-05T14:45:00Z",
"lastUpdated": "2026-10-05T14:45:00Z"
},
"affects": [
{
"ref": "pkg:composer/twig/twig@3.8.0"
}
]
},
{
"id": "CVE-2026-4567",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-4567"
},
"analysis": {
"state": "in_triage",
"firstIssued": "2026-10-06T16:20:00Z",
"lastUpdated": "2026-10-06T16:20:00Z"
},
"affects": [
{
"ref": "pkg:composer/twig/twig@3.8.0"
}
]
},
{
"id": "GHSA-29mw-wpgm-hmr9",
"source": {
"name": "GitHub",
"url": "https://github.com/advisories/GHSA-29mw-wpgm-hmr9"
},
"analysis": {
"state": "not_affected",
"justification": "code_not_present",
"detail": "A build input that never reaches the bundle.",
"firstIssued": "2026-10-04T08:00:00Z",
"lastUpdated": "2026-10-04T08:00:00Z"
},
"affects": [
{
"ref": "pkg:npm/lodash@4.17.20"
}
],
"properties": [
{
"name": "mteu:vex:justification",
"value": "component_not_present"
}
]
}
]
}
Loading
Loading