{
  "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, stores an SbomScan record on the assessment and appends one dated evidence line to requirement I.2. Fail the build on `gate.untriagedHigh`, 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.",
        "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 or revoked API key.",
            "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). Carries a Retry-After header in seconds.",
            "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 hard stops under Art. 13(2)-(4) that cannot be scored away — treat a non-empty array as a failure.",
        "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 and its latest SBOM screen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssessmentResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked API key.",
            "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). Carries a Retry-After header in seconds.",
            "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, and there is no Origin check — a bearer token is never attached automatically by a browser, so there is no CSRF surface to close."
      }
    },
    "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"
            ]
          }
        }
      },
      "Gate": {
        "type": "object",
        "description": "Everything a pipeline needs to decide an exit code.",
        "required": [
          "total",
          "high",
          "worst",
          "untriagedHigh"
        ],
        "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. This is the number to fail a build on."
          }
        }
      },
      "ScanResponse": {
        "type": "object",
        "required": [
          "scan",
          "gate"
        ],
        "properties": {
          "scan": {
            "type": "object",
            "required": [
              "id",
              "createdAt",
              "assessmentId",
              "format",
              "componentCount",
              "screenedCount",
              "notScreenableCount",
              "unscannedCount",
              "findings",
              "findingsTruncated",
              "evidenceLine"
            ],
            "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 batch failed transiently; re-run to complete."
              },
              "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 appended to requirement I.2 of the assessment."
              },
              "osvError": {
                "type": "string",
                "description": "Present when OSV was partly unavailable; the counts still say what was screened."
              }
            }
          },
          "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": "Hard stops under Art. 13(2)-(4). Treat a non-empty array as a failure."
          },
          "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"
        ],
        "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"
          },
          "untriagedTotal": {
            "type": "integer"
          }
        }
      },
      "AssessmentResponse": {
        "type": "object",
        "required": [
          "assessment",
          "readiness",
          "evidence",
          "sbom"
        ],
        "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"
              }
            }
          },
          "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"
              }
            ]
          }
        }
      }
    }
  }
}