{
  "openapi": "3.1.0",
  "info": {
    "title": "Vandorisk API",
    "version": "1.0.0",
    "summary": "Screen an SBOM from CI and read an assessment's CRA readiness.",
    "description": "Two endpoints, meant for one job: screen an SBOM from a build pipeline, write the result into the product's technical file as dated evidence under Annex I Part I(2), and let the build fail when a high-severity finding has nobody's name against it. The API runs the same screening pipeline as the upload button in the assessment wizard, so a pipeline scan and a hand upload mean the same thing in the file.\n\nVandorisk is guided self-assessment software. Screening an SBOM is evidence toward Annex I Part I(2); it is not a conformity assessment, and the manufacturer remains responsible for the Declaration of Conformity.",
    "contact": {
      "name": "Vandorisk",
      "url": "https://www.vandorisk.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://www.vandorisk.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "SBOM",
      "description": "Screen a bill of materials against OSV."
    },
    {
      "name": "Assessments",
      "description": "Read the readiness of one assessment."
    }
  ],
  "paths": {
    "/api/v1/scan": {
      "post": {
        "operationId": "scanSbom",
        "tags": [
          "SBOM"
        ],
        "summary": "Screen an SBOM and record the result as evidence",
        "description": "Screens every component against OSV and stores an SbomScan record on the assessment. It also appends a dated evidence line to requirement I.2, unless it repeats the latest scan line there (`evidenceLineAdded`). Fail the build when any of `gate.untriagedHigh`, `gate.kev` or `gate.unrated` is above zero, and not on `gate.high`: a finding dispositioned not_affected or fixed in the wizard stops counting, so the gate goes green when the team has answered the findings rather than when the scanner happens to be quiet. `gate.unrated` counts findings whose severity could not be established; unknown is not low, so it stops the build too. `gate.kev` keeps counting a finding an earlier screen of the assessment found listed when this screen could not read CISA's catalogue, but a listing made since then is unknown: when `scan.kevChecked` is false, stop the build and run it again.",
        "x-required-scope": "sbom:scan",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The SBOM was screened and the scan recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, unparseable SBOM, or more than 3000 components.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key, or one whose creator can no longer use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Valid key without the \"sbom:scan\" scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such assessment, or it belongs to another workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request body over 2 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The SBOM parsed but contained no components. Refused deliberately: a \"0 components, clean\" evidence line would read as diligence that never happened.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key rate limit (60 requests/minute), or 300 requests/minute from one network or too many refused keys from it, or over the daily scan quota (500 scans a day per key, 2000 per workspace across its keys and the browser, counted per UTC day). Carries a Retry-After header in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The request failed on the server. Try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/assessments/{id}": {
      "get": {
        "operationId": "getAssessment",
        "tags": [
          "Assessments"
        ],
        "summary": "Read one assessment's readiness",
        "description": "Returns the assessment summary, its readiness scored by the same engine the wizard and the exported technical file use, and the latest SBOM screen. `pct` counts a met answer in full; `evidencedPct` counts a met answer with no evidence behind it at half weight. `blockers` are everything the Declaration of Conformity is held for: hard stops under Art. 13(2)-(4) that cannot be scored away, the Annex V details the product has not recorded, the scope question on step 2 while it is unanswered, the conformity route and the family refusal — treat a non-empty array as a failure. For a product recorded as outside the CRA's scope (`outsideScope`) only the Art. 13(2)-(4) hard stops are listed: the scope answer is what holds its Declaration. `vertical` reports an attached vertical standard's readiness separately; it is voluntary and never adds a blocker.",
        "x-required-scope": "assessments:read",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Assessment id, as shown in the wizard URL.",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The assessment, its readiness, its latest SBOM screen and any attached vertical standard.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssessmentResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key, or one whose creator can no longer use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Valid key without the \"assessments:read\" scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such assessment, or it belongs to another workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key rate limit (60 requests/minute), or 300 requests/minute from one network or too many refused keys from it. Carries a Retry-After header in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The request failed on the server. Try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key minted on the keys page, sent as `Authorization: Bearer vrk_live_…`. The key is shown once, because only its SHA-256 hash is stored. A key belongs to one workspace and carries only the scopes it was created with: sbom:scan, assessments:read. Cookies are ignored on these routes."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable reason the request was refused."
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "Field-level validation output, present on some 400s."
          }
        }
      },
      "ScanRequest": {
        "type": "object",
        "required": [
          "assessmentId",
          "sbom"
        ],
        "properties": {
          "assessmentId": {
            "type": "string",
            "maxLength": 64,
            "description": "The assessment the scan is recorded against."
          },
          "sbom": {
            "type": "object",
            "additionalProperties": true,
            "description": "A parsed CycloneDX 1.4-1.6 or SPDX 2.2/2.3 JSON document — the object itself, not a string."
          }
        }
      },
      "Finding": {
        "type": "object",
        "required": [
          "id",
          "severity",
          "score",
          "component"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "CVE alias where one exists, otherwise the OSV id.",
            "examples": [
              "CVE-2021-23337"
            ]
          },
          "severity": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "HIGH"
            ]
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "examples": [
              7.2
            ]
          },
          "component": {
            "type": "string",
            "examples": [
              "lodash"
            ]
          },
          "version": {
            "type": "string",
            "description": "The version of the copy of the component the advisory was found in, as the SBOM gives it (empty when it gives none). Two versions of one package are two findings.",
            "examples": [
              "4.17.20"
            ]
          },
          "purl": {
            "type": "string",
            "description": "The package URL of that copy, when the SBOM gives one.",
            "examples": [
              "pkg:npm/lodash@4.17.20"
            ]
          },
          "kev": {
            "type": "boolean",
            "description": "Present and true when CISA's Known Exploited Vulnerabilities catalogue lists the finding."
          },
          "kevSince": {
            "type": "string",
            "format": "date",
            "description": "Present with `kev` when the catalogue gives the date the finding was added to it.",
            "examples": [
              "2026-01-15"
            ]
          },
          "epss": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Present when an EPSS score is published for the CVE: the estimated probability, 0 to 1, that the vulnerability is exploited in the next 30 days. Orders findings of equal severity; it is not a gate.",
            "examples": [
              0.042
            ]
          },
          "unrated": {
            "type": "boolean",
            "description": "Present and true when the advisory's details could not be read, so severity and score are null because they are unknown, and the id is the OSV id because no CVE alias could be found. Not a low-severity finding: counted in `gate.unrated` until it is dispositioned."
          }
        }
      },
      "Gate": {
        "type": "object",
        "description": "Everything a pipeline needs to decide an exit code.",
        "required": [
          "total",
          "high",
          "worst",
          "untriagedHigh",
          "kev",
          "unrated"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Advisory rows across all components."
          },
          "high": {
            "type": "integer",
            "description": "Of those, HIGH or CRITICAL."
          },
          "worst": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Finding"
              },
              {
                "type": "null"
              }
            ],
            "description": "The single worst finding, or null when there is none."
          },
          "untriagedHigh": {
            "type": "integer",
            "description": "HIGH/CRITICAL findings with no not_affected or fixed disposition recorded. Fail a build when it is above zero."
          },
          "kev": {
            "type": "integer",
            "description": "Findings with no not_affected or fixed disposition that CISA's KEV catalogue lists as exploited in the wild, by this screen's copy of the catalogue or by the listing an earlier screen of the assessment recorded, so a finding stays counted when the catalogue cannot be read. Treat any value above zero as a hard stop."
          },
          "unrated": {
            "type": "integer",
            "description": "Findings with no not_affected or fixed disposition whose severity could not be established, because the advisory's details could not be read. They may be critical or KEV-listed. Fail a build when it is above zero, and re-run the scan or triage them."
          }
        }
      },
      "ScanResponse": {
        "type": "object",
        "required": [
          "scan",
          "gate"
        ],
        "properties": {
          "scan": {
            "type": "object",
            "required": [
              "id",
              "createdAt",
              "assessmentId",
              "format",
              "componentCount",
              "screenedCount",
              "notScreenableCount",
              "unscannedCount",
              "findings",
              "findingsTruncated",
              "evidenceLine",
              "evidenceLineAdded",
              "kevChecked",
              "enrichmentComplete",
              "unratedCount"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "assessmentId": {
                "type": "string"
              },
              "format": {
                "type": "string",
                "examples": [
                  "cyclonedx",
                  "spdx"
                ]
              },
              "componentCount": {
                "type": "integer"
              },
              "screenedCount": {
                "type": "integer",
                "description": "Components OSV was actually asked about."
              },
              "notScreenableCount": {
                "type": "integer",
                "description": "Components OSV structurally cannot answer for (no purl, unindexed purl type, no usable version). Not a pass and not a failure — check EUVD or NVD for them by hand."
              },
              "unscannedCount": {
                "type": "integer",
                "description": "Components whose OSV lookup failed or did not finish; re-run to complete. A screen with any is incomplete: it clears no finding an earlier complete screen reported, so do not treat it as a pass."
              },
              "findings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Finding"
                },
                "description": "Ranked worst first, capped at 200 rows."
              },
              "findingsTruncated": {
                "type": "boolean"
              },
              "evidenceLine": {
                "type": "string",
                "description": "The dated line this screen wrote. It is appended to requirement I.2 of the assessment unless it says what the latest scan line there already says; see `evidenceLineAdded`."
              },
              "evidenceLineAdded": {
                "type": "boolean",
                "description": "Whether `evidenceLine` was appended to requirement I.2. False when it repeats the latest scan line there: the scan is still recorded, and I.2 gains a line only when the result changes."
              },
              "kevChecked": {
                "type": "boolean",
                "description": "False when CISA's KEV catalogue could not be consulted; missing `kev` flags then mean not checked, never not exploited. `gate.kev` still counts what earlier screens found listed, but not a listing made since: treat false as a failed screen and run it again."
              },
              "enrichmentComplete": {
                "type": "boolean",
                "description": "False when the details of some advisories could not be read. Those findings carry `unrated: true` and were not checked against KEV."
              },
              "unratedCount": {
                "type": "integer",
                "description": "How many findings carry `unrated: true`, whether triaged or not."
              },
              "osvError": {
                "type": "string",
                "description": "Present when OSV was partly unavailable; the counts still say what was screened."
              },
              "warning": {
                "type": "string",
                "description": "For a key that also holds `assessments:read`: present when the SBOM names a different product than the assessment's (CycloneDX `metadata.component`, or the package an SPDX document describes), and names both. The scan still ran and was recorded against this assessment, so check the assessment id the pipeline sends. A key without `assessments:read` gets this field on every scan, empty, whatever the SBOM names."
              }
            }
          },
          "gate": {
            "$ref": "#/components/schemas/Gate"
          }
        }
      },
      "Gap": {
        "type": "object",
        "required": [
          "requirementId",
          "title",
          "legalRef",
          "state"
        ],
        "properties": {
          "requirementId": {
            "type": "string",
            "examples": [
              "P.4"
            ]
          },
          "title": {
            "type": "string"
          },
          "legalRef": {
            "type": "string",
            "examples": [
              "Art. 14"
            ]
          },
          "state": {
            "type": "string",
            "examples": [
              "not met",
              "not assessed",
              "partial"
            ]
          }
        }
      },
      "Readiness": {
        "type": "object",
        "required": [
          "pct",
          "evidencedPct",
          "total",
          "applicable",
          "assessed",
          "blockers",
          "orderedGaps"
        ],
        "properties": {
          "pct": {
            "type": "integer",
            "description": "Percentage complete, counting a met answer in full."
          },
          "evidencedPct": {
            "type": "integer",
            "description": "Percentage with unevidenced met answers at half weight — the honest number."
          },
          "total": {
            "type": "integer"
          },
          "applicable": {
            "type": "integer"
          },
          "assessed": {
            "type": "integer"
          },
          "met": {
            "type": "integer"
          },
          "metEvidenced": {
            "type": "integer"
          },
          "metAsserted": {
            "type": "integer",
            "description": "Met answers with nothing recorded behind them."
          },
          "partial": {
            "type": "integer"
          },
          "notMet": {
            "type": "integer"
          },
          "na": {
            "type": "integer"
          },
          "blockers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Everything the Declaration of Conformity is held for, one sentence each, as the wizard's Readiness step lists them: hard stops under Art. 13(2)-(4), the Annex V details the product has not recorded and a support period that is not one (identityBlockers), the scope question on step 2 while it is unanswered, the conformity route when it is not yet recorded (Art. 32), and the family refusal when the Declaration cannot name every variant. For a product recorded as outside the CRA's scope (outsideScope), only the hard stops under Art. 13(2)-(4): the scope answer is what holds its Declaration, and no detail, route or family changes that. Treat a non-empty array as a failure."
          },
          "identityBlockers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The step-1 details among blockers, on their own: the Annex V identifications the product has not recorded (product name, model, batch or serial number, manufacturer name and address) and a stored support period that does not read as one. Empty when none is missing, and when the product is recorded as outside the CRA's scope (outsideScope)."
          },
          "conformityRoute": {
            "type": [
              "string",
              "null"
            ],
            "description": "Above the default class: why the Declaration cannot yet say how conformity was assessed (the body, the procedure it performed and the certificate, or cited harmonised standards applied in full). Null when nothing is missing, and when the product is recorded as outside the CRA's scope (outsideScope)."
          },
          "outsideScope": {
            "type": "boolean",
            "description": "True when the product is recorded as outside the CRA's scope: no Declaration of Conformity or technical file will be issued for it, whatever the numbers above say."
          },
          "familyDeclarationRefusal": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why no Declaration of Conformity can be issued for this product's family: the Declaration names every variant, and a variant is classified on another conformity route, recorded as outside the CRA's scope, has the scope question on step 2 unanswered, or has no model, batch or serial number. Null when nothing refuses it, when the Declaration names this product alone, or when this product is recorded as outside the CRA's scope (outsideScope). When set, the same sentence is also one of blockers."
          },
          "gapCount": {
            "type": "integer"
          },
          "orderedGaps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Gap"
            },
            "description": "Worst first: \"not met\", then \"not assessed\", then \"partial\"."
          }
        }
      },
      "SbomSummary": {
        "type": "object",
        "description": "The latest SBOM screen on this assessment, or null when it has never been screened.",
        "required": [
          "lastScanAt",
          "componentCount",
          "findingCount",
          "highCount",
          "untriagedHigh",
          "untriagedUnrated",
          "unscannedCount",
          "complete"
        ],
        "properties": {
          "lastScanAt": {
            "type": "string",
            "format": "date-time"
          },
          "format": {
            "type": "string"
          },
          "componentCount": {
            "type": "integer"
          },
          "screenedCount": {
            "type": "integer"
          },
          "notScreenableCount": {
            "type": "integer"
          },
          "findingCount": {
            "type": "integer"
          },
          "highCount": {
            "type": "integer"
          },
          "untriagedHigh": {
            "type": "integer",
            "description": "HIGH or CRITICAL findings with no not_affected or fixed disposition."
          },
          "untriagedTotal": {
            "type": "integer",
            "description": "Findings of any severity with no not_affected or fixed disposition."
          },
          "untriagedUnrated": {
            "type": "integer",
            "description": "Of the untriaged findings, those whose advisory record could not be read: severity unknown, not low. Treat any non-zero value as you treat untriagedHigh."
          },
          "unscannedCount": {
            "type": "integer",
            "description": "Components not screened on this scan because the OSV lookup failed or did not finish."
          },
          "complete": {
            "type": "boolean",
            "description": "False when the scan did not reach every component it could ask about (some were not screened, or the OSV lookup could not be reached), or screened none. Its counts then cannot show that a finding went away."
          }
        }
      },
      "VerticalStandard": {
        "type": "object",
        "description": "A product-category standard attached to the assessment alongside the CRA requirements, scored by the same engine as the wizard. It is an interim draft: meeting it confers no presumption of conformity, so it never changes `readiness` and never adds to `readiness.blockers`.",
        "required": [
          "packId",
          "standardRef",
          "title",
          "useCase",
          "status",
          "readiness"
        ],
        "properties": {
          "packId": {
            "type": "string",
            "examples": [
              "en-304-622"
            ]
          },
          "standardRef": {
            "type": "string",
            "examples": [
              "ETSI EN 304 622"
            ]
          },
          "title": {
            "type": "string",
            "examples": [
              "Security information and event management"
            ]
          },
          "useCase": {
            "type": "string",
            "description": "The use case chosen in the wizard, which decides the requirements that apply.",
            "examples": [
              "UC-3-FLEET"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "interim-draft"
            ],
            "description": "Published draft text, not a standard cited in the Official Journal."
          },
          "readiness": {
            "type": "object",
            "required": [
              "applicable",
              "assessed",
              "met"
            ],
            "properties": {
              "applicable": {
                "type": "integer",
                "description": "Requirements that apply for the use case. The remote data processing requirements are left out when the assessment records none."
              },
              "assessed": {
                "type": "integer",
                "description": "Of those, answered met, partial or not met."
              },
              "met": {
                "type": "integer"
              }
            }
          }
        }
      },
      "AssessmentResponse": {
        "type": "object",
        "required": [
          "assessment",
          "readiness",
          "evidence",
          "sbom",
          "vertical"
        ],
        "properties": {
          "assessment": {
            "type": "object",
            "required": [
              "id",
              "productId",
              "productName",
              "packVersion",
              "currentPackVersion",
              "category",
              "route"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "productId": {
                "type": "string"
              },
              "productName": {
                "type": "string"
              },
              "versionLabel": {
                "type": "string"
              },
              "packId": {
                "type": "string"
              },
              "packVersion": {
                "type": "string",
                "description": "The pack version this assessment was created under."
              },
              "currentPackVersion": {
                "type": "string",
                "description": "The pack the server evaluates against — a difference means the stored assessment predates the bundled pack."
              },
              "category": {
                "type": "string",
                "examples": [
                  "default",
                  "important-1",
                  "important-2",
                  "critical"
                ]
              },
              "route": {
                "type": "string",
                "examples": [
                  "self-assessment"
                ]
              },
              "currentStep": {
                "type": "integer"
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "family": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "id",
                      "name"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The product family the product belongs to, or null. Variants that share one assessment get one Declaration naming every variant."
              }
            }
          },
          "readiness": {
            "$ref": "#/components/schemas/Readiness"
          },
          "evidence": {
            "type": "object",
            "description": "Evidence expectations: what an assessor is likely to ask to see for each requirement, and how much has evidence filed against it. Counted as REFERENCED once the manufacturer's answer says the artefact exists and a document — or a record Vandorisk holds (an SBOM screen, dispositions, published advisories, an Article 14 rehearsal) — points at it. Whether that document satisfies the requirement is the assessor's judgement, not this API's. Drawn from CEN/CENELEC enquiry drafts that are not cited in the Official Journal: advisory only, and never a gate on the Declaration.",
            "required": [
              "expectationsReferenced",
              "expectationsTotal",
              "gaps"
            ],
            "properties": {
              "expectationsReferenced": {
                "type": "integer",
                "description": "Expectations whose answer says the artefact exists AND which carry a document or Vandorisk record."
              },
              "expectationsTotal": {
                "type": "integer",
                "description": "Over requirements answered met or partial; not-applicable expectations excluded."
              },
              "gaps": {
                "type": "array",
                "description": "Requirements answered met or partial with an expectation still open, pack order.",
                "items": {
                  "type": "object",
                  "required": [
                    "requirementId",
                    "title",
                    "legalRef",
                    "referenced",
                    "total"
                  ],
                  "properties": {
                    "requirementId": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "legalRef": {
                      "type": "string"
                    },
                    "referenced": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "sbom": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/SbomSummary"
              },
              {
                "type": "null"
              }
            ]
          },
          "vertical": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VerticalStandard"
              },
              {
                "type": "null"
              }
            ],
            "description": "The attached vertical standard, or null when none is attached or the recorded classification no longer matches the product category it covers."
          }
        }
      }
    }
  }
}