feat: sbom-import — CycloneDX/SPDX SBOM import with vulnerability matching - #385
Merged
Conversation
…venance Adds the sbomComponent schema (purl, name, version, licenses[], hashes[], type, bomRef, vexCveIds, moduleVersie relation) to the voorzieningen register — wired into both the register's schemas list and its configuration.schemas magic-mapping map, not just the schema definition (the register-config gotcha that silently drops a schema from repair-step import otherwise). moduleVersie gains three optional provenance fields (sbomLastImportedAt, sbomFormat, sbomFileName) and an inverse sbomComponents relation. Fully additive — existing objects remain valid unset. @SPEC openspec/specs/sbom-import/spec.md
…roller SbomParserService: a pure CycloneDX 1.5/1.6 (+ optional SPDX 2.3) JSON parser with no OpenRegister/HTTP dependency — unit-testable against fixture files alone. Rejects unsupported bomFormat/specVersion via UnsupportedSbomFormatException, no partial component list. Also extracts VEX vulnerabilities[] cveId/bom-ref pairs. SbomImportService: parse → soft-delete-aware replace of the previous sbomComponent set → bounded-batch (100/batch) bulk-save of the new set → progress-tracking for imports over 50 components → provenance recorded on moduleVersie (PUT-semantic save, existing fields carried forward). A failed create leaves the version with no component set rather than a mixed old/new one. SbomController: importSbom/getSbomImportStatus routes. Upload is bounded (10MB default) and validated as JSON before the parser ever runs. Auth: admin group OR manage-tier group + manage-ACL (RBAC read) on the target version's parent module — explicit body guard, not just the route annotation (no-admin-idor). Also: SettingsService gains the sbomComponent schema-id key-map entries in all three places moduleVersie already had them (including the normalizeVoorzieningenConfig() allowlist, which was missing sbomComponent even though the other two maps had it — closes a gap where an admin-set schema id would be silently dropped). ObjectService test stub gains saveObjects()/deleteObjects() signatures matching the real OpenRegister API. phpstan-baseline.neon gets one new entry for a structural false-positive (a slim try-block whose only call is to a well-typed local method, so static analysis can't see the OpenRegister-side exceptions the outer catch (\Exception) is deliberately guarding against). 30 PHPUnit tests (8 controller + 12 import service + 10 parser), all passing. PHPCS/PHPMD/PHPStan/Psalm clean on all four new files. @SPEC openspec/specs/sbom-import/spec.md
…ty match
SbomComponentsPanel: ModuleversieDetail sidebar tab ("Components", registered
in customComponents.js and manifest.json) showing the imported sbomComponent
list (name/version/purl/licenses via CnDataTable) with summary counts (total
components, distinct licenses, matched vulnerabilities), an upload control
(format select + file input + Import button posting to SbomController), the
"last imported" provenance line, and an empty state when nothing has been
imported yet.
sbomVulnerabilityMatch.js: the read-time join against kwetsbaarheid —
confirmed (CVE-id) matches via a component's VEX-extracted vexCveIds, and
possible (name/purl) matches scoped to kwetsbaarheid records whose modules
already reference the version's parent module (never a catalogue-wide
scan). Nothing is persisted; editing a kwetsbaarheid changes the match set
on next render with no re-import. Structurally verified to make zero
HTTP/fetch/axios calls.
19 vitest tests, all passing (177/177 across the full suite). Manifest
passes `npm run check:manifest` (ajv schema validation).
@SPEC openspec/specs/sbom-import/spec.md
Every new user-facing string from the Components tab, upload control, and
confirmed/possible match badges, added to both l10n/nl.{js,json} and
l10n/en_US.{js,json} (English source keys per this app's i18n convention).
@SPEC openspec/specs/sbom-import/spec.md
docs/features/sbom-import.md describes the upload flow, replace-on-reimport semantics, what gets stored, and the confirmed/possible vulnerability-match model. tests/e2e/sbom-import.spec.ts covers the two UI-only scenarios (empty state; Components tab reflects an import + a second import replaces the first) by driving the real file input, upload button, and rendered table via data-testid selectors; every other scenario is tagged @e2e with a pointer to its existing PHPUnit/vitest coverage. NOTE: this spec was written and type-checks / lists correctly under `playwright test --list`, but was not executed against a live Nextcloud instance in this session (no docker environment was started for this resume) — screenshots for the docs page are likewise deferred to a live-capture pass. @SPEC openspec/specs/sbom-import/spec.md
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.
rubenvdlinde
added a commit
that referenced
this pull request
Jul 25, 2026
The SBOM provenance properties (sbomLastImportedAt, sbomFormat, sbomFileName, sbomComponents) were defined on the *organisatie* schema instead of *moduleVersie*. SbomImportService::recordProvenance() writes them onto the moduleVersie object and getStatus() reads them back from it, but because the moduleVersie magic table had no such columns the writes were silently dropped by the magic mapper and the import-status endpoint always reported 'never imported' even after a successful import. Moved all four properties to moduleVersie (their descriptions and inversedBy already referenced 'deze versie' / moduleVersie) and bumped the register version to 2.4.2 so the columns are provisioned on upgrade. Live-verified on 8080: import 200 -> component persisted in magic table 11_5037, provenance columns populated on 11_52, status endpoint returns sbomLastImportedAt/Format/FileName, replace-previous soft-deletes the prior set, and CycloneDX VEX + license extraction populate vex_cve_ids and licenses.
rubenvdlinde
added a commit
that referenced
this pull request
Jul 25, 2026
…guard (#385) The sbom-import Playwright spec listed but had never run against a live instance (tracked in #386). Executing it surfaced four issues that all had to be fixed before the spec could exercise the real import flow: - API fixtures returned 401: Nextcloud answers an unauthenticated API request with 401 but no WWW-Authenticate challenge, so Playwright's reactive httpCredentials never sent the Authorization header. Send basic auth pre-emptively (httpCredentials send: 'always'). - The Components tab was reached via a bare /apps/softwarecatalog/... path, which the hash-router treats as an empty hash and falls back to the Dashboard — the tab never mounts. Deep-link with the '#' route. - The first-run onboarding walkthrough overlay intercepted the tab click; dismiss it after navigation. - The Components / History tabs live inside a right-hand sidebar that mounts collapsed, so the tab was present but not visible; open the sidebar first. - upload-and-replace runs two full import round-trips back-to-back; on a loaded shared instance each approaches the default slice, so mark it test.slow() and give the success assertions a 40s budget. Also adds SbomRegisterShapeTest — a pure register-file guard asserting the SBOM provenance properties live on moduleVersie (not organisatie, the misplacement fixed in the prior commit), are optional, and that sbomComponent is a member of the voorzieningen register. 4 tests / 28 assertions. Live-verified on 8080: sbom-import 2/2 and compliance-matrix 2/2 green; SbomRegisterShapeTest 4/4 green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the strict-validated
sbom-importchange (2026-07-23 market-gap wave). The dependency-management research domain had zero implementation; reference architecture is OWASP Dependency-Track (CycloneDX = ECMA-424, SPDX = ISO 5962).Pure CycloneDX 1.5/1.6 + SPDX 2.3 parser (unit-testable without OpenRegister), import service with soft-delete-aware idempotent replace and bounded batches + progress tracking, IDOR-guarded upload endpoint with size bounding, sbomComponent schema + moduleVersie provenance, Components tab with render-time vulnerability matching against existing kwetsbaarheden (confirmed/possible badges). No outbound HTTP.
Two real defects found while finishing: sbomComponent was never added to the voorzieningen register's schemas list or magic-mapping (the repair step would never have created it), and it was missing from SettingsService's schema-id allowlist (admin config silently dropped). Both fixed.
Tests: 390 PHP / 0 fail (30 SBOM-specific against real fixtures), 192/192 vitest, manifest Ajv PASS. openspec validate --specs --strict 48/48. Archived.
Follow-up: the new Playwright spec lists correctly but needs a live-instance run to confirm the sidebar-tab selector.
🤖 Generated with Claude Code