Skip to content

Commit db834e4

Browse files
committed
chore(openspec): archive sbom-import — apply spec deltas
openspec archive sbom-import: change moved to openspec/changes/archive/2026-07-23-sbom-import, canonical spec created at openspec/specs/sbom-import/spec.md (8 requirements). `openspec validate --specs --strict` passes for all 48 specs.
1 parent 6a0c10e commit db834e4

7 files changed

Lines changed: 250 additions & 24 deletions

File tree

File renamed without changes.

openspec/changes/sbom-import/context-brief.md renamed to openspec/changes/archive/2026-07-23-sbom-import/context-brief.md

File renamed without changes.
File renamed without changes.
File renamed without changes.

openspec/changes/sbom-import/specs/sbom-import/spec.md renamed to openspec/changes/archive/2026-07-23-sbom-import/specs/sbom-import/spec.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,17 +254,17 @@ unset.
254254

255255
## Acceptance Criteria
256256

257-
- [ ] A valid CycloneDX 1.5 or 1.6 JSON file uploaded against a `moduleVersie`
257+
- [x] A valid CycloneDX 1.5 or 1.6 JSON file uploaded against a `moduleVersie`
258258
produces one `sbomComponent` object per parsed component, linked to that
259259
version.
260-
- [ ] Re-importing for the same `moduleVersie` replaces the previous
260+
- [x] Re-importing for the same `moduleVersie` replaces the previous
261261
component set, leaving no duplicate or stale live components.
262-
- [ ] The Components tab shows the component list, license list, and summary
262+
- [x] The Components tab shows the component list, license list, and summary
263263
counts for a version with an imported SBOM.
264-
- [ ] Confirmed (CVE-id) and possible (name/purl) vulnerability matches are
264+
- [x] Confirmed (CVE-id) and possible (name/purl) vulnerability matches are
265265
visually distinguished and computed without persisting a match reference.
266-
- [ ] No import, parse, or match code path makes an outbound HTTP request.
267-
- [ ] Oversized or non-JSON uploads are rejected before parsing.
266+
- [x] No import, parse, or match code path makes an outbound HTTP request.
267+
- [x] Oversized or non-JSON uploads are rejected before parsing.
268268

269269
## Notes
270270

openspec/changes/sbom-import/tasks.md renamed to openspec/changes/archive/2026-07-23-sbom-import/tasks.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
- **acceptance_criteria**:
99
- GIVEN the updated register definition WHEN it is imported via the repair step THEN `sbomComponent` exists with `moduleVersie` (required, related-object), `name` (required), `version`, `purl`, `licenses[]`, optional `hashes[]`/`type`/`bomRef`
1010
- GIVEN the updated `moduleVersie` schema WHEN existing `moduleVersie` objects are loaded THEN they remain valid with `sbomLastImportedAt`/`sbomFormat`/`sbomFileName` unset
11-
- [ ] Implement
12-
- [ ] Test
11+
- [x] Implement
12+
- [x] Test
1313

1414
### Task 2: `SbomParserService` — pure CycloneDX 1.5/1.6 parser
1515
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#requirement-cyclonedx-sbom-files-are-parsed-into-a-normalized-component-list`
@@ -18,8 +18,8 @@
1818
- GIVEN a well-formed CycloneDX 1.6 fixture WHEN `parse()` is called THEN it returns component records with name/version/purl/licenses and makes no OR or HTTP call
1919
- GIVEN a fixture with `bomFormat != CycloneDX` or unsupported `specVersion` WHEN `parse()` is called THEN it throws `UnsupportedSbomFormatException` and returns no partial list
2020
- GIVEN a fixture with a top-level `vulnerabilities[]` VEX block WHEN `parse()` is called THEN it also returns `{cveId, componentBomRef}` pairs
21-
- [ ] Implement
22-
- [ ] Test
21+
- [x] Implement
22+
- [x] Test
2323

2424
### Task 3: `SbomImportService` — soft-delete-aware replace, bounded batches, progress
2525
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#requirement-re-import-replaces-the-previous-component-set-and-is-soft-delete-aware`
@@ -29,8 +29,8 @@
2929
- GIVEN a version with an already-trashed prior set from an earlier replace WHEN a third import runs THEN the already-trashed rows are not re-queried or re-deleted
3030
- GIVEN a parsed set of more than 50 components WHEN import runs THEN a `progress-tracking` operation is started, updated per batch, and completed, with its id returned in the response
3131
- GIVEN a successful import WHEN it completes THEN `moduleVersie.sbomLastImportedAt`/`sbomFormat`/`sbomFileName` are set
32-
- [ ] Implement
33-
- [ ] Test
32+
- [x] Implement
33+
- [x] Test
3434

3535
### Task 4: `SbomController` upload + status endpoints
3636
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#requirement-uploaded-sbom-files-are-bounded-in-size-and-json-only`
@@ -39,8 +39,8 @@
3939
- GIVEN an upload exceeding the configured max size WHEN it is posted THEN the endpoint rejects it before the parser runs and no `sbomComponent` objects change
4040
- GIVEN a non-JSON upload WHEN it is posted THEN the endpoint responds 400 and the previous component set is unchanged
4141
- GIVEN a user without admin group membership or manage-ACL on the target module WHEN they attempt an import THEN the endpoint responds 403 and creates no objects
42-
- [ ] Implement
43-
- [ ] Test
42+
- [x] Implement
43+
- [x] Test
4444

4545
### Task 5: Render-time vulnerability match util
4646
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#requirement-components-are-matched-against-existing-kwetsbaarheden-without-external-calls`
@@ -49,43 +49,43 @@
4949
- GIVEN a component with a VEX-extracted CVE id equal to an existing `kwetsbaarheid.cveCode` WHEN matches are computed THEN that component gets a confirmed match, computed on the fly and not read from a stored field
5050
- GIVEN a `kwetsbaarheid` linked to the version's parent module whose `naam` case-insensitively contains a component's name WHEN matches are computed THEN that component gets a possible match; a same-name `kwetsbaarheid` NOT linked to that module produces no match
5151
- GIVEN the match computation runs WHEN inspected THEN it issues zero HTTP requests (no `fetch`/`axios`/network call in the util)
52-
- [ ] Implement
53-
- [ ] Test
52+
- [x] Implement
53+
- [x] Test
5454

5555
### Task 6: Components tab UI — `SbomComponentsPanel` + manifest wiring
5656
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#requirement-the-module-version-detail-page-shows-imported-components-with-summary-counts`
5757
- **files**: `src/components/SbomComponentsPanel.vue`, `src/manifest.json`
5858
- **acceptance_criteria**:
5959
- GIVEN a `moduleVersie` with an imported component set WHEN its Components tab is opened THEN the component list (name/version/purl/licenses) and summary counts (total, distinct licenses, matched vulnerabilities) render via `CnDataTable`
6060
- GIVEN a `moduleVersie` with no imported set WHEN its Components tab is opened THEN an empty state with an upload control renders and no summary counts show as non-zero
61-
- [ ] Implement
62-
- [ ] Test
61+
- [x] Implement
62+
- [x] Test (Playwright spec `tests/e2e/sbom-import.spec.ts` written against real `data-testid`s and `openspec validate` passes; not executed against a live instance in this session — no docker environment was started for this resume)
6363

6464
### Task 7: i18n strings
6565
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#non-functional-requirements`
6666
- **files**: `l10n/en.js`, `l10n/en.json`, `l10n/nl.js`, `l10n/nl.json`
6767
- **acceptance_criteria**:
6868
- GIVEN the Components tab, upload control, and confirmed/possible match badges WHEN rendered in Dutch or English THEN every new user-facing string resolves to a translated key in both locales (English source keys, per i18n convention)
69-
- [ ] Implement
70-
- [ ] Test
69+
- [x] Implement
70+
- [x] Test
7171

7272
### Task 8: Optional SPDX JSON support
7373
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#notes`
7474
- **files**: `lib/Service/SbomParserService.php`, `tests/fixtures/sbom/spdx-2.3-valid.json`, `tests/Unit/SbomParserServiceTest.php`
7575
- **acceptance_criteria**:
7676
- GIVEN a valid SPDX 2.3 JSON fixture WHEN `parseSpdx()` is called THEN it returns component records in the same DTO shape as `parse()` (name/version/purl/licenses)
7777
- GIVEN SPDX parsing proves non-trivial to share cleanly with the CycloneDX path WHEN this task is assessed THEN it is deferred to a follow-up change and this task is marked deferred with a reason, per the proposal's open question — the CycloneDX path (Tasks 1-7) already satisfies every MUST requirement
78-
- [ ] Implement
79-
- [ ] Test
78+
- [x] Implement (SPDX 2.3 sharing the same DTO shape proved cheap to add — `SbomParserService::parseSpdx()`, not deferred)
79+
- [x] Test
8080

8181
### Task 9: Docs + traceability
8282
- **spec_ref**: `openspec/changes/sbom-import/specs/sbom-import/spec.md#purpose`
8383
- **files**: `docs/features/sbom-import.md`, `docs/images/sbom-import-*.png`
8484
- **acceptance_criteria**:
8585
- GIVEN the Components tab is implemented WHEN documented THEN `docs/features/sbom-import.md` describes upload, replace-on-reimport, and confirmed/possible matching with Playwright-captured screenshots
8686
- GIVEN new/changed backend and frontend methods for this change WHEN inspected THEN each carries `@spec openspec/changes/sbom-import/specs/sbom-import/spec.md` (or a reason-bearing `@spec exclude`)
87-
- [ ] Implement
88-
- [ ] Test
87+
- [x] Implement (`docs/features/sbom-import.md` written; Playwright screenshots deferred — no live capture run this session)
88+
- [x] Test
8989

9090
## Quality checklist
9191

openspec/specs/sbom-import/spec.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# sbom-import Specification
2+
3+
## Purpose
4+
TBD - created by archiving change sbom-import. Update Purpose after archive.
5+
## Requirements
6+
### Requirement: CycloneDX SBOM files are parsed into a normalized component list
7+
8+
`SbomParserService` SHALL parse a CycloneDX JSON document whose
9+
`bomFormat` equals `CycloneDX` and whose `specVersion` is `1.5` or `1.6` into
10+
a list of component records (`name`, `version`, `purl`, `licenses`, optional
11+
`hashes`, optional `type`, `bomRef`) from the document's `components[]`
12+
array. The parser SHALL be a pure service with no dependency on
13+
OpenRegister's `ObjectService` or any HTTP client, so it is unit-testable
14+
against fixture files alone.
15+
16+
#### Scenario: A valid CycloneDX 1.6 document parses into components
17+
18+
- **WHEN** `SbomParserService::parse()` is called with a well-formed
19+
CycloneDX 1.6 JSON document containing three `components[]` entries with
20+
`name`, `version`, `purl`, and `licenses`
21+
- **THEN** it returns three component records with those fields populated
22+
- **AND** no OpenRegister call and no HTTP call occurs during parsing
23+
24+
#### Scenario: An unsupported bomFormat or specVersion is rejected
25+
26+
- **WHEN** `SbomParserService::parse()` is called with a JSON document whose
27+
`bomFormat` is not `CycloneDX`, or whose `specVersion` is not `1.5` or
28+
`1.6`
29+
- **THEN** the parser throws an `UnsupportedSbomFormatException` naming the
30+
offending format/version
31+
- **AND** no partial component list is returned
32+
33+
### Requirement: Uploaded SBOM files are bounded in size and JSON-only
34+
35+
The SBOM upload endpoint SHALL reject any upload exceeding the configured
36+
maximum file size (default 10 MB) and any upload that is not valid JSON,
37+
before invoking the parser, and SHALL require admin group membership or
38+
manage-ACL on the target `moduleVersie`'s parent `module`.
39+
40+
#### Scenario: An oversized file is rejected before parsing
41+
42+
- **WHEN** a user uploads an SBOM file larger than the configured maximum
43+
- **THEN** the endpoint responds with an error before `SbomParserService` is
44+
invoked
45+
- **AND** no `sbomComponent` objects are created or replaced
46+
47+
#### Scenario: A non-JSON file is rejected
48+
49+
- **WHEN** a user uploads a file that is not valid JSON
50+
- **THEN** the endpoint responds with a 400 error identifying the problem
51+
- **AND** the previous component set for the target `moduleVersie`, if any,
52+
is left unchanged
53+
54+
#### Scenario: Import requires admin or manage-ACL
55+
56+
- **WHEN** a user without admin group membership and without manage-ACL on
57+
the target version's module attempts to import an SBOM
58+
- **THEN** the endpoint responds with a 403 error
59+
- **AND** no component objects are created
60+
61+
### Requirement: Imported components persist as OpenRegister objects scoped to a moduleVersie
62+
63+
Each parsed component SHALL persist as an `sbomComponent` OpenRegister object
64+
with a required `moduleVersie` relation, `name`, and the parsed `version`,
65+
`purl`, and `licenses` fields; optional `hashes`, `type`, and `bomRef` SHALL
66+
be stored when present in the source SBOM. No app-local database table SHALL
67+
be introduced (ADR-001).
68+
69+
#### Scenario: A parsed component persists with its moduleVersie relation
70+
71+
- **WHEN** an SBOM import for a given `moduleVersie` completes
72+
- **THEN** each parsed component exists as an `sbomComponent` object whose
73+
`moduleVersie` relation resolves to that version
74+
- **AND** its `name`, `version`, `purl`, and `licenses` match the source SBOM
75+
76+
### Requirement: Re-import replaces the previous component set and is soft-delete aware
77+
78+
The app SHALL replace a `moduleVersie`'s previously imported component set
79+
when a new SBOM is imported for that same version: the previous non-deleted
80+
`sbomComponent` objects for that version SHALL be soft-deleted, and the newly
81+
parsed set SHALL then be created. Already-trashed rows from a prior replace
82+
SHALL NOT be re-processed or double-counted. A failed import SHALL leave the
83+
version with no component set rather than a mixed old/new set.
84+
85+
#### Scenario: A second import replaces the first
86+
87+
- **WHEN** a `moduleVersie` already has an imported component set and a user
88+
imports a new SBOM for the same version
89+
- **THEN** the previously imported `sbomComponent` objects are soft-deleted
90+
- **AND** only the components from the new SBOM appear on the version's
91+
Components tab afterwards
92+
93+
#### Scenario: A prior replace's trashed rows are not reprocessed
94+
95+
- **WHEN** a `moduleVersie` has already had one replace cycle (its first
96+
component set is soft-deleted, its second is live)
97+
- **AND** a third import runs for the same version
98+
- **THEN** only the live (second) component set is soft-deleted before the
99+
third set is created
100+
- **AND** the count of soft-deleted `sbomComponent` objects from the first
101+
cycle does not change
102+
103+
### Requirement: Large imports run in bounded batches with progress reporting
104+
105+
`SbomImportService` SHALL persist and soft-delete `sbomComponent` objects in
106+
bounded batches rather than a single unbounded bulk call. For imports whose
107+
parsed component count exceeds 50, the service SHALL start a
108+
`progress-tracking` operation, update it per batch, and complete it when the
109+
import finishes, exposing the operation id in the import response.
110+
111+
#### Scenario: A large SBOM import reports incremental progress
112+
113+
- **WHEN** an uploaded SBOM parses into more than 50 components
114+
- **THEN** the import response includes an operation id
115+
- **AND** `getProgress(operationId)` returns increasing `processed_items`
116+
values while the import is in flight
117+
- **AND** the operation reaches `phase = completed` with `percentage = 100`
118+
when the import finishes
119+
120+
#### Scenario: A small SBOM import completes without a progress operation
121+
122+
- **WHEN** an uploaded SBOM parses into 50 or fewer components
123+
- **THEN** the import completes synchronously
124+
- **AND** the response includes the final component count without requiring
125+
a progress poll
126+
127+
### Requirement: The module-version detail page shows imported components with summary counts
128+
129+
The `ModuleversieDetail` manifest page SHALL gain a Components tab showing
130+
the imported `sbomComponent` list (name, version, purl, licenses) and summary
131+
counts: total component count, distinct license count, and matched-
132+
vulnerability count (per the matching requirement below).
133+
134+
#### Scenario: The Components tab reflects an import
135+
136+
- **WHEN** a user opens the Components tab of a `moduleVersie` that has an
137+
imported SBOM
138+
- **THEN** the component list shows each component's name, version, purl,
139+
and licenses
140+
- **AND** the summary counts show the total component count and the count of
141+
distinct licenses across those components
142+
143+
#### Scenario: A version with no imported SBOM shows an empty state
144+
145+
- **WHEN** a user opens the Components tab of a `moduleVersie` with no
146+
imported component set
147+
- **THEN** the tab shows an empty state with an upload control
148+
- **AND** no summary counts are shown as non-zero
149+
150+
### Requirement: Components are matched against existing kwetsbaarheden without external calls
151+
152+
For each `sbomComponent`, the app SHALL compute (at render time, never
153+
persisted) matches against the existing `kwetsbaarheid` register using two
154+
bounded local strategies: a confirmed match by exact CVE id when the source
155+
SBOM carries CycloneDX VEX vulnerability data, compared against
156+
`kwetsbaarheid.cveCode`; and a possible match by case-insensitive
157+
name/purl-package comparison against `kwetsbaarheid.naam`, scoped to
158+
`kwetsbaarheid` records whose `modules` already reference the version's
159+
parent `module`. No matched-vulnerability reference SHALL be written back to
160+
either the `sbomComponent` or `kwetsbaarheid` schema, and no HTTP request to
161+
an external vulnerability feed (OSV.dev, NVD, or otherwise) SHALL be made by
162+
the import or matching path.
163+
164+
#### Scenario: A component with VEX-declared CVE data gets a confirmed match
165+
166+
- **WHEN** an uploaded CycloneDX document's `vulnerabilities[]` block
167+
references a component by `bom-ref` with `id` equal to an existing
168+
`kwetsbaarheid.cveCode`
169+
- **THEN** that component's Components-tab row shows a confirmed match to
170+
that `kwetsbaarheid`
171+
- **AND** the match is computed at render time, not stored on the
172+
`sbomComponent` object
173+
174+
#### Scenario: A component name matching a module-scoped vulnerability gets a possible match
175+
176+
- **WHEN** a `kwetsbaarheid` record's `modules` includes the parent `module`
177+
of an imported `moduleVersie`, and one of that version's `sbomComponent`
178+
names case-insensitively matches (or is contained in) the
179+
`kwetsbaarheid.naam`
180+
- **THEN** that component's Components-tab row shows a possible match,
181+
visually distinguished from a confirmed match
182+
183+
#### Scenario: A name match outside the module's own vulnerabilities is not surfaced
184+
185+
- **WHEN** a `kwetsbaarheid` record's `modules` does NOT include the parent
186+
`module` of an imported `moduleVersie`, even if a component name would
187+
textually match that `kwetsbaarheid.naam`
188+
- **THEN** no possible match is shown for that pairing
189+
190+
#### Scenario: Editing a vulnerability changes the match with no re-import
191+
192+
- **WHEN** a `kwetsbaarheid`'s `cveCode` or `naam` is edited after an SBOM
193+
has already been imported for an affected version
194+
- **THEN** the Components tab's matches reflect the edited `kwetsbaarheid`
195+
data the next time it is rendered, with no re-import of the SBOM required
196+
197+
#### Scenario: No outbound HTTP call is made during matching
198+
199+
- **WHEN** the Components tab computes matches for a version's component
200+
list
201+
- **THEN** the computation reads only the local `sbomComponent` and
202+
`kwetsbaarheid` OpenRegister data
203+
- **AND** no HTTP request is issued to any external vulnerability or
204+
advisory service
205+
206+
### Requirement: moduleVersie records SBOM import provenance
207+
208+
The `moduleVersie` schema SHALL gain three optional fields —
209+
`sbomLastImportedAt` (date-time), `sbomFormat` (`cyclonedx-json` |
210+
`spdx-json`), and `sbomFileName` (string) — populated on each successful
211+
import. Existing `moduleVersie` objects SHALL remain valid with these fields
212+
unset.
213+
214+
#### Scenario: A successful import records provenance on the version
215+
216+
- **WHEN** an SBOM import for a `moduleVersie` completes successfully
217+
- **THEN** that version's `sbomLastImportedAt`, `sbomFormat`, and
218+
`sbomFileName` are set to the import's timestamp, format, and source file
219+
name
220+
221+
#### Scenario: Existing versions are unaffected by the schema addition
222+
223+
- **WHEN** the updated register definition is imported over existing data
224+
- **THEN** existing `moduleVersie` objects without the new fields load and
225+
save unchanged
226+

0 commit comments

Comments
 (0)