diff --git a/api-flow/consumer.md b/api-flow/consumer.md index 1bd7137..3451ecb 100644 --- a/api-flow/consumer.md +++ b/api-flow/consumer.md @@ -23,8 +23,18 @@ Component Release) to find the list of artefacts for the particular Product Rele ## API flow based on TEI discovery ```mermaid - --- +config: + sequence: + diagramMarginX: 60 + diagramMarginY: 40 + actorFontSize: 20 + actorFontWeight: bold + noteFontSize: 18 + theme: neo dark + layout: elk + look: neo + title: TEA consumer flow --- sequenceDiagram @@ -60,7 +70,6 @@ sequenceDiagram user ->> tea_component_release: Obtain latest collections tea_component_release -->> user: List of TEA Artifacts end - ``` ## API flow based on direct access to API @@ -70,6 +79,16 @@ In this case, the client wants to search for a specific product release using th ```mermaid --- +config: + sequence: + diagramMarginX: 60 + diagramMarginY: 40 + actorFontSize: 20 + actorFontWeight: bold + noteFontSize: 18 + theme: neo dark + layout: elk + look: neo title: TEA client flow with search --- @@ -109,6 +128,16 @@ for a release. ```mermaid --- +config: + sequence: + diagramMarginX: 60 + diagramMarginY: 40 + actorFontSize: 20 + actorFontWeight: bold + noteFontSize: 18 + theme: neo dark + layout: elk + look: neo title: TEA client flow with direct query for release --- @@ -137,6 +166,16 @@ another query is done to get reason for update and new collection list of artefa ```mermaid --- +config: + sequence: + diagramMarginX: 60 + diagramMarginY: 40 + actorFontSize: 20 + actorFontWeight: bold + noteFontSize: 18 + theme: neo + layout: elk + look: neo title: TEA client collection query --- diff --git a/doc/authentication.md b/doc/authentication.md new file mode 100644 index 0000000..13f5bf6 --- /dev/null +++ b/doc/authentication.md @@ -0,0 +1,97 @@ +# Authentication + +The Transparency Exchange API (TEA) supports two primary authentication mechanisms: Bearer Token authentication and Mutual TLS (mTLS) authentication. Implementations MUST support at least one of these mechanisms, and SHOULD support both for maximum compatibility. + +## Bearer Token Authentication + +Bearer token authentication uses JSON Web Tokens (JWTs) issued by an external identity provider. This is the recommended authentication mechanism for most use cases. + +### Token Acquisition + +Tokens are acquired out-of-band from the TEA server. The exact process depends on the service provider's identity management system, but typically involves: + +1. User or service authenticates with the provider's portal or API +2. Provider issues a short-lived JWT (recommended: < 1 hour) +3. Client includes the token in API requests + +### Token Format + +Tokens MUST be valid JWTs conforming to RFC 7519. The token payload SHOULD include: + +- `iss`: Issuer identifier +- `sub`: Subject (user or service identifier) +- `aud`: Audience (TEA server identifier) +- `exp`: Expiration time +- `iat`: Issued at time +- `scope`: Space-separated list of authorized scopes + +### Request Format + +Include the token in the `Authorization` header: + +``` +Authorization: Bearer +``` + +### Token Validation + +Servers MUST validate: + +- Token signature using the issuer's public key +- Token expiration (`exp` claim) +- Token audience (`aud` claim) +- Required scopes for the operation + +## Mutual TLS Authentication + +Mutual TLS authentication uses client certificates for mutual authentication between client and server. + +### Certificate Requirements + +- Client certificates MUST use ECDSA P-384 or Ed25519 algorithms +- Certificates MUST be issued by a trusted Certificate Authority (CA) +- Certificate Subject Alternative Name (SAN) MUST include the client identifier + +### TLS Configuration + +- Minimum TLS version: 1.3 +- Server MUST request client certificates +- Server MUST validate certificate chain +- Client MUST present valid certificate + +### Authorization Mapping + +The client certificate's subject or SAN is mapped to TEA identities and scopes through server-side configuration. + +## Authentication Flow + +```mermaid +sequenceDiagram + participant Client + participant TEA Server + participant IdP + + alt Bearer Token + Client->>IdP: Acquire token + IdP-->>Client: JWT token + Client->>TEA Server: Request + Authorization: Bearer + TEA Server->>TEA Server: Validate token + else mTLS + Client->>TEA Server: TLS handshake with client cert + TEA Server->>TEA Server: Validate certificate + end + + TEA Server-->>Client: Authenticated response +``` + +## Error Responses + +- `401 Unauthorized`: Missing or invalid credentials +- `403 Forbidden`: Valid credentials but insufficient permissions + +## Security Considerations + +- Tokens SHOULD be short-lived (< 1 hour) +- mTLS certificates SHOULD have short validity periods +- Implement token revocation mechanisms +- Log authentication failures for security monitoring diff --git a/doc/authorization.md b/doc/authorization.md new file mode 100644 index 0000000..a8a317f --- /dev/null +++ b/doc/authorization.md @@ -0,0 +1,98 @@ +# Authorization + +The Transparency Exchange API (TEA) uses a scope-based authorization model. Permissions are granted through scopes assigned to authenticated identities. Scopes follow a hierarchical structure with read operations generally requiring fewer permissions than write operations. + +## Scope Model + +Scopes are granted to identities during authentication. Bearer tokens include scopes in the `scope` claim as a space-separated list. mTLS certificates are mapped to scopes through server configuration. + +### Scope Hierarchy + +``` +SCOPE_ADMIN_FULL (admin) +├── SCOPE_PUBLISHER_* (publisher) +│ ├── SCOPE_PUBLISHER_PRODUCTS_WRITE +│ ├── SCOPE_PUBLISHER_COMPONENTS_WRITE +│ ├── SCOPE_PUBLISHER_RELEASES_WRITE +│ ├── SCOPE_PUBLISHER_ARTIFACTS_WRITE +│ └── SCOPE_PUBLISHER_COLLECTIONS_WRITE +└── SCOPE_CONSUMER_* (consumer) + ├── SCOPE_CONSUMER_PRODUCTS_READ + ├── SCOPE_CONSUMER_COMPONENTS_READ + ├── SCOPE_CONSUMER_COLLECTIONS_READ + ├── SCOPE_CONSUMER_ARTIFACTS_READ + ├── SCOPE_CONSUMER_ARTIFACTS_DOWNLOAD + └── SCOPE_CONSUMER_INSIGHTS_QUERY +``` + +## Consumer Scopes + +### Read Operations + +- `SCOPE_CONSUMER_PRODUCTS_READ`: List and retrieve product metadata +- `SCOPE_CONSUMER_COMPONENTS_READ`: List and retrieve component metadata +- `SCOPE_CONSUMER_COLLECTIONS_READ`: Access collection metadata and versions +- `SCOPE_CONSUMER_ARTIFACTS_READ`: Retrieve artifact metadata +- `SCOPE_CONSUMER_ARTIFACTS_DOWNLOAD`: Download artifact content +- `SCOPE_CONSUMER_INSIGHTS_QUERY`: Execute CEL-based queries and searches + +## Publisher Scopes + +### Write Operations + +- `SCOPE_PUBLISHER_PRODUCTS_WRITE`: Create, update, delete products +- `SCOPE_PUBLISHER_COMPONENTS_WRITE`: Create, update, delete components +- `SCOPE_PUBLISHER_RELEASES_WRITE`: Create, update product/component releases +- `SCOPE_PUBLISHER_ARTIFACTS_WRITE`: Upload and delete artifacts +- `SCOPE_PUBLISHER_COLLECTIONS_WRITE`: Create and update collections + +## Admin Scopes + +### Administrative Operations + +- `SCOPE_ADMIN_FULL`: Full administrative access including user management, system configuration, and audit functions + +## Authorization Logic + +### API Endpoint Requirements + +| Endpoint | Required Scopes | +| ----------------------------- | ----------------------------------- | +| `GET /.well-known/tea` | None (public) | +| `GET /v1/discovery` | None (public) | +| `GET /v1/products*` | `SCOPE_CONSUMER_PRODUCTS_READ` | +| `GET /v1/components*` | `SCOPE_CONSUMER_COMPONENTS_READ` | +| `GET /v1/collections*` | `SCOPE_CONSUMER_COLLECTIONS_READ` | +| `GET /v1/artifacts*` | `SCOPE_CONSUMER_ARTIFACTS_READ` | +| `GET /v1/artifacts/*/content` | `SCOPE_CONSUMER_ARTIFACTS_DOWNLOAD` | +| `POST /v1/insights/*` | `SCOPE_CONSUMER_INSIGHTS_QUERY` | +| `POST /v1/publisher/*` | Publisher scopes as above | + +### Scope Validation + +Servers MUST validate that the authenticated identity possesses all required scopes for the requested operation. Missing scopes result in `403 Forbidden` responses. + +### Scope Granularity + +Scopes are intentionally coarse-grained to simplify implementation. Fine-grained access control (e.g., per-object permissions) is not supported in the base specification but may be implemented as extensions. + +## Authorization Flow + +```mermaid +flowchart TD + A[Request] --> B[Authenticate] + B --> C{Valid Identity?} + C -->|No| D[401 Unauthorized] + C -->|Yes| E[Extract Scopes] + E --> F{Required Scopes?} + F -->|No| G[403 Forbidden] + F -->|Yes| H[Process Request] + H --> I[Response] +``` + +## Security Considerations + +- Implement least privilege: grant only necessary scopes +- Regularly rotate credentials and review scope assignments +- Audit authorization decisions for compliance +- Consider scope expiration for temporary access diff --git a/doc/tea-requirements.md b/doc/tea-requirements.md index c60a1e6..2f8b070 100644 --- a/doc/tea-requirements.md +++ b/doc/tea-requirements.md @@ -1,6 +1,7 @@ # TEA Requirements ## Repository discovery + Based on an identifier a repository URL needs to be found. The identifier can be: - PURL @@ -34,6 +35,9 @@ Collections are OPTIONAL. - VDR - Vulnerability Disclosure Report - VEX - Vulnerability Exploitability eXchange - CDXA - Attestation +- ML-BOM - Machine Learning Bill of Material (profile of SBOM) +- DATA-BOM - Data Bill of Material (profile of SBOM) +- AI-BOM - Artificial Intelligence Bill of Material (profile of SBOM) Authn/Authz MUST be supported @@ -61,7 +65,7 @@ Authn/Authz MUST be supported ## Artefact Publishing -The API MUST provide a way to publish an artefact, either standalone or to a collection. +The API MUST provide a way to publish an artefact, either standalone or to a collection. The detection of duplicate artefacts with the same identity MUST be handled and prevented. Authn/Authz MUST be supported @@ -85,11 +89,11 @@ PURL, CPE, SWID, GAV, GTIN, and GMN. For example: -- Return the identity of all BOMs that have a vulnerable version of Apache Log4J: +- Return the identity of all BOMs that have a vulnerable version of Apache Log4J: `pkg:maven/org.apache.logging.log4j/log4j-core@2.10.0` -The API MUST provide a way to search for the metadata component across all available BOMs. -The API SHOULD support multiple identity formats including PURL, CPE, SWID, GAV, GTIN, and GMN. +The API MUST provide a way to search for the metadata component across all available BOMs. +The API SHOULD support multiple identity formats including PURL, CPE, SWID, GAV, GTIN, and GMN. For example: - Return the identity of all artefacts that describe `cpe:/a:acme:commerce_suite:1.0`. diff --git a/doc/tea-usecases.md b/doc/tea-usecases.md index bd3b702..916a6a9 100644 --- a/doc/tea-usecases.md +++ b/doc/tea-usecases.md @@ -6,10 +6,10 @@ a..." The use cases are divided in two categories: -* Use cases for __customers__ (end-users, manufacturers) to find a repository with +* Use cases for __customers__ (end-users, manufacturers) to find a repository with Transparency artefacts for a single unit purchased * Use cases where there are different __products__ - * This applies after discovery where we need to handle various things a customer may + * This applies after discovery where we need to handle various things a customer may buy as a single unit ## Customer focused use cases @@ -18,18 +18,17 @@ The use cases are divided in two categories: As a consumer that has an SBOM for a product, I want to be able to retrieve VEX and VDR files automatically both for current and old versions of the software. In the SBOM the product is identified by a PURL or other means (CPE, …) - ### C2: Consumer: Automation based on product name/identifier As a consumer, I want to download artefacts for a product based on known data. A combination of manufacturer, product name, vendor product ID, EAN bar code or other unique identifier. -After discovering the base repository URL I want to be able to find a specific +After discovering the base repository URL I want to be able to find a specific product variant and version. If the consumer is a business, then the procurement process may include delivery of an SBOM with proper identifiers and possibly URLs or identifiers in another document, which may bootstrap the discovery process in a more exact way than in the case of buying a product in a retail market. Alice bought a gadget at the gadget store that contains a full Linux system. Where and how will she find the SBOM and VEX for the gadget? -### C3: Consumer: Artefact retrieval +### C3: Consumer: Artefact retrieval As a consumer, I want to retrieve one or more supply chain artefacts for the products that I have access to, possibly through licensing or other means. As a consumer, I should be able to retrieve all source artefacts such as xBOMs, VDR/VEX, CDXA, and CLE. @@ -42,8 +41,8 @@ A CLE captures all lifecycle events over time, however, there is a need to retri As a consumer, I want the ability to simply ask the API questions rather than having to download, process, and analyze raw supply chain artefacts on my own systems. Common questions should be -provided by the API by default along with the ability to query for more complex answers using -the Common Expression Language (CEL). +provided by the API by default along with the ability to query for more complex answers using +the Common Expression Language (CEL). _NOTE_: Project Hyades (Dependency-Track v5) already implements CEL with the CycloneDX object model and has proven that this approach works for complex queries. @@ -82,7 +81,6 @@ They need to make an assessment before starting tests and possible production us The can either use the Debian package, the Alpine Linux Package or build a binary themselves from source code. How can they find the transparency exchange data sets? - ### O1: Open Source project The hatturl open source project publish a library and a server side software on Github. This is diff --git a/doc/tea_consumer_flow.excalidraw b/doc/tea_consumer_flow.excalidraw new file mode 100644 index 0000000..221943a --- /dev/null +++ b/doc/tea_consumer_flow.excalidraw @@ -0,0 +1,1092 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "version": 2, + "versionNonce": 623532812, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 313667311, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "text", + "id": "t_seq", + "x": 300, + "y": 850, + "text": "TEA Consumer & Discovery Flow", + "fontSize": 28, + "strokeColor": "#1e1e1e", + "width": 446.6, + "height": 35, + "baseline": 31, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "version": 2, + "versionNonce": 288208189, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 508448315, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "uHead2_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "uHead2", + "x": 50, + "y": 950, + "width": 120, + "height": 40, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#4a9eed", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "uHead2_text", + "x": 66, + "y": 960, + "width": 88, + "height": 20, + "text": "TEA Client", + "fontSize": 16, + "fontFamily": 1, + "containerId": "uHead2", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 709554373, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 442489299, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 16 + }, + { + "version": 2, + "versionNonce": 166791788, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 326732043, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "uLine2", + "x": 110, + "y": 990, + "width": 0, + "height": 550, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 550 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 442817095, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 45220990, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "ellipse", + "id": "uh2", + "x": 100, + "y": 900, + "width": 20, + "height": 20, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeColor": "#4a9eed", + "strokeWidth": 2 + }, + { + "version": 2, + "versionNonce": 479732483, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 329452988, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "ub2", + "x": 100, + "y": 922, + "width": 20, + "height": 20, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#4a9eed", + "strokeWidth": 2 + }, + { + "version": 2, + "versionNonce": 367400102, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 499749119, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "dHead_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "dHead", + "x": 300, + "y": 950, + "width": 160, + "height": 40, + "backgroundColor": "#fff3bf", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#f59e0b", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "dHead_text", + "x": 291.45, + "y": 961.25, + "width": 177.10000000000002, + "height": 17.5, + "text": "Discovery (.well-known)", + "fontSize": 14, + "fontFamily": 1, + "containerId": "dHead", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 836560960, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 268053522, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 165355887, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 225359599, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "dLine", + "x": 380, + "y": 990, + "width": 0, + "height": 550, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 550 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 434646783, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 963980987, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "prHead_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "prHead", + "x": 550, + "y": 950, + "width": 160, + "height": 40, + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#22c55e", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "prHead_text", + "x": 556.85, + "y": 961.25, + "width": 146.3, + "height": 17.5, + "text": "TEA Product Release", + "fontSize": 14, + "fontFamily": 1, + "containerId": "prHead", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 532673844, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 416354069, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 546612311, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 12876854, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "prLine", + "x": 630, + "y": 990, + "width": 0, + "height": 550, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 550 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 986414963, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 60246222, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "crHead_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "crHead", + "x": 800, + "y": 950, + "width": 180, + "height": 40, + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#8b5cf6", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "crHead_text", + "x": 809.15, + "y": 961.25, + "width": 161.70000000000002, + "height": 17.5, + "text": "TEA Component Release", + "fontSize": 14, + "fontFamily": 1, + "containerId": "crHead", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 678700252, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 159387056, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 628557341, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 604831108, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "crLine", + "x": 890, + "y": 990, + "width": 0, + "height": 550, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 550 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 30546574, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 135177463, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq1_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq1", + "x": 110, + "y": 1020, + "width": 270, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 270, + 0 + ] + ], + "strokeColor": "#f59e0b", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq1_text", + "x": 156.45, + "y": 1011.25, + "width": 177.10000000000002, + "height": 17.5, + "text": "1. GET /.well-known/tea", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq1", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 349482594, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 789890610, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 847461378, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 173622756, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq2_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq2", + "x": 380, + "y": 1060, + "width": -270, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -270, + 0 + ] + ], + "strokeColor": "#f59e0b", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq2_text", + "x": 152.6, + "y": 1051.25, + "width": 184.8, + "height": 17.5, + "text": "2. Returns API endpoints", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq2", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 110092748, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 31118657, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 255758566, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 900025800, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq3_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq3", + "x": 110, + "y": 1110, + "width": 270, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 270, + 0 + ] + ], + "strokeColor": "#f59e0b", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq3_text", + "x": 152.6, + "y": 1101.25, + "width": 184.8, + "height": 17.5, + "text": "3. Call Discovery w/ TEI", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq3", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 329326862, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 811770140, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 26311709, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 127687205, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq4_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq4", + "x": 380, + "y": 1150, + "width": -270, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -270, + 0 + ] + ], + "strokeColor": "#f59e0b", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq4_text", + "x": 137.2, + "y": 1141.25, + "width": 215.60000000000002, + "height": 17.5, + "text": "4. Return Product Release(s)", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq4", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 158861047, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 536938846, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 625818849, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 962926849, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq5_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq5", + "x": 110, + "y": 1200, + "width": 520, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 520, + 0 + ] + ], + "strokeColor": "#22c55e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq5_text", + "x": 281.45, + "y": 1191.25, + "width": 177.10000000000002, + "height": 17.5, + "text": "5. Resolve Prod Release", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq5", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 437731164, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 921428612, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 870790990, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 93929788, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq6_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq6", + "x": 630, + "y": 1240, + "width": -520, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -520, + 0 + ] + ], + "strokeColor": "#22c55e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq6_text", + "x": 262.2, + "y": 1231.25, + "width": 215.60000000000002, + "height": 17.5, + "text": "6. Return Component Releases", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq6", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 613806409, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 71366738, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 63362911, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 126503006, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq7_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq7", + "x": 110, + "y": 1290, + "width": 780, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 780, + 0 + ] + ], + "strokeColor": "#8b5cf6", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq7_text", + "x": 311.35, + "y": 1281.25, + "width": 377.3, + "height": 17.5, + "text": "7. For each Component Release, obtain collections", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq7", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 338008809, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 781177126, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 108508532, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 215234389, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "seq8_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "seq8", + "x": 890, + "y": 1330, + "width": -780, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -780, + 0 + ] + ], + "strokeColor": "#8b5cf6", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "seq8_text", + "x": 411.45, + "y": 1321.25, + "width": 177.10000000000002, + "height": 17.5, + "text": "8. Return TEA Artifacts", + "fontSize": 14, + "fontFamily": 1, + "containerId": "seq8", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 78058391, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 889122579, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "currentItemStrokeColor": "#1e1e1e", + "currentItemBackgroundColor": "transparent", + "currentItemFillStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemStrokeStyle": "solid", + "currentItemRoughness": 1, + "currentItemOpacity": 100, + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemTextAlign": "left", + "currentItemStrokeSharpness": "round" + } +} \ No newline at end of file diff --git a/doc/tea_data_model.excalidraw b/doc/tea_data_model.excalidraw new file mode 100644 index 0000000..bb60eae --- /dev/null +++ b/doc/tea_data_model.excalidraw @@ -0,0 +1,1015 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "version": 2, + "versionNonce": 167281445, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 866940117, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "text", + "id": "title", + "x": 300, + "y": 20, + "text": "Transparency Exchange API (TEA) Data Model", + "fontSize": 28, + "strokeColor": "#1e1e1e", + "width": 646.8000000000001, + "height": 35, + "baseline": 31, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "version": 2, + "versionNonce": 290557472, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 785009667, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "tei_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "tei", + "x": 50, + "y": 200, + "width": 220, + "height": 80, + "backgroundColor": "#fff3bf", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#f59e0b" + }, + { + "type": "text", + "id": "tei_text", + "x": 76.39999999999999, + "y": 220, + "width": 167.20000000000002, + "height": 40, + "text": "Discovery (TEI)\nResolves to Release", + "fontSize": 16, + "fontFamily": 1, + "containerId": "tei", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 238103836, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 276140534, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 36 + }, + { + "version": 2, + "versionNonce": 345925228, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 178738737, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "product_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "product", + "x": 400, + "y": 100, + "width": 200, + "height": 80, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#4a9eed" + }, + { + "type": "text", + "id": "product_text", + "x": 438.4, + "y": 120, + "width": 123.20000000000002, + "height": 40, + "text": "TEA Product\n(Product Line)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "product", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 318599718, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 576954118, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 36 + }, + { + "version": 2, + "versionNonce": 717272336, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 12733120, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "prod_rel_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "prod_rel", + "x": 400, + "y": 300, + "width": 200, + "height": 80, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#4a9eed" + }, + { + "type": "text", + "id": "prod_rel_text", + "x": 416.4, + "y": 320, + "width": 167.20000000000002, + "height": 40, + "text": "TEA Product Release\n(Primary Entry)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "prod_rel", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 559444216, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 645638008, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 36 + }, + { + "version": 2, + "versionNonce": 235904703, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 734388130, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_tei_strel_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_tei_strel", + "x": 270, + "y": 240, + "width": 130, + "height": 60, + "points": [ + [ + 0, + 0 + ], + [ + 130, + 60 + ] + ], + "strokeColor": "#f59e0b", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "tei", + "fixedPoint": [ + 1, + 0.5 + ] + }, + "endBinding": { + "elementId": "prod_rel", + "fixedPoint": [ + 0, + 0.5 + ] + }, + "startArrowhead": null + }, + { + "type": "text", + "id": "a_tei_strel_text", + "x": 292.65, + "y": 261.25, + "width": 84.7, + "height": 17.5, + "text": "resolves to", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_tei_strel", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 751043349, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 227857383, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 697299633, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 710202603, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_prod_rel_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_prod_rel", + "x": 500, + "y": 180, + "width": 0, + "height": 120, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 120 + ] + ], + "strokeColor": "#4a9eed", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "product", + "fixedPoint": [ + 0.5, + 1 + ] + }, + "endBinding": { + "elementId": "prod_rel", + "fixedPoint": [ + 0.5, + 0 + ] + }, + "startArrowhead": null + }, + { + "type": "text", + "id": "a_prod_rel_text", + "x": 453.8, + "y": 231.25, + "width": 92.4, + "height": 17.5, + "text": "has releases", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_prod_rel", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 122921120, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 554857509, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 232196529, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 449554058, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "component_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "component", + "x": 800, + "y": 100, + "width": 200, + "height": 80, + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#8b5cf6" + }, + { + "type": "text", + "id": "component_text", + "x": 842.8, + "y": 120, + "width": 114.4, + "height": 40, + "text": "TEA Component\n(Lineage)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "component", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 674237119, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 422435724, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 36 + }, + { + "version": 2, + "versionNonce": 931136153, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 147374708, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "comp_rel_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "comp_rel", + "x": 800, + "y": 300, + "width": 240, + "height": 80, + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#8b5cf6" + }, + { + "type": "text", + "id": "comp_rel_text", + "x": 812.2, + "y": 322.5, + "width": 215.60000000000002, + "height": 35, + "text": "TEA Component Release\n(/component/{uuid}/releases)", + "fontSize": 14, + "fontFamily": 1, + "containerId": "comp_rel", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 878891488, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 23290723, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 31 + }, + { + "version": 2, + "versionNonce": 370736448, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 401196270, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_comp_rel_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_comp_rel", + "x": 900, + "y": 180, + "width": 0, + "height": 120, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 120 + ] + ], + "strokeColor": "#8b5cf6", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "component", + "fixedPoint": [ + 0.5, + 1 + ] + }, + "endBinding": { + "elementId": "comp_rel", + "fixedPoint": [ + 0.5, + 0 + ] + }, + "startArrowhead": null + }, + { + "type": "text", + "id": "a_comp_rel_text", + "x": 853.8, + "y": 231.25, + "width": 92.4, + "height": 17.5, + "text": "has releases", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_comp_rel", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 598949984, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 312229238, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 113491510, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 429136782, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "collection_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "collection", + "x": 600, + "y": 500, + "width": 220, + "height": 80, + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#22c55e" + }, + { + "type": "text", + "id": "collection_text", + "x": 639.6, + "y": 520, + "width": 140.8, + "height": 40, + "text": "TEA Collection\n(Versioned list)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "collection", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 584982859, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 587906769, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 36 + }, + { + "version": 2, + "versionNonce": 499529419, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 761705895, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_prel_coll_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_prel_coll", + "x": 500, + "y": 380, + "width": 100, + "height": 160, + "points": [ + [ + 0, + 0 + ], + [ + 100, + 160 + ] + ], + "strokeColor": "#1e1e1e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "prod_rel", + "fixedPoint": [ + 0.5, + 1 + ] + }, + "endBinding": { + "elementId": "collection", + "fixedPoint": [ + 0, + 0.5 + ] + }, + "startArrowhead": null + }, + { + "type": "text", + "id": "a_prel_coll_text", + "x": 538.45, + "y": 451.25, + "width": 23.1, + "height": 17.5, + "text": "has", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_prel_coll", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 338494172, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 166154473, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 835357745, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 947892553, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_crel_coll_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_crel_coll", + "x": 900, + "y": 380, + "width": -80, + "height": 160, + "points": [ + [ + 0, + 0 + ], + [ + -80, + 160 + ] + ], + "strokeColor": "#1e1e1e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "comp_rel", + "fixedPoint": [ + 0.5, + 1 + ] + }, + "endBinding": { + "elementId": "collection", + "fixedPoint": [ + 1, + 0.5 + ] + }, + "startArrowhead": null + }, + { + "type": "text", + "id": "a_crel_coll_text", + "x": 848.45, + "y": 451.25, + "width": 23.1, + "height": 17.5, + "text": "has", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_crel_coll", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 625958386, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 497177979, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 441589794, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 464410241, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "artifact_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "rectangle", + "id": "artifact", + "x": 600, + "y": 700, + "width": 220, + "height": 80, + "backgroundColor": "#eebefa", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#ec4899" + }, + { + "type": "text", + "id": "artifact_text", + "x": 613.2, + "y": 720, + "width": 193.60000000000002, + "height": 40, + "text": "TEA Artifact\n(xBOM, VEX, CDXA, CLE)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "artifact", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 3658297, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 886159966, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 36 + }, + { + "version": 2, + "versionNonce": 64546817, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 412898613, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_coll_art_text", + "type": "text" + } + ], + "updated": 1771600797567, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_coll_art", + "x": 710, + "y": 580, + "width": 0, + "height": 120, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 120 + ] + ], + "strokeColor": "#22c55e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "collection", + "fixedPoint": [ + 0.5, + 1 + ] + }, + "endBinding": { + "elementId": "artifact", + "fixedPoint": [ + 0.5, + 0 + ] + }, + "startArrowhead": null + }, + { + "type": "text", + "id": "a_coll_art_text", + "x": 679.2, + "y": 631.25, + "width": 61.60000000000001, + "height": 17.5, + "text": "contains", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_coll_art", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 107192323, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 817296938, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797567, + "link": null, + "locked": false, + "baseline": 13.5 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "currentItemStrokeColor": "#1e1e1e", + "currentItemBackgroundColor": "transparent", + "currentItemFillStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemStrokeStyle": "solid", + "currentItemRoughness": 1, + "currentItemOpacity": 100, + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemTextAlign": "left", + "currentItemStrokeSharpness": "round" + } +} \ No newline at end of file diff --git a/doc/tea_discovery_security.excalidraw b/doc/tea_discovery_security.excalidraw new file mode 100644 index 0000000..c45f23b --- /dev/null +++ b/doc/tea_discovery_security.excalidraw @@ -0,0 +1,1103 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "version": 3, + "versionNonce": 835041064, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 737075209, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "text", + "id": "t_disc_title", + "x": 300, + "y": 20, + "text": "TEA Discovery & Security Architecture", + "fontSize": 28, + "strokeColor": "#1e1e1e", + "width": 569.8000000000001, + "height": 35, + "baseline": 31, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "index": "a0", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": { + "type": 2 + }, + "containerId": null, + "originalText": "TEA Discovery & Security Architecture", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 149, + "versionNonce": 786606632, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 105464567, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "tei_struct_text", + "type": "text" + } + ], + "updated": 1771601605550, + "link": null, + "locked": false, + "type": "rectangle", + "id": "tei_struct", + "x": 49.9765625, + "y": 100, + "width": 450.0234375, + "height": 300.3515625, + "backgroundColor": "#fff3bf", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#f59e0b", + "index": "a1", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "tei_struct_text", + "x": 94.70040893554688, + "y": 230.17578125, + "width": 360.57574462890625, + "height": 40, + "text": "Transparency Exchange Identifier (TEI)\nurn:tei:::", + "fontSize": 16, + "fontFamily": 1, + "containerId": "tei_struct", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 150, + "versionNonce": 73082152, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 744852632, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601605550, + "link": null, + "locked": false, + "baseline": 36, + "index": "a2", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "Transparency Exchange Identifier (TEI)\nurn:tei:::", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 376, + "versionNonce": 896843096, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 819988572, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601611430, + "link": null, + "locked": false, + "type": "text", + "id": "t_types", + "x": 62.16796875, + "y": 139.89453125, + "text": "Supported Types: PURL, SWID, HASH,\nUUID, EAN/UPC, GTIN, ASIN, UDI", + "fontSize": 22.564062499999995, + "strokeColor": "#1e1e1e", + "width": 421.94796875000003, + "height": 56.41015625, + "baseline": 36, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "index": "a3", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": { + "type": 2 + }, + "containerId": null, + "originalText": "Supported Types: PURL, SWID, HASH,\nUUID, EAN/UPC, GTIN, ASIN, UDI", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 810552616, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 707153589, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "dns_res_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "rectangle", + "id": "dns_res", + "x": 650, + "y": 100, + "width": 200, + "height": 80, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#4a9eed", + "index": "a4", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "dns_res_text", + "x": 670.8, + "y": 120, + "width": 158.4, + "height": 40, + "text": "DNS Resolution\n(from domain-name)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "dns_res", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 1180744024, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 225575650, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 36, + "index": "a5", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "DNS Resolution\n(from domain-name)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 1478764584, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 793267654, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_tei_dns", + "x": 500, + "y": 140, + "width": 150, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 150, + 0 + ] + ], + "strokeColor": "#f59e0b", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "tei_struct", + "fixedPoint": [ + 1, + 0.5 + ], + "focus": 0 + }, + "endBinding": { + "elementId": "dns_res", + "fixedPoint": [ + 0, + 0.5 + ], + "focus": 0 + }, + "startArrowhead": null, + "lastCommittedPoint": null, + "index": "a6", + "fillStyle": "solid", + "backgroundColor": "transparent", + "roundness": { + "type": 2 + } + }, + { + "version": 3, + "versionNonce": 1236258392, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 277235211, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "well_known_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "rectangle", + "id": "well_known", + "x": 600, + "y": 300, + "width": 300, + "height": 100, + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#22c55e", + "index": "a7", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "well_known_text", + "x": 609.2, + "y": 330, + "width": 281.6, + "height": 40, + "text": "Discovery Endpoint\nhttps:///.well-known/tea", + "fontSize": 16, + "fontFamily": 1, + "containerId": "well_known", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 1153229608, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 130605028, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 36, + "index": "a8", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "Discovery Endpoint\nhttps:///.well-known/tea", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 475284312, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 966250622, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_dns_wk_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_dns_wk", + "x": 750, + "y": 180, + "width": 0, + "height": 120, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 120 + ] + ], + "strokeColor": "#4a9eed", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "dns_res", + "fixedPoint": [ + 0.5, + 1 + ], + "focus": 0 + }, + "endBinding": { + "elementId": "well_known", + "fixedPoint": [ + 0.5, + 0 + ], + "focus": 0 + }, + "startArrowhead": null, + "lastCommittedPoint": null, + "index": "a9", + "fillStyle": "solid", + "backgroundColor": "transparent", + "roundness": { + "type": 2 + } + }, + { + "type": "text", + "id": "a_dns_wk_text", + "x": 715.35, + "y": 231.25, + "width": 69.30000000000001, + "height": 17.5, + "text": "HTTPS GET", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_dns_wk", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 881212968, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 840751313, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 13.5, + "index": "aA", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "HTTPS GET", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 1822562392, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 916185838, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "json_resp_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "rectangle", + "id": "json_resp", + "x": 600, + "y": 500, + "width": 300, + "height": 100, + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#8b5cf6", + "index": "aB", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "json_resp_text", + "x": 626.8, + "y": 530, + "width": 246.40000000000003, + "height": 40, + "text": "TEA Server Index (JSON)\nList of endpoints & versions", + "fontSize": 16, + "fontFamily": 1, + "containerId": "json_resp", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 946616616, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 780073158, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 36, + "index": "aC", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "TEA Server Index (JSON)\nList of endpoints & versions", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 2121092440, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 223506811, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_wk_json_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_wk_json", + "x": 750, + "y": 400, + "width": 0, + "height": 100, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 100 + ] + ], + "strokeColor": "#22c55e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "well_known", + "fixedPoint": [ + 0.5, + 1 + ], + "focus": 0 + }, + "endBinding": { + "elementId": "json_resp", + "fixedPoint": [ + 0.5, + 0 + ], + "focus": 0 + }, + "startArrowhead": null, + "lastCommittedPoint": null, + "index": "aD", + "fillStyle": "solid", + "backgroundColor": "transparent", + "roundness": { + "type": 2 + } + }, + { + "type": "text", + "id": "a_wk_json_text", + "x": 669.15, + "y": 441.25, + "width": 161.70000000000002, + "height": 17.5, + "text": "Returns endpoint URLs", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_wk_json", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 1675285544, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 914463783, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 13.5, + "index": "aE", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "Returns endpoint URLs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 2064245336, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 70566273, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "client_logic_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "rectangle", + "id": "client_logic", + "x": 100, + "y": 500, + "width": 300, + "height": 100, + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#ef4444", + "index": "aF", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "client_logic_text", + "x": 104.79999999999998, + "y": 520, + "width": 290.40000000000003, + "height": 60, + "text": "Client Selection Logic\nMatches version, honors priority,\nretries on failure (5xx, cert)", + "fontSize": 16, + "fontFamily": 1, + "containerId": "client_logic", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 2092830504, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 121030079, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 56, + "index": "aG", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "Client Selection Logic\nMatches version, honors priority,\nretries on failure (5xx, cert)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 1095489368, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 827435008, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_json_cli", + "x": 600, + "y": 550, + "width": 200, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -200, + 0 + ] + ], + "strokeColor": "#8b5cf6", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "json_resp", + "fixedPoint": [ + 0, + 0.5 + ], + "focus": 0 + }, + "endBinding": { + "elementId": "client_logic", + "fixedPoint": [ + 1, + 0.5 + ], + "focus": 0 + }, + "startArrowhead": null, + "lastCommittedPoint": null, + "index": "aH", + "fillStyle": "solid", + "backgroundColor": "transparent", + "roundness": { + "type": 2 + } + }, + { + "version": 3, + "versionNonce": 729934376, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 612330223, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "full_api_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "rectangle", + "id": "full_api", + "x": 100, + "y": 700, + "width": 350, + "height": 80, + "backgroundColor": "#eebefa", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#ec4899", + "index": "aI", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "full_api_text", + "x": 90.19999999999999, + "y": 722.5, + "width": 369.6, + "height": 35, + "text": "Constructed API Call\n/v/discovery?tei=", + "fontSize": 14, + "fontFamily": 1, + "containerId": "full_api", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 898051160, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 897653891, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 31, + "index": "aJ", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "Constructed API Call\n/v/discovery?tei=", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 722625832, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 863461482, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_cli_api", + "x": 250, + "y": 600, + "width": 0, + "height": 100, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 100 + ] + ], + "strokeColor": "#ef4444", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "client_logic", + "fixedPoint": [ + 0.5, + 1 + ], + "focus": 0 + }, + "endBinding": { + "elementId": "full_api", + "fixedPoint": [ + 0.5, + 0 + ], + "focus": 0 + }, + "startArrowhead": null, + "lastCommittedPoint": null, + "index": "aK", + "fillStyle": "solid", + "backgroundColor": "transparent", + "roundness": { + "type": 2 + } + }, + { + "version": 3, + "versionNonce": 294642008, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 935183848, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "auth_layer_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "rectangle", + "id": "auth_layer", + "x": 600, + "y": 700, + "width": 250, + "height": 100, + "backgroundColor": "#c3fae8", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#15803d", + "index": "aL", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "auth_layer_text", + "x": 601.8, + "y": 723.75, + "width": 246.40000000000003, + "height": 52.5, + "text": "Security / Auth Layer\n- HTTP Bearer Token\n- Mutual TLS (mTLS) Client Certs", + "fontSize": 14, + "fontFamily": 1, + "containerId": "auth_layer", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 2027651112, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 672485143, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 48.5, + "index": "aM", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "Security / Auth Layer\n- HTTP Bearer Token\n- Mutual TLS (mTLS) Client Certs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "version": 3, + "versionNonce": 884602456, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 240663246, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "a_api_auth_text", + "type": "text" + } + ], + "updated": 1771601377884, + "link": null, + "locked": false, + "type": "arrow", + "id": "a_api_auth", + "x": 450, + "y": 740, + "width": 150, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 150, + 0 + ] + ], + "strokeColor": "#ec4899", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "full_api", + "fixedPoint": [ + 1, + 0.5 + ], + "focus": 0 + }, + "endBinding": { + "elementId": "auth_layer", + "fixedPoint": [ + 0, + 0.5 + ], + "focus": 0 + }, + "startArrowhead": null, + "lastCommittedPoint": null, + "index": "aN", + "fillStyle": "solid", + "backgroundColor": "transparent", + "roundness": { + "type": 2 + } + }, + { + "type": "text", + "id": "a_api_auth_text", + "x": 482.65, + "y": 731.25, + "width": 84.7, + "height": 17.5, + "text": "secured via", + "fontSize": 14, + "fontFamily": 1, + "containerId": "a_api_auth", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 3, + "versionNonce": 80296744, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 317220990, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771601377884, + "link": null, + "locked": false, + "baseline": 13.5, + "index": "aO", + "fillStyle": "solid", + "strokeWidth": 2, + "backgroundColor": "transparent", + "roundness": null, + "originalText": "secured via", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/doc/tea_publisher_flow.excalidraw b/doc/tea_publisher_flow.excalidraw new file mode 100644 index 0000000..b34cc6d --- /dev/null +++ b/doc/tea_publisher_flow.excalidraw @@ -0,0 +1,948 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "version": 2, + "versionNonce": 735189831, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 493599073, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "text", + "id": "t_seq2", + "x": 350, + "y": 1650, + "text": "TEA Publisher Flow", + "fontSize": 28, + "strokeColor": "#1e1e1e", + "width": 277.20000000000005, + "height": 35, + "baseline": 31, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "version": 2, + "versionNonce": 983350661, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 372839194, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pbHead_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "pbHead", + "x": 50, + "y": 1750, + "width": 120, + "height": 40, + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#ef4444", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "pbHead_text", + "x": 83.6, + "y": 1760, + "width": 52.800000000000004, + "height": 20, + "text": "Vendor", + "fontSize": 16, + "fontFamily": 1, + "containerId": "pbHead", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 891424686, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 519508987, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 16 + }, + { + "version": 2, + "versionNonce": 200057426, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 160900905, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pbLine", + "x": 110, + "y": 1790, + "width": 0, + "height": 350, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 350 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 890327808, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 961803583, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "ellipse", + "id": "pbh", + "x": 100, + "y": 1700, + "width": 20, + "height": 20, + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeColor": "#ef4444", + "strokeWidth": 2 + }, + { + "version": 2, + "versionNonce": 417291581, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 223982292, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "pbb", + "x": 100, + "y": 1722, + "width": 20, + "height": 20, + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#ef4444", + "strokeWidth": 2 + }, + { + "version": 2, + "versionNonce": 171505583, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 206442783, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "ppHead_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "ppHead", + "x": 300, + "y": 1750, + "width": 160, + "height": 40, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#4a9eed", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "ppHead_text", + "x": 337.65, + "y": 1761.25, + "width": 84.7, + "height": 17.5, + "text": "TEA Product", + "fontSize": 14, + "fontFamily": 1, + "containerId": "ppHead", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 111956121, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 540702299, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 608506924, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 995563976, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "ppLine", + "x": 380, + "y": 1790, + "width": 0, + "height": 350, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 350 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 359271078, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 459983326, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pcHead_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "pcHead", + "x": 550, + "y": 1750, + "width": 160, + "height": 40, + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#8b5cf6", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "pcHead_text", + "x": 579.95, + "y": 1761.25, + "width": 100.10000000000001, + "height": 17.5, + "text": "TEA Component", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pcHead", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 339317497, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 266302514, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 43997061, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 66353842, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pcLine", + "x": 630, + "y": 1790, + "width": 0, + "height": 350, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 350 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 10162484, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 467471766, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pcHead2_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "rectangle", + "id": "pcHead2", + "x": 800, + "y": 1750, + "width": 180, + "height": 40, + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "roundness": { + "type": 3 + }, + "strokeColor": "#22c55e", + "strokeWidth": 2 + }, + { + "type": "text", + "id": "pcHead2_text", + "x": 836.1, + "y": 1761.25, + "width": 107.80000000000001, + "height": 17.5, + "text": "TEA Collection", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pcHead2", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 34816800, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 367252007, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 746602882, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 803867599, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pcLine2", + "x": 890, + "y": 1790, + "width": 0, + "height": 350, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 350 + ] + ], + "strokeColor": "#b0b0b0", + "strokeWidth": 1, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "version": 2, + "versionNonce": 479137662, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 8343542, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pseq1_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pseq1", + "x": 110, + "y": 1840, + "width": 270, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 270, + 0 + ] + ], + "strokeColor": "#4a9eed", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "pseq1_text", + "x": 171.85, + "y": 1831.25, + "width": 146.3, + "height": 17.5, + "text": "1. POST /v1/product", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pseq1", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 919186660, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 985070238, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 285675331, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 878566305, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pseq2_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pseq2", + "x": 380, + "y": 1880, + "width": -270, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -270, + 0 + ] + ], + "strokeColor": "#4a9eed", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "pseq2_text", + "x": 114.1, + "y": 1871.25, + "width": 261.8, + "height": 17.5, + "text": "2. Returns Product Identifier (PI)", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pseq2", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 365632409, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 430366614, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 500453078, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 484489509, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pseq3_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pseq3", + "x": 110, + "y": 1930, + "width": 520, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 520, + 0 + ] + ], + "strokeColor": "#8b5cf6", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "pseq3_text", + "x": 177.49999999999997, + "y": 1921.25, + "width": 385.00000000000006, + "height": 17.5, + "text": "3. POST /v1/component (with PI, Component Version)", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pseq3", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 996674539, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 149908619, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 732777045, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 375918744, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pseq4_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pseq4", + "x": 630, + "y": 1970, + "width": -520, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -520, + 0 + ] + ], + "strokeColor": "#8b5cf6", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "pseq4_text", + "x": 281.45, + "y": 1961.25, + "width": 177.10000000000002, + "height": 17.5, + "text": "4. Returns Component ID", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pseq4", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 290342518, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 985361675, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 86711719, + "isDeleted": false, + "strokeStyle": "solid", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 585995016, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pseq5_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pseq5", + "x": 110, + "y": 2020, + "width": 780, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + 780, + 0 + ] + ], + "strokeColor": "#22c55e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "pseq5_text", + "x": 288.25, + "y": 2011.25, + "width": 423.50000000000006, + "height": 17.5, + "text": "5. POST /v1/collection (with Component ID and Artifact)", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pseq5", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 322237589, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 645475552, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + }, + { + "version": 2, + "versionNonce": 718048721, + "isDeleted": false, + "strokeStyle": "dashed", + "strokeSharpness": "round", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 935023425, + "groupIds": [], + "frameId": null, + "boundElements": [ + { + "id": "pseq6_text", + "type": "text" + } + ], + "updated": 1771600797571, + "link": null, + "locked": false, + "type": "arrow", + "id": "pseq6", + "x": 890, + "y": 2060, + "width": -780, + "height": 0, + "points": [ + [ + 0, + 0 + ], + [ + -780, + 0 + ] + ], + "strokeColor": "#22c55e", + "strokeWidth": 2, + "endArrowhead": "arrow", + "startArrowhead": null + }, + { + "type": "text", + "id": "pseq6_text", + "x": 407.6, + "y": 2051.25, + "width": 184.8, + "height": 17.5, + "text": "6. Returns Collection ID", + "fontSize": 14, + "fontFamily": 1, + "containerId": "pseq6", + "verticalAlign": "middle", + "textAlign": "center", + "strokeColor": "#1e1e1e", + "version": 2, + "versionNonce": 288204797, + "isDeleted": false, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "seed": 464034892, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1771600797571, + "link": null, + "locked": false, + "baseline": 13.5 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "currentItemStrokeColor": "#1e1e1e", + "currentItemBackgroundColor": "transparent", + "currentItemFillStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemStrokeStyle": "solid", + "currentItemRoughness": 1, + "currentItemOpacity": 100, + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemTextAlign": "left", + "currentItemStrokeSharpness": "round" + } +} \ No newline at end of file diff --git a/doc/versioning.md b/doc/versioning.md new file mode 100644 index 0000000..f91330c --- /dev/null +++ b/doc/versioning.md @@ -0,0 +1,146 @@ +# Versioning + +The Transparency Exchange API (TEA) uses semantic versioning for API evolution. This document describes the versioning strategy, compatibility guarantees, and migration guidelines. + +## Semantic Versioning + +TEA follows [Semantic Versioning 2.0.0](https://semver.org/) for API versions: + +``` +MAJOR.MINOR.PATCH +``` + +### Version Components + +- **MAJOR**: Breaking changes that require client updates +- **MINOR**: Backward-compatible additions (new endpoints, optional fields) +- **PATCH**: Backward-compatible bug fixes + +### Pre-release Versions + +Pre-release versions use the format: + +``` +MAJOR.MINOR.PATCH-PRERELEASE +``` + +Examples: + +- `1.0.0-alpha.1` +- `1.0.0-beta.2` +- `1.0.0-rc.1` + +## API Versioning Strategy + +### URL Versioning + +API versions are included in the URL path: + +``` +/v{MAJOR}/... +``` + +Current version: `v1` + +### Content Negotiation + +For content that may evolve independently of the API version, use content negotiation with the `Accept` header: + +``` +Accept: application/vnd.cyclonedx+json; version=1.5 +``` + +## Compatibility Guarantees + +### Backward Compatibility + +- **PATCH** versions: Fully backward compatible +- **MINOR** versions: Backward compatible additions only +- **MAJOR** versions: May include breaking changes + +### Forward Compatibility + +Clients SHOULD ignore unknown fields in responses. Servers MUST NOT require unknown fields in requests. + +### Deprecation Policy + +1. Features are marked as deprecated in MINOR releases +2. Deprecated features are removed in the next MAJOR release +3. Deprecation notices include: + - Deprecation version + - Removal version + - Migration guidance + +## Version Discovery + +### Well-Known Endpoint + +Clients discover available API versions through `/.well-known/tea`: + +```json +{ + "schemaVersion": 1, + "endpoints": [ + { + "url": "https://api.example.com/tea/v1", + "versions": ["1.0.0", "1.1.0"], + "priority": 1 + } + ] +} +``` + +### Version Headers + +Servers MAY include version information in responses: + +``` +X-API-Version: 1.0.0 +``` + +## Migration Guidelines + +### Minor Version Upgrades + +1. Review release notes for new features +2. Update client code to handle new optional fields +3. Test with new version in staging environment +4. Gradually roll out updated clients + +### Major Version Upgrades + +1. Review breaking changes documentation +2. Update client code for required changes +3. Implement feature flags if needed +4. Test extensively in staging +5. Plan rollback strategy +6. Execute blue-green deployment + +### Testing Strategy + +- Maintain test suites for multiple API versions during transition periods +- Use contract testing to validate compatibility +- Implement canary deployments for gradual rollout + +## Implementation Considerations + +### Server-Side + +- Support multiple concurrent API versions +- Use version-aware routing +- Implement graceful degradation for older clients +- Provide version-specific documentation + +### Client-Side + +- Implement version negotiation logic +- Handle version-specific response formats +- Provide upgrade prompts for deprecated versions +- Support fallback to older versions when possible + +## Version Support Policy + +- Current MAJOR version receives active development and support +- Previous MAJOR version receives security updates only +- Versions older than N-1 MAJOR releases are deprecated +- Deprecation notices provided 6 months before end of support diff --git a/graph-schema.json b/graph-schema.json new file mode 100644 index 0000000..ac32c88 --- /dev/null +++ b/graph-schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Dependency Graph Schema", + "description": "Minimal JSON schema for dependency graph with nodes and edges.", + "type": "object", + "properties": { + "nodes": { + "type": "array", + "description": "List of dependency nodes.", + "items": { + "type": "object", + "properties": { + "purl": { + "type": "string", + "description": "Package URL (PURL) identifier." + }, + "name": { + "type": "string", + "description": "Package name." + }, + "version": { + "type": "string", + "description": "Exact version." + }, + "digest": { + "type": "string", + "description": "Content digest (e.g., SHA256)." + }, + "license": { + "type": "string", + "description": "License SPDX identifier." + }, + "source_repo": { + "type": "string", + "description": "Source repository URL." + }, + "lifecycle_state": { + "enum": ["ACTIVE", "DEPRECATED", "QUARANTINED", "RETIRED"], + "description": "Lifecycle state for deprecation protocol." + } + }, + "required": ["purl", "name", "version", "digest", "lifecycle_state"] + } + }, + "edges": { + "type": "array", + "description": "List of dependency edges.", + "items": { + "type": "object", + "properties": { + "from_purl": { + "type": "string", + "description": "PURL of the dependent package." + }, + "to_purl": { + "type": "string", + "description": "PURL of the dependency." + }, + "scope": { + "enum": ["build", "runtime"], + "description": "Scope of the dependency." + }, + "reason": { + "enum": ["direct", "transitive"], + "description": "Direct or transitive dependency." + } + }, + "required": ["from_purl", "to_purl", "scope", "reason"] + } + } + }, + "required": ["nodes", "edges"] +} diff --git a/proto/tea/v1/artifact.proto b/proto/tea/v1/artifact.proto new file mode 100644 index 0000000..5028174 --- /dev/null +++ b/proto/tea/v1/artifact.proto @@ -0,0 +1,364 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; +import "buf/validate/validate.proto"; + +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// TEA Artifact +// ============================================================================ + +// Artifact represents a TEA Artifact - a security-related document or file +// linked to a component release, such as an SBOM, VEX, attestation, or license. +// +// TEA Artifacts are strictly IMMUTABLE: if the underlying document changes, +// a new TEA Artifact object must be created. URLs referenced in this object +// must always resolve to the same resource to ensure published checksums +// remain valid and verifiable. +// +// TEA Artifacts can be reused across multiple TEA Collections, allowing the +// same document to be referenced by different component or product releases. +message Artifact { + // Unique identifier for this TEA Artifact. + // Format: UUID v4 or v7. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // Human-readable name for the artifact. + string name = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; + + // Type of the artifact (SBOM, VEX, attestation, etc.). + ArtifactType type = 3 [(buf.validate.field).enum = {defined_only: true, not_in: [0]}]; + + // Distribution types this artifact applies to. + // If empty, the artifact applies to ALL distributions of the release. + // Values must match distributionType values from ComponentRelease.distributions. + repeated string component_distributions = 4 [json_name = "componentDistributions"]; + + // Available formats for this artifact. + // The same artifact content may be available in multiple formats + // (e.g., JSON and XML for CycloneDX). + repeated ArtifactFormat formats = 5 [(buf.validate.field).repeated.min_items = 1]; + + // Timestamp when this artifact was created in the TEA system. + google.protobuf.Timestamp created_date = 6 [json_name = "createdDate"]; + + // Optional description of the artifact. + string description = 7 [(buf.validate.field).string.max_len = 4096]; + + // Subject of the artifact (what it describes). + // For BOMs, this typically references the component/product. + ArtifactSubject subject = 8; + + // Optional deprecation information. + optional Deprecation deprecation = 9; +} + +// ============================================================================ +// Artifact Types +// ============================================================================ + +// ArtifactType classifies the type of transparency artifact. +enum ArtifactType { + // Unspecified artifact type - should not be used. + ARTIFACT_TYPE_UNSPECIFIED = 0; + + // Machine-readable statements containing facts, evidence, or testimony. + // Examples: in-toto attestations, SLSA provenance. + ARTIFACT_TYPE_ATTESTATION = 1; + + // Bill of Materials: SBOM, OBOM, HBOM, SaaSBOM, AI/ML-BOM, etc. + // Format-agnostic (CycloneDX, SPDX, etc.). + ARTIFACT_TYPE_BOM = 2; + + // Build-system specific metadata file. + // Examples: pom.xml, package.json, .nuspec, Cargo.toml. + ARTIFACT_TYPE_BUILD_META = 3; + + // Industry, regulatory, or other certification from an accredited + // certification body. + ARTIFACT_TYPE_CERTIFICATION = 4; + + // Describes how a component or service was manufactured or deployed. + // Includes build formulas, deployment manifests, IaC. + ARTIFACT_TYPE_FORMULATION = 5; + + // License file or license information. + ARTIFACT_TYPE_LICENSE = 6; + + // Release notes document. + ARTIFACT_TYPE_RELEASE_NOTES = 7; + + // A security.txt file (RFC 9116). + ARTIFACT_TYPE_SECURITY_TXT = 8; + + // A threat model document. + // Includes DFDs, attack trees, STRIDE analysis. + ARTIFACT_TYPE_THREAT_MODEL = 9; + + // Vulnerability information: VDR (Vulnerability Disclosure Report) + // or VEX (Vulnerability Exploitability eXchange). + ARTIFACT_TYPE_VULNERABILITIES = 10; + + // Common Lifecycle Enumeration document. + ARTIFACT_TYPE_CLE = 11; + + // CDXA - CycloneDX Attestations. + ARTIFACT_TYPE_CDXA = 12; + + // Cryptographic Bill of Materials (CBOM). + ARTIFACT_TYPE_CBOM = 13; + + // Model card for ML models. + ARTIFACT_TYPE_MODEL_CARD = 14; + + // Static analysis report (SARIF or proprietary). + ARTIFACT_TYPE_STATIC_ANALYSIS = 15; + + // Dynamic analysis report. + ARTIFACT_TYPE_DYNAMIC_ANALYSIS = 16; + + // Penetration test report. + ARTIFACT_TYPE_PENTEST_REPORT = 17; + + // Risk assessment document. + ARTIFACT_TYPE_RISK_ASSESSMENT = 18; + + // Plans of Action and Milestones (POAM). + ARTIFACT_TYPE_POAM = 19; + + // Quality metrics report. + ARTIFACT_TYPE_QUALITY_METRICS = 20; + + // Test harness or integration test suite. + ARTIFACT_TYPE_HARNESS = 21; + + // Conformance test report or compliance verification. + ARTIFACT_TYPE_CONFORMANCE = 22; + + // Other document type not covered above. + ARTIFACT_TYPE_OTHER = 99; +} + +// ============================================================================ +// Artifact Format +// ============================================================================ + +// ArtifactFormat represents a specific encoding/format of an artifact. +// The same artifact content may be available in multiple formats. +message ArtifactFormat { + // MIME type of the document. + // Examples: + // - application/vnd.cyclonedx+json + // - application/vnd.cyclonedx+xml + // - application/spdx+json + // - application/json + // - application/xml + // - text/plain + string mime_type = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 256, + json_name = "mimeType" + ]; + + // Human-readable description of this format. + // Example: "CycloneDX SBOM (XML format)" + string description = 2 [(buf.validate.field).string.max_len = 1024]; + + // Direct download URL for the artifact in this format. + // Must point to an immutable resource. + string url = 3 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).required = true + ]; + + // Optional URL for a detached digital signature of the artifact. + // Common formats: .asc (PGP), .sig (GPG), .p7s (PKCS#7). + string signature_url = 4 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "signatureUrl" + ]; + + // Checksums for integrity verification. + // At least SHA-256 is recommended. + repeated Checksum checksums = 5 [(buf.validate.field).repeated.min_items = 1]; + + // File size in bytes. + optional int64 size_bytes = 6 [json_name = "sizeBytes"]; + + // Encoding of the content (e.g., "utf-8", "base64"). + string encoding = 7; + + // Specification version (for typed artifacts). + // Example: "1.5" for CycloneDX 1.5, "2.3" for SPDX 2.3. + string spec_version = 8 [json_name = "specVersion"]; +} + +// ============================================================================ +// Artifact Subject +// ============================================================================ + +// ArtifactSubject describes what entity an artifact is about. +message ArtifactSubject { + // Type of subject. + SubjectType type = 1; + + // Identifiers for the subject. + repeated Identifier identifiers = 2; + + // Human-readable name of the subject. + string name = 3; + + // Version of the subject (if applicable). + string version = 4; +} + +// SubjectType classifies what an artifact describes. +enum SubjectType { + // Unspecified subject type. + SUBJECT_TYPE_UNSPECIFIED = 0; + + // The artifact describes a component. + SUBJECT_TYPE_COMPONENT = 1; + + // The artifact describes a product. + SUBJECT_TYPE_PRODUCT = 2; + + // The artifact describes a service. + SUBJECT_TYPE_SERVICE = 3; + + // The artifact describes an organization. + SUBJECT_TYPE_ORGANIZATION = 4; + + // The artifact describes a build/release process. + SUBJECT_TYPE_BUILD = 5; +} + +// ============================================================================ +// Artifact Content (for streaming downloads) +// ============================================================================ + +// ArtifactContentChunk is used for streaming artifact content. +message ArtifactContentChunk { + // Chunk of content data. + bytes data = 1; + + // Offset of this chunk in the full content. + int64 offset = 2; + + // Total size of the full content (in first chunk). + optional int64 total_size = 3 [json_name = "totalSize"]; + + // MIME type of the content (in first chunk). + string mime_type = 4 [json_name = "mimeType"]; + + // ETag for caching/conditional requests. + string etag = 5; +} + +// ============================================================================ +// Request/Response Messages +// ============================================================================ + +// GetArtifactRequest is the request for getting artifact metadata. +message GetArtifactRequest { + // UUID of the artifact to retrieve. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} + +// GetArtifactContentRequest is the request for downloading artifact content. +message GetArtifactContentRequest { + // UUID of the artifact. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Preferred format (MIME type). If not available, server chooses. + string preferred_format = 2 [json_name = "preferredFormat"]; + + // If-None-Match header for conditional requests. + // If the ETag matches, server returns NOT_MODIFIED. + string if_none_match = 3 [json_name = "ifNoneMatch"]; + + // Range request (for partial downloads). + // Format: "bytes=start-end" + string range = 4; +} + +// GetArtifactContentResponse is the response containing artifact content. +// For large artifacts, use streaming RPC instead. +message GetArtifactContentResponse { + // Artifact content. + bytes content = 1; + + // MIME type of the content. + string mime_type = 2 [json_name = "mimeType"]; + + // ETag for caching. + string etag = 3; + + // Content length. + int64 content_length = 4 [json_name = "contentLength"]; + + // Last modified timestamp. + google.protobuf.Timestamp last_modified = 5 [json_name = "lastModified"]; +} + +// ListArtifactsRequest lists artifacts with optional filters. +message ListArtifactsRequest { + // Pagination parameters. + PageRequest pagination = 1; + + // Filter by artifact type. + ArtifactType type = 2; + + // Filter by MIME type. + string mime_type = 3 [json_name = "mimeType"]; + + // Optional sort specification. + SortSpec sort = 4; +} + +// ListArtifactsResponse is the response for listing artifacts. +message ListArtifactsResponse { + // List of artifacts. + repeated Artifact artifacts = 1; + + // Pagination information. + PageResponse pagination = 2; +} + +// SearchArtifactsBySubjectRequest finds artifacts by their subject. +message SearchArtifactsBySubjectRequest { + // Subject identifier to search for. + Identifier identifier = 1 [(buf.validate.field).required = true]; + + // Filter by artifact type. + ArtifactType type = 2; + + // Pagination parameters. + PageRequest pagination = 3; +} + +// SearchArtifactsBySubjectResponse contains matching artifacts. +message SearchArtifactsBySubjectResponse { + // Matching artifacts. + repeated Artifact artifacts = 1; + + // Pagination information. + PageResponse pagination = 2; +} \ No newline at end of file diff --git a/proto/tea/v1/collection.proto b/proto/tea/v1/collection.proto new file mode 100644 index 0000000..0facc2a --- /dev/null +++ b/proto/tea/v1/collection.proto @@ -0,0 +1,316 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; +import "tea/v1/artifact.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// TEA Collection +// ============================================================================ + +// Collection represents a TEA Collection - a versioned list of artifacts for a +// specific ComponentRelease or ProductRelease. +// +// Collections are versioned to indicate changes (e.g., an updated VEX or +// corrected SBOM). When artifacts are updated, a new Collection version is +// created with the same UUID but incremented version number. +// +// The UUID of a Collection for a ComponentRelease is identical to the +// ComponentRelease UUID. For ProductReleases, it's the ProductRelease UUID. +message Collection { + // Unique identifier for this TEA Collection. + // For ComponentRelease collections: equals the ComponentRelease UUID. + // For ProductRelease collections: equals the ProductRelease UUID. + // Format: UUID v4 or v7. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // Collection version number. + // Incremented each time the collection content changes. + // Starts at 1 for new collections. + int32 version = 2 [ + (buf.validate.field).int32.gte = 1, + (buf.validate.field).required = true + ]; + + // Timestamp when this collection version was created. + google.protobuf.Timestamp date = 3; + + // Scope of this collection - indicates what the collection belongs to. + CollectionScope belongs_to = 4 [json_name = "belongsTo"]; + + // Reason for this collection version update. + UpdateReason update_reason = 5 [json_name = "updateReason"]; + + // List of TEA Artifacts in this collection. + repeated Artifact artifacts = 6; + + // Digital signature of the collection (if signed). + optional CollectionSignature signature = 7; + + // Timestamp when this collection was created in the TEA system. + google.protobuf.Timestamp created_date = 8 [json_name = "createdDate"]; + + // Optional deprecation information. + optional Deprecation deprecation = 9; + + // List of conformance vectors (compliance standards or frameworks) this collection complies with. + // Examples: "NIST SP 800-53", "OWASP Top 10", "ISO 27001", "PCI DSS", "GDPR". + // These indicate the security, privacy, or regulatory standards met by the artifacts in this collection. + repeated string conformance_vectors = 10 [json_name = "conformanceVectors"]; +} + +// ============================================================================ +// Collection Scope +// ============================================================================ + +// CollectionScope indicates what entity the collection belongs to. +enum CollectionScope { + // Unspecified scope. + COLLECTION_SCOPE_UNSPECIFIED = 0; + + // Collection belongs to a ComponentRelease. + COLLECTION_SCOPE_RELEASE = 1; + + // Collection belongs to a ProductRelease. + COLLECTION_SCOPE_PRODUCT_RELEASE = 2; +} + +// ============================================================================ +// Update Reason +// ============================================================================ + +// UpdateReasonType categorizes why a collection was updated. +enum UpdateReasonType { + // Unspecified update reason. + UPDATE_REASON_TYPE_UNSPECIFIED = 0; + + // Initial release of the collection. + UPDATE_REASON_TYPE_INITIAL_RELEASE = 1; + + // VEX (Vulnerability Exploitability eXchange) artifact was updated. + // Updates to VEX may produce different alerts in TEA clients. + UPDATE_REASON_TYPE_VEX_UPDATED = 2; + + // One or more artifacts (other than VEX) were updated. + UPDATE_REASON_TYPE_ARTIFACT_UPDATED = 3; + + // One or more artifacts were removed from the collection. + UPDATE_REASON_TYPE_ARTIFACT_REMOVED = 4; + + // One or more artifacts were added to the collection. + UPDATE_REASON_TYPE_ARTIFACT_ADDED = 5; + + // Correction to metadata (not artifact content). + UPDATE_REASON_TYPE_METADATA_CORRECTION = 6; + + // Security-related update requiring immediate attention. + UPDATE_REASON_TYPE_SECURITY_UPDATE = 7; +} + +// UpdateReason provides context for why a collection version was created. +message UpdateReason { + // Type of update. + UpdateReasonType type = 1 [(buf.validate.field).enum = {defined_only: true}]; + + // Human-readable description of the update. + string comment = 2 [(buf.validate.field).string.max_len = 4096]; + + // UUIDs of affected artifacts (for update/remove/add reasons). + repeated string affected_artifact_uuids = 3 [json_name = "affectedArtifactUuids"]; +} + +// ============================================================================ +// Collection Signature +// ============================================================================ + +// SignatureAlgorithm specifies the algorithm used for digital signatures. +enum SignatureAlgorithm { + // Unspecified algorithm. + SIGNATURE_ALGORITHM_UNSPECIFIED = 0; + + // RSA with SHA-256 (RSASSA-PKCS1-v1_5). + SIGNATURE_ALGORITHM_RS256 = 1; + + // RSA with SHA-384. + SIGNATURE_ALGORITHM_RS384 = 2; + + // RSA with SHA-512. + SIGNATURE_ALGORITHM_RS512 = 3; + + // ECDSA with P-256 and SHA-256. + SIGNATURE_ALGORITHM_ES256 = 4; + + // ECDSA with P-384 and SHA-384. + SIGNATURE_ALGORITHM_ES384 = 5; + + // ECDSA with P-521 and SHA-512. + SIGNATURE_ALGORITHM_ES512 = 6; + + // EdDSA with Ed25519. + SIGNATURE_ALGORITHM_EDDSA = 7; + + // RSA-PSS with SHA-256. + SIGNATURE_ALGORITHM_PS256 = 8; + + // RSA-PSS with SHA-384. + SIGNATURE_ALGORITHM_PS384 = 9; + + // RSA-PSS with SHA-512. + SIGNATURE_ALGORITHM_PS512 = 10; +} + +// CollectionSignature contains the digital signature of a collection. +message CollectionSignature { + // Signature algorithm used. + SignatureAlgorithm algorithm = 1; + + // Base64-encoded signature value. + string value = 2 [(buf.validate.field).string.min_len = 1]; + + // Timestamp when the signature was created. + google.protobuf.Timestamp signed_at = 3 [json_name = "signedAt"]; + + // Key identifier (for key lookup/verification). + string key_id = 4 [json_name = "keyId"]; + + // Optional X.509 certificate chain (PEM-encoded). + repeated string certificate_chain = 5 [json_name = "certificateChain"]; + + // Sigstore bundle (if Sigstore/cosign was used). + optional SigstoreBundle sigstore_bundle = 6 [json_name = "sigstoreBundle"]; +} + +// SigstoreBundle contains Sigstore-specific signature information. +message SigstoreBundle { + // Rekor log entry URL. + string rekor_log_url = 1 [json_name = "rekorLogUrl"]; + + // Rekor log entry ID. + string rekor_log_id = 2 [json_name = "rekorLogId"]; + + // Fulcio certificate (PEM-encoded). + string fulcio_certificate = 3 [json_name = "fulcioCertificate"]; + + // Timestamp authority response (RFC 3161). + bytes timestamp_authority_response = 4 [json_name = "timestampAuthorityResponse"]; +} + +// ============================================================================ +// Collection Version Info +// ============================================================================ + +// CollectionVersionInfo provides summary information about a collection version +// without including the full artifact list. +message CollectionVersionInfo { + // Collection UUID. + string uuid = 1; + + // Version number. + int32 version = 2; + + // Creation timestamp. + google.protobuf.Timestamp date = 3; + + // Update reason summary. + UpdateReason update_reason = 4 [json_name = "updateReason"]; + + // Number of artifacts in this version. + int32 artifact_count = 5 [json_name = "artifactCount"]; + + // Whether this version is signed. + bool is_signed = 6 [json_name = "isSigned"]; +} + +// ============================================================================ +// Request/Response Messages +// ============================================================================ + +// GetCollectionRequest is the request for getting the latest collection version. +message GetCollectionRequest { + // UUID of the collection to retrieve. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Include artifact content metadata in the response. + // If false, only artifact UUIDs are returned. + bool include_artifacts = 2 [json_name = "includeArtifacts"]; +} + +// ListCollectionVersionsRequest is the request for listing all versions of a collection. +message ListCollectionVersionsRequest { + // UUID of the collection. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Pagination parameters. + PageRequest pagination = 2; + + // Include full artifact details (default: false, summary only). + bool include_artifacts = 3 [json_name = "includeArtifacts"]; +} + +// ListCollectionVersionsResponse is the response for listing collection versions. +message ListCollectionVersionsResponse { + // Collection version summaries (or full collections if include_artifacts=true). + repeated CollectionVersionInfo versions = 1; + + // Full collection data (only populated if include_artifacts=true). + repeated Collection collections = 2; + + // Pagination information. + PageResponse pagination = 3; +} + +// GetCollectionVersionRequest is the request for getting a specific collection version. +message GetCollectionVersionRequest { + // UUID of the collection. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Version number to retrieve. + int32 version = 2 [(buf.validate.field).int32.gte = 1]; +} + +// CompareCollectionVersionsRequest compares two versions of a collection. +message CompareCollectionVersionsRequest { + // UUID of the collection. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // First version to compare (base). + int32 base_version = 2 [(buf.validate.field).int32.gte = 1, json_name = "baseVersion"]; + + // Second version to compare (target). + int32 target_version = 3 [(buf.validate.field).int32.gte = 1, json_name = "targetVersion"]; +} + +// CompareCollectionVersionsResponse shows differences between two collection versions. +message CompareCollectionVersionsResponse { + // UUID of the collection. + string uuid = 1; + + // Base version number. + int32 base_version = 2 [json_name = "baseVersion"]; + + // Target version number. + int32 target_version = 3 [json_name = "targetVersion"]; + + // Artifacts added in target version. + repeated string added_artifact_uuids = 4 [json_name = "addedArtifactUuids"]; + + // Artifacts removed in target version. + repeated string removed_artifact_uuids = 5 [json_name = "removedArtifactUuids"]; + + // Artifacts modified in target version (same UUID, different content). + repeated string modified_artifact_uuids = 6 [json_name = "modifiedArtifactUuids"]; +} \ No newline at end of file diff --git a/proto/tea/v1/common.proto b/proto/tea/v1/common.proto new file mode 100644 index 0000000..57e9cbe --- /dev/null +++ b/proto/tea/v1/common.proto @@ -0,0 +1,371 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// Identifier Types +// ============================================================================ + +// IdentifierType defines the type of identifier used to identify a product, +// component, or release in the transparency ecosystem. +enum IdentifierType { + // Unspecified identifier type - should not be used in valid data. + IDENTIFIER_TYPE_UNSPECIFIED = 0; + + // TEI - Transparency Exchange Identifier + // Format: urn:tei::: + // Example: urn:tei:uuid:cyclonedx.org:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + IDENTIFIER_TYPE_TEI = 1; + + // PURL - Package URL (https://github.com/package-url/purl-spec) + // Example: pkg:maven/org.apache.logging.log4j/log4j-core@2.24.3 + IDENTIFIER_TYPE_PURL = 2; + + // CPE - Common Platform Enumeration (https://nvd.nist.gov/products/cpe) + // Example: cpe:2.3:a:apache:log4j:2.24.3:*:*:*:*:*:*:* + IDENTIFIER_TYPE_CPE = 3; + + // SWID - Software Identification Tag (ISO/IEC 19770-2) + IDENTIFIER_TYPE_SWID = 4; + + // GAV - Maven Group:Artifact:Version coordinates + // Example: org.apache.logging.log4j:log4j-core:2.24.3 + IDENTIFIER_TYPE_GAV = 5; + + // GTIN - Global Trade Item Number (EAN/UPC barcodes) + // Example: 1234567890123 + IDENTIFIER_TYPE_GTIN = 6; + + // GMN - Global Model Number + IDENTIFIER_TYPE_GMN = 7; + + // UDI - Unique Device Identification (medical devices) + IDENTIFIER_TYPE_UDI = 8; + + // ASIN - Amazon Standard Identification Number + IDENTIFIER_TYPE_ASIN = 9; + + // HASH - Content hash identifier + // Format varies by hash algorithm + IDENTIFIER_TYPE_HASH = 10; +} + +// Identifier represents a typed identifier for a TEA entity. +// Identifiers are immutable and globally unique within their type namespace. +message Identifier { + // Type of the identifier. + IdentifierType id_type = 1 [ + (buf.validate.field).enum = {defined_only: true, not_in: [0]}, + json_name = "idType" + ]; + + // Value of the identifier in its canonical string form. + // Must conform to the format specification of the identifier type. + string id_value = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 2048, + json_name = "idValue" + ]; +} + +// ============================================================================ +// Checksum Types +// ============================================================================ + +// ChecksumAlgorithm defines the cryptographic hash algorithm used for checksums. +// Algorithms are ordered by security strength (weakest to strongest). +enum ChecksumAlgorithm { + // Unspecified algorithm - should not be used in valid data. + CHECKSUM_ALGORITHM_UNSPECIFIED = 0; + + // MD5 - 128-bit hash (DEPRECATED: cryptographically broken) + // Only for legacy compatibility, not recommended for new data. + CHECKSUM_ALGORITHM_MD5 = 1 [deprecated = true]; + + // SHA-1 - 160-bit hash (DEPRECATED: cryptographically weak) + // Only for legacy compatibility, not recommended for new data. + CHECKSUM_ALGORITHM_SHA1 = 2 [deprecated = true]; + + // SHA-256 - 256-bit hash (RECOMMENDED minimum) + CHECKSUM_ALGORITHM_SHA256 = 3; + + // SHA-384 - 384-bit hash + CHECKSUM_ALGORITHM_SHA384 = 4; + + // SHA-512 - 512-bit hash + CHECKSUM_ALGORITHM_SHA512 = 5; + + // SHA3-256 - 256-bit Keccak-based hash + CHECKSUM_ALGORITHM_SHA3_256 = 6; + + // SHA3-384 - 384-bit Keccak-based hash + CHECKSUM_ALGORITHM_SHA3_384 = 7; + + // SHA3-512 - 512-bit Keccak-based hash + CHECKSUM_ALGORITHM_SHA3_512 = 8; + + // BLAKE2b-256 - 256-bit BLAKE2b hash + CHECKSUM_ALGORITHM_BLAKE2B_256 = 9; + + // BLAKE2b-384 - 384-bit BLAKE2b hash + CHECKSUM_ALGORITHM_BLAKE2B_384 = 10; + + // BLAKE2b-512 - 512-bit BLAKE2b hash + CHECKSUM_ALGORITHM_BLAKE2B_512 = 11; + + // BLAKE3 - Variable-length BLAKE3 hash (default 256-bit) + CHECKSUM_ALGORITHM_BLAKE3 = 12; +} + +// Checksum represents a cryptographic hash of content for integrity verification. +message Checksum { + // Algorithm used to compute the checksum. + ChecksumAlgorithm alg_type = 1 [ + (buf.validate.field).enum = {defined_only: true, not_in: [0]}, + json_name = "algType" + ]; + + // Hexadecimal-encoded checksum value (lowercase). + string alg_value = 2 [ + (buf.validate.field).string.pattern = "^[a-f0-9]+$", + (buf.validate.field).string.min_len = 32, + (buf.validate.field).string.max_len = 256, + json_name = "algValue" + ]; +} + +// ============================================================================ +// Pagination +// ============================================================================ + +// PageRequest specifies pagination parameters for list operations. +message PageRequest { + // Maximum number of items to return per page. + // Server may return fewer items. Default is 20, maximum is 100. + int32 page_size = 1 [ + (buf.validate.field).int32 = {gte: 1, lte: 100}, + json_name = "pageSize" + ]; + + // Opaque token for fetching the next page of results. + // Obtained from PageResponse.next_page_token of a previous request. + // Omit for the first page. + string page_token = 2 [json_name = "pageToken"]; +} + +// PageResponse contains pagination metadata in list responses. +message PageResponse { + // Token to retrieve the next page of results. + // Empty if there are no more results. + string next_page_token = 1 [json_name = "nextPageToken"]; + + // Total number of items across all pages (if known). + // May be omitted if the total is expensive to compute. + optional int64 total_count = 2 [json_name = "totalCount"]; +} + +// ============================================================================ +// Error Types +// ============================================================================ + +// ErrorCode defines standard error codes for TEA API responses. +// These map to HTTP status codes where applicable. +enum ErrorCode { + // Unspecified error - should not be used. + ERROR_CODE_UNSPECIFIED = 0; + + // The request was invalid or malformed (HTTP 400). + ERROR_CODE_INVALID_ARGUMENT = 1; + + // Authentication credentials were missing or invalid (HTTP 401). + ERROR_CODE_UNAUTHENTICATED = 2; + + // The caller does not have permission for this operation (HTTP 403). + ERROR_CODE_PERMISSION_DENIED = 3; + + // The requested resource was not found (HTTP 404). + ERROR_CODE_NOT_FOUND = 4; + + // The resource already exists (HTTP 409). + ERROR_CODE_ALREADY_EXISTS = 5; + + // The operation was rejected due to resource exhaustion (HTTP 429). + ERROR_CODE_RESOURCE_EXHAUSTED = 6; + + // The operation was cancelled (HTTP 499). + ERROR_CODE_CANCELLED = 7; + + // An internal server error occurred (HTTP 500). + ERROR_CODE_INTERNAL = 8; + + // The service is temporarily unavailable (HTTP 503). + ERROR_CODE_UNAVAILABLE = 9; +} + +// ErrorDetail provides structured error information. +message ErrorDetail { + // Machine-readable error code. + ErrorCode code = 1; + + // Human-readable error message. + string message = 2; + + // Field that caused the error (for validation errors). + string field = 3; + + // Additional error context as key-value pairs. + map metadata = 4; +} + +// ErrorResponse is the standard error response format for TEA APIs. +message ErrorResponse { + // Primary error code. + ErrorCode code = 1; + + // Human-readable error message. + string message = 2; + + // Detailed error information. + repeated ErrorDetail details = 3; + + // Unique request ID for tracing. + string request_id = 4 [json_name = "requestId"]; + + // Optional documentation URL for this error type. + string documentation_url = 5 [json_name = "documentationUrl"]; +} + +// ============================================================================ +// Timestamp Utilities +// ============================================================================ + +// DateRange represents a range of dates for filtering. +message DateRange { + // Start of the date range (inclusive). + google.protobuf.Timestamp start = 1; + + // End of the date range (exclusive). + google.protobuf.Timestamp end = 2; +} + +// ============================================================================ +// Sorting +// ============================================================================ + +// SortOrder specifies the direction of sorting. +enum SortOrder { + // Unspecified sort order - server default (usually descending by date). + SORT_ORDER_UNSPECIFIED = 0; + + // Ascending order (oldest first, A-Z). + SORT_ORDER_ASC = 1; + + // Descending order (newest first, Z-A). + SORT_ORDER_DESC = 2; +} + +// SortField specifies which field to sort by. +enum SortField { + // Unspecified sort field - server default. + SORT_FIELD_UNSPECIFIED = 0; + + // Sort by creation date. + SORT_FIELD_CREATED_DATE = 1; + + // Sort by release date. + SORT_FIELD_RELEASE_DATE = 2; + + // Sort by version (semantic versioning order). + SORT_FIELD_VERSION = 3; + + // Sort by name (alphabetical). + SORT_FIELD_NAME = 4; +} + +// SortSpec specifies sorting parameters. +message SortSpec { + // Field to sort by. + SortField field = 1; + + // Sort direction. + SortOrder order = 2; +} + +// ============================================================================ +// Health Check (following gRPC health checking protocol) +// ============================================================================ + +// HealthStatus represents the health state of a service. +enum HealthStatus { + // Unknown health status. + HEALTH_STATUS_UNSPECIFIED = 0; + + // Service is healthy and serving requests. + HEALTH_STATUS_SERVING = 1; + + // Service is unhealthy and not serving requests. + HEALTH_STATUS_NOT_SERVING = 2; + + // Service exists but health status unknown. + HEALTH_STATUS_UNKNOWN = 3; +} + +// HealthCheckRequest is used to query service health. +message HealthCheckRequest { + // Service name to check. Empty string checks overall server health. + string service = 1; +} + +// HealthCheckResponse contains the health status. +message HealthCheckResponse { + // Current health status. + HealthStatus status = 1; + + // Optional detailed health information. + map details = 2; +} + +// ============================================================================ +// Deprecation Support +// ============================================================================ + +// DeprecationState represents the lifecycle state of an entity. +enum DeprecationState { + // Unspecified deprecation state. + DEPRECATION_STATE_UNSPECIFIED = 0; + + // Entity is active and supported. + DEPRECATION_STATE_ACTIVE = 1; + + // Entity is deprecated but still operational. + DEPRECATION_STATE_DEPRECATED = 2; + + // Entity is in sunset period and will be removed. + DEPRECATION_STATE_SUNSET = 3; +} + +// Deprecation contains deprecation information for an entity. +message Deprecation { + // Current deprecation state. + DeprecationState state = 1; + + // Human-readable deprecation notice. + string notice = 2 [(buf.validate.field).string.max_len = 2048]; + + // Sunset date when the entity will be removed (if known). + optional google.protobuf.Timestamp sunset_date = 3 [json_name = "sunsetDate"]; + + // UUID of the successor entity (if applicable). + optional string successor_uuid = 4 [(buf.validate.field).string.uuid = true, json_name = "successorUuid"]; +} diff --git a/proto/tea/v1/component.proto b/proto/tea/v1/component.proto new file mode 100644 index 0000000..61c053f --- /dev/null +++ b/proto/tea/v1/component.proto @@ -0,0 +1,334 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// TEA Component +// ============================================================================ + +// Component represents a TEA Component - a component lineage. +// A product release may be constructed with one or multiple TEA Components, +// each with their own set of releases and related artifacts. +// +// Example: "Apache Log4j Core" is a Component with multiple ComponentReleases +// like 2.24.3, 2.24.2, etc. +message Component { + // Unique identifier for this TEA Component. + // Format: UUID v4 or v7. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // Human-readable name of the component. + string name = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; + + // Optional description of the component. + string description = 3 [(buf.validate.field).string.max_len = 4096]; + + // List of identifiers for this component. + // A component may have multiple identifiers (e.g., both CPE and PURL). + repeated Identifier identifiers = 4; + + // Timestamp when this component was created in the TEA system. + google.protobuf.Timestamp created_date = 5 [json_name = "createdDate"]; + + // Timestamp when this component was last modified. + google.protobuf.Timestamp modified_date = 6 [json_name = "modifiedDate"]; + + // Component type classification. + ComponentType component_type = 7 [json_name = "componentType"]; + + // Optional license information for the component. + repeated LicenseInfo licenses = 8; + + // Optional publisher/author information. + string publisher = 9; + + // Optional URL to the component's homepage. + string homepage_url = 10 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "homepageUrl" + ]; + + // Optional URL to the component's VCS repository. + string vcs_url = 11 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "vcsUrl" + ]; + + // Optional deprecation information. + optional Deprecation deprecation = 12; +} + +// ComponentType classifies the type of component. +enum ComponentType { + // Unspecified component type. + COMPONENT_TYPE_UNSPECIFIED = 0; + + // A software application. + COMPONENT_TYPE_APPLICATION = 1; + + // A software framework. + COMPONENT_TYPE_FRAMEWORK = 2; + + // A software library. + COMPONENT_TYPE_LIBRARY = 3; + + // An operating system. + COMPONENT_TYPE_OPERATING_SYSTEM = 4; + + // A physical or virtual device. + COMPONENT_TYPE_DEVICE = 5; + + // A firmware image. + COMPONENT_TYPE_FIRMWARE = 6; + + // A software file (not a library/application). + COMPONENT_TYPE_FILE = 7; + + // A container image (Docker, OCI). + COMPONENT_TYPE_CONTAINER = 8; + + // A platform (cloud service, runtime). + COMPONENT_TYPE_PLATFORM = 9; + + // A machine learning model. + COMPONENT_TYPE_MACHINE_LEARNING_MODEL = 10; + + // A dataset used for ML or other purposes. + COMPONENT_TYPE_DATA = 11; + + // Cryptographic asset (key, certificate). + COMPONENT_TYPE_CRYPTOGRAPHIC_ASSET = 12; +} + +// LicenseInfo contains license information for a component. +message LicenseInfo { + // SPDX license identifier (e.g., "Apache-2.0", "MIT"). + string spdx_id = 1 [json_name = "spdxId"]; + + // License name (if not SPDX-recognized). + string name = 2; + + // URL to the license text. + string url = 3 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; +} + +// ============================================================================ +// TEA Component Release +// ============================================================================ + +// ComponentRelease represents a specific version of a TEA Component. +// Each release may include multiple distributions and has an associated +// TEA Collection containing security artifacts. +// +// The UUID of a ComponentRelease is identical to the UUID of its associated +// TEA Collection. +message ComponentRelease { + // Unique identifier for this component release. + // This is also the UUID of the associated TEA Collection. + // Format: UUID v4 or v7. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // UUID of the TEA Component this release belongs to. + string component = 2 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // Human-readable version string. + // Can follow any versioning scheme (semver, calver, etc.). + string version = 3 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 256 + ]; + + // Timestamp when this release was created in the TEA system. + google.protobuf.Timestamp created_date = 4 [json_name = "createdDate"]; + + // Upstream release timestamp. + // This is the actual release date, not when it was added to TEA. + google.protobuf.Timestamp release_date = 5 [json_name = "releaseDate"]; + + // Indicates if this is a pre-release (beta, RC, milestone, etc.). + // This flag can be disabled (set to false) after creation, but not enabled. + bool pre_release = 6 [json_name = "preRelease"]; + + // List of identifiers for this release. + repeated Identifier identifiers = 7; + + // List of distributions for this release. + // Distributions capture variations such as architecture, packaging, or + // localization of the same release. + repeated Distribution distributions = 8; + + // Optional deprecation information. + optional Deprecation deprecation = 9; +} + +// ============================================================================ +// Distribution +// ============================================================================ + +// Distribution represents a specific variant of a ComponentRelease. +// For software, each distribution typically corresponds to a different +// digital file delivered to users (e.g., by platform or packaging type). +// For hardware, distributions may reflect differences in packaging, language, +// or other physical attributes. +message Distribution { + // Unique identifier for this distribution type. + // Defined by the producer and used to associate TEA Artifacts. + // Examples: "jar", "tar.gz", "windows-x64.zip", "windows-x64.exe" + string distribution_type = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 128, + (buf.validate.field).string.pattern = "^[a-zA-Z0-9][a-zA-Z0-9._-]*$", + json_name = "distributionType" + ]; + + // Human-readable description of the distribution. + string description = 2 [(buf.validate.field).string.max_len = 1024]; + + // Identifiers specific to this distribution. + // Example: PURL with type qualifier for specific packaging. + repeated Identifier identifiers = 3; + + // Direct download URL for the distribution. + string url = 4 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; + + // Direct download URL for the distribution's external signature. + string signature_url = 5 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "signatureUrl" + ]; + + // Checksums for the distribution file. + // At least SHA-256 is recommended. + repeated Checksum checksums = 6; + + // File size in bytes (if known). + optional int64 size_bytes = 7 [json_name = "sizeBytes"]; + + // MIME type of the distribution file. + string mime_type = 8 [json_name = "mimeType"]; +} + +// ============================================================================ +// Request/Response Messages +// ============================================================================ + +// ListComponentsRequest is the request for listing components. +message ListComponentsRequest { + // Pagination parameters. + PageRequest pagination = 1; + + // Optional search query (matches name, description). + string query = 2 [(buf.validate.field).string.max_len = 256]; + + // Filter by component type. + ComponentType component_type = 3 [json_name = "componentType"]; + + // Filter by identifier. + optional Identifier identifier = 4; + + // Optional sort specification. + SortSpec sort = 5; +} + +// ListComponentsResponse is the response for listing components. +message ListComponentsResponse { + // List of components. + repeated Component components = 1; + + // Pagination information. + PageResponse pagination = 2; +} + +// GetComponentRequest is the request for getting a single component. +message GetComponentRequest { + // UUID of the component to retrieve. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} + +// ListComponentReleasesRequest is the request for listing releases of a component. +message ListComponentReleasesRequest { + // UUID of the component. + string component_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "componentUuid" + ]; + + // Pagination parameters. + PageRequest pagination = 2; + + // Include pre-releases in the response. + bool include_pre_releases = 3 [json_name = "includePreReleases"]; + + // Filter by release date range. + DateRange release_date_range = 4 [json_name = "releaseDateRange"]; + + // Optional sort specification. + SortSpec sort = 5; +} + +// ListComponentReleasesResponse is the response for listing component releases. +message ListComponentReleasesResponse { + // List of component releases. + repeated ComponentRelease releases = 1; + + // Pagination information. + PageResponse pagination = 2; +} + +// GetComponentReleaseRequest is the request for getting a single component release. +message GetComponentReleaseRequest { + // UUID of the component release to retrieve. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} + +// SearchByChecksumRequest finds releases by distribution checksum. +message SearchByChecksumRequest { + // Checksum to search for. + Checksum checksum = 1 [(buf.validate.field).required = true]; + + // Pagination parameters. + PageRequest pagination = 2; +} + +// SearchByChecksumResponse contains matching releases. +message SearchByChecksumResponse { + // Matching component releases. + repeated ComponentRelease releases = 1; + + // Pagination information. + PageResponse pagination = 2; +} \ No newline at end of file diff --git a/proto/tea/v1/consumer.proto b/proto/tea/v1/consumer.proto new file mode 100644 index 0000000..1178f82 --- /dev/null +++ b/proto/tea/v1/consumer.proto @@ -0,0 +1,386 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; +import "tea/v1/product.proto"; +import "tea/v1/component.proto"; +import "tea/v1/collection.proto"; +import "tea/v1/artifact.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// Consumer Service +// ============================================================================ + +// ConsumerService provides read-only access to TEA transparency artifacts. +// This is the primary API for TEA consumers (customers, auditors, automated +// systems) to discover and retrieve artifacts. +// +// All endpoints support optional authentication. Some endpoints may require +// authentication depending on the server's authorization policy. +// +// This service implements the TEA Consumer API specification. +service ConsumerService { + // ========================================================================== + // Product Operations + // ========================================================================== + + // ListProducts returns a paginated list of available products. + // Products are optional higher-level groupings of product releases. + rpc ListProducts(ListProductsRequest) returns (ListProductsResponse) { + option (google.api.http) = { + get: "/v1/products" + }; + } + + // GetProduct retrieves a single product by UUID. + rpc GetProduct(GetProductRequest) returns (Product) { + option (google.api.http) = { + get: "/v1/products/{uuid}" + }; + } + + // ListProductReleases returns releases for a specific product. + rpc ListProductReleases(ListProductReleasesRequest) returns (ListProductReleasesResponse) { + option (google.api.http) = { + get: "/v1/products/{product_uuid}/releases" + }; + } + + // GetProductRelease retrieves a single product release by UUID. + rpc GetProductRelease(GetProductReleaseRequest) returns (ProductRelease) { + option (google.api.http) = { + get: "/v1/product-releases/{uuid}" + }; + } + + // GetProductReleaseCollection retrieves the collection for a product release. + rpc GetProductReleaseCollection(GetProductReleaseCollectionRequest) returns (Collection) { + option (google.api.http) = { + get: "/v1/product-releases/{uuid}/collection" + }; + } + + // ========================================================================== + // Component Operations + // ========================================================================== + + // ListComponents returns a paginated list of available components. + rpc ListComponents(ListComponentsRequest) returns (ListComponentsResponse) { + option (google.api.http) = { + get: "/v1/components" + }; + } + + // GetComponent retrieves a single component by UUID. + rpc GetComponent(GetComponentRequest) returns (Component) { + option (google.api.http) = { + get: "/v1/components/{uuid}" + }; + } + + // ListComponentReleases returns releases for a specific component. + rpc ListComponentReleases(ListComponentReleasesRequest) returns (ListComponentReleasesResponse) { + option (google.api.http) = { + get: "/v1/components/{component_uuid}/releases" + }; + } + + // GetComponentRelease retrieves a single component release by UUID. + rpc GetComponentRelease(GetComponentReleaseRequest) returns (ComponentRelease) { + option (google.api.http) = { + get: "/v1/component-releases/{uuid}" + }; + } + + // GetComponentReleaseCollection retrieves the collection for a component release. + rpc GetComponentReleaseCollection(GetComponentReleaseCollectionRequest) returns (Collection) { + option (google.api.http) = { + get: "/v1/component-releases/{uuid}/collection" + }; + } + + // ========================================================================== + // Collection Operations + // ========================================================================== + + // GetCollection retrieves the latest version of a collection. + rpc GetCollection(GetCollectionRequest) returns (Collection) { + option (google.api.http) = { + get: "/v1/collections/{uuid}" + }; + } + + // ListCollectionVersions returns all versions of a collection. + rpc ListCollectionVersions(ListCollectionVersionsRequest) returns (ListCollectionVersionsResponse) { + option (google.api.http) = { + get: "/v1/collections/{uuid}/versions" + }; + } + + // GetCollectionVersion retrieves a specific version of a collection. + rpc GetCollectionVersion(GetCollectionVersionRequest) returns (Collection) { + option (google.api.http) = { + get: "/v1/collections/{uuid}/versions/{version}" + }; + } + + // CompareCollectionVersions shows differences between two versions. + rpc CompareCollectionVersions(CompareCollectionVersionsRequest) returns (CompareCollectionVersionsResponse) { + option (google.api.http) = { + get: "/v1/collections/{uuid}/compare" + }; + } + + // ========================================================================== + // Artifact Operations + // ========================================================================== + + // GetArtifact retrieves artifact metadata by UUID. + rpc GetArtifact(GetArtifactRequest) returns (Artifact) { + option (google.api.http) = { + get: "/v1/artifacts/{uuid}" + }; + } + + // GetArtifactContent downloads artifact content. + // Supports conditional requests (If-None-Match) and range requests. + // For large artifacts, consider using the streaming variant. + rpc GetArtifactContent(GetArtifactContentRequest) returns (GetArtifactContentResponse) { + option (google.api.http) = { + get: "/v1/artifacts/{uuid}/content" + }; + } + + // StreamArtifactContent streams artifact content in chunks. + // Recommended for large artifacts. + rpc StreamArtifactContent(GetArtifactContentRequest) returns (stream ArtifactContentChunk) { + option (google.api.http) = { + get: "/v1/artifacts/{uuid}/stream" + }; + } + + // HeadArtifactContent checks artifact status without downloading. + // Returns ETag and Last-Modified for conditional requests. + rpc HeadArtifactContent(HeadArtifactContentRequest) returns (HeadArtifactContentResponse) { + option (google.api.http) = { + // Note: gRPC-Gateway will convert this to HEAD + get: "/v1/artifacts/{uuid}/content/head" + }; + } + + // ========================================================================== + // Search Operations + // ========================================================================== + + // SearchByIdentifier finds entities matching an identifier. + rpc SearchByIdentifier(SearchByIdentifierRequest) returns (SearchByIdentifierResponse) { + option (google.api.http) = { + get: "/v1/search/identifier" + }; + } + + // SearchByChecksum finds releases by distribution checksum. + rpc SearchByChecksum(SearchByChecksumRequest) returns (SearchByChecksumResponse) { + option (google.api.http) = { + get: "/v1/search/checksum" + }; + } + + // SearchArtifacts finds artifacts matching criteria. + rpc SearchArtifacts(SearchArtifactsRequest) returns (SearchArtifactsResponse) { + option (google.api.http) = { + get: "/v1/search/artifacts" + }; + } +} + +// ============================================================================ +// Additional Request/Response Messages +// ============================================================================ + +// GetProductReleaseCollectionRequest is the request for getting a product release's collection. +message GetProductReleaseCollectionRequest { + // UUID of the product release. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Collection version to retrieve (latest if omitted). + optional int32 version = 2; +} + +// GetComponentReleaseCollectionRequest is the request for getting a component release's collection. +message GetComponentReleaseCollectionRequest { + // UUID of the component release. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Collection version to retrieve (latest if omitted). + optional int32 version = 2; +} + +// HeadArtifactContentRequest checks artifact without downloading. +message HeadArtifactContentRequest { + // UUID of the artifact. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Preferred format (MIME type). + string preferred_format = 2 [json_name = "preferredFormat"]; +} + +// HeadArtifactContentResponse contains artifact metadata without content. +message HeadArtifactContentResponse { + // ETag for conditional requests. + string etag = 1; + + // Last modification timestamp. + google.protobuf.Timestamp last_modified = 2 [json_name = "lastModified"]; + + // Content length in bytes. + int64 content_length = 3 [json_name = "contentLength"]; + + // Content MIME type. + string content_type = 4 [json_name = "contentType"]; + + // Whether the artifact accepts range requests. + bool accept_ranges = 5 [json_name = "acceptRanges"]; + + // Checksums for the content. + repeated Checksum checksums = 6; +} + +// SearchByIdentifierRequest finds entities by identifier. +message SearchByIdentifierRequest { + // Identifier to search for. + Identifier identifier = 1 [(buf.validate.field).required = true]; + + // Types of entities to search (empty = all). + repeated EntityType entity_types = 2 [json_name = "entityTypes"]; + + // Pagination parameters. + PageRequest pagination = 3; +} + +// EntityType specifies what kind of entity to search for. +enum EntityType { + // Unspecified entity type. + ENTITY_TYPE_UNSPECIFIED = 0; + + // Search for products. + ENTITY_TYPE_PRODUCT = 1; + + // Search for product releases. + ENTITY_TYPE_PRODUCT_RELEASE = 2; + + // Search for components. + ENTITY_TYPE_COMPONENT = 3; + + // Search for component releases. + ENTITY_TYPE_COMPONENT_RELEASE = 4; + + // Search for artifacts. + ENTITY_TYPE_ARTIFACT = 5; +} + +// SearchByIdentifierResponse contains matching entities. +message SearchByIdentifierResponse { + // Matching products. + repeated Product products = 1; + + // Matching product releases. + repeated ProductRelease product_releases = 2 [json_name = "productReleases"]; + + // Matching components. + repeated Component components = 3; + + // Matching component releases. + repeated ComponentRelease component_releases = 4 [json_name = "componentReleases"]; + + // Pagination information. + PageResponse pagination = 5; +} + +// SearchArtifactsRequest searches for artifacts. +message SearchArtifactsRequest { + // Filter by artifact type. + ArtifactType artifact_type = 1 [json_name = "artifactType"]; + + // Filter by MIME type. + string mime_type = 2 [json_name = "mimeType"]; + + // Filter by subject identifier. + optional Identifier subject_identifier = 3 [json_name = "subjectIdentifier"]; + + // Filter by creation date range. + DateRange created_date_range = 4 [json_name = "createdDateRange"]; + + // Text search query. + string query = 5; + + // Pagination parameters. + PageRequest pagination = 6; + + // Sort specification. + SortSpec sort = 7; +} + +// SearchArtifactsResponse contains matching artifacts. +message SearchArtifactsResponse { + // Matching artifacts. + repeated Artifact artifacts = 1; + + // Pagination information. + PageResponse pagination = 2; +} + +// ============================================================================ +// Batch Operations +// ============================================================================ + +// BatchGetArtifactsRequest retrieves multiple artifacts at once. +message BatchGetArtifactsRequest { + // UUIDs of artifacts to retrieve. + repeated string uuids = 1 [ + (buf.validate.field).repeated.min_items = 1, + (buf.validate.field).repeated.max_items = 100 + ]; +} + +// BatchGetArtifactsResponse contains multiple artifacts. +message BatchGetArtifactsResponse { + // Retrieved artifacts (in request order). + repeated Artifact artifacts = 1; + + // UUIDs that were not found. + repeated string not_found_uuids = 2 [json_name = "notFoundUuids"]; +} + +// BatchGetCollectionsRequest retrieves multiple collections at once. +message BatchGetCollectionsRequest { + // UUIDs of collections to retrieve. + repeated string uuids = 1 [ + (buf.validate.field).repeated.min_items = 1, + (buf.validate.field).repeated.max_items = 50 + ]; + + // Include artifacts in response. + bool include_artifacts = 2 [json_name = "includeArtifacts"]; +} + +// BatchGetCollectionsResponse contains multiple collections. +message BatchGetCollectionsResponse { + // Retrieved collections. + repeated Collection collections = 1; + + // UUIDs that were not found. + repeated string not_found_uuids = 2 [json_name = "notFoundUuids"]; +} diff --git a/proto/tea/v1/discovery.proto b/proto/tea/v1/discovery.proto new file mode 100644 index 0000000..97d9c3f --- /dev/null +++ b/proto/tea/v1/discovery.proto @@ -0,0 +1,360 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// Discovery Service +// ============================================================================ + +// DiscoveryService provides TEI (Transparency Exchange Identifier) resolution +// and API endpoint discovery capabilities. +// +// The discovery flow: +// 1. Client has a TEI (e.g., from QR code, invoice, software about box) +// 2. Client extracts domain from TEI and fetches /.well-known/tea +// 3. Client connects to listed endpoint and calls Discover with the TEI +// 4. Server resolves TEI to a ProductRelease UUID +// 5. Client can then use Consumer API to fetch artifacts +service DiscoveryService { + // Discover resolves a TEI to a ProductRelease. + // This is the primary entry point for TEA clients. + // + // The TEI is URL-encoded in the query parameter. + // Example: /v1/discovery?tei=urn%3Atei%3Auuid%3Aexample.com%3A... + rpc Discover(DiscoverRequest) returns (DiscoverResponse) { + option (google.api.http) = { + get: "/v1/discovery" + }; + } + + // GetWellKnown returns the .well-known/tea discovery document. + // This is typically served as a static JSON file, but can be dynamic. + // + // Note: In production, this is served at /.well-known/tea not /v1. + // The gRPC method is provided for completeness and testing. + rpc GetWellKnown(GetWellKnownRequest) returns (WellKnownResponse) { + option (google.api.http) = { + get: "/.well-known/tea" + }; + } + + // Health check for the discovery service. + rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse) { + option (google.api.http) = { + get: "/v1/health" + }; + } + + // GetServerInfo returns metadata about the TEA server. + rpc GetServerInfo(GetServerInfoRequest) returns (ServerInfo) { + option (google.api.http) = { + get: "/v1/info" + }; + } +} + +// ============================================================================ +// TEI Types +// ============================================================================ + +// TeiType specifies the type component of a TEI URN. +enum TeiType { + // Unspecified TEI type. + TEI_TYPE_UNSPECIFIED = 0; + + // UUID-based TEI: urn:tei:uuid:: + TEI_TYPE_UUID = 1; + + // PURL-based TEI: urn:tei:purl:: + TEI_TYPE_PURL = 2; + + // SWID-based TEI: urn:tei:swid:: + TEI_TYPE_SWID = 3; + + // Hash-based TEI: urn:tei:hash::: + TEI_TYPE_HASH = 4; + + // EAN/UPC-based TEI: urn:tei:eanupc:: + TEI_TYPE_EANUPC = 5; + + // GTIN-based TEI: urn:tei:gtin:: + TEI_TYPE_GTIN = 6; + + // ASIN-based TEI: urn:tei:asin:: + TEI_TYPE_ASIN = 7; + + // UDI-based TEI: urn:tei:udi:: + TEI_TYPE_UDI = 8; +} + +// ParsedTei represents a parsed TEI URN structure. +message ParsedTei { + // Original TEI string. + string raw = 1; + + // TEI type component. + TeiType type = 2; + + // Domain name component (used for DNS resolution). + string domain = 3; + + // Unique identifier component. + string unique_id = 4 [json_name = "uniqueId"]; +} + +// ============================================================================ +// Well-Known Discovery +// ============================================================================ + +// GetWellKnownRequest is the request for the .well-known/tea endpoint. +message GetWellKnownRequest { + // No parameters needed. +} + +// WellKnownResponse is the response from /.well-known/tea. +// Conforms to the TEA Well-Known Schema. +message WellKnownResponse { + // Schema version. Currently always 1. + int32 schema_version = 1 [ + (buf.validate.field).int32.const = 1, + json_name = "schemaVersion" + ]; + + // List of available TEA endpoints. + repeated Endpoint endpoints = 2 [(buf.validate.field).repeated.min_items = 1]; +} + +// Endpoint describes a TEA API endpoint. +message Endpoint { + // Base URL of the TEA API endpoint (no trailing slash). + // Example: "https://api.teaexample.com" + string url = 1 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).required = true + ]; + + // Supported TEA API versions for this endpoint. + // Example: ["0.2.0-beta.2", "1.0.0"] + repeated string versions = 2 [(buf.validate.field).repeated.min_items = 1]; + + // Optional priority (0.0 to 1.0). Higher = preferred. + // Default is 1.0. + float priority = 3; +} + +// ============================================================================ +// Discovery Request/Response +// ============================================================================ + +// DiscoverRequest is the request to resolve a TEI. +message DiscoverRequest { + // The TEI to resolve (URL-encoded). + // Example: urn:tei:uuid:cyclonedx.org:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + string tei = 1 [ + (buf.validate.field).string.min_len = 10, + (buf.validate.field).string.max_len = 2048, + (buf.validate.field).required = true + ]; +} + +// DiscoverResponse contains the result of TEI resolution. +message DiscoverResponse { + // UUID of the resolved ProductRelease. + string product_release_uuid = 1 [ + (buf.validate.field).string.uuid = true, + json_name = "productReleaseUuid" + ]; + + // Base URL of this TEA server. + string server_url = 2 [json_name = "serverUrl"]; + + // Supported API versions on this server. + repeated string supported_versions = 3 [json_name = "supportedVersions"]; + + // The TEI that was resolved. + string tei = 4; + + // Parsed TEI components (for client convenience). + ParsedTei parsed_tei = 5 [json_name = "parsedTei"]; + + // All known TEIs for this ProductRelease. + // Helps clients avoid duplicate entries. + repeated string all_teis = 6 [json_name = "allTeis"]; + + // All known identifiers for this ProductRelease. + repeated Identifier identifiers = 7; + + // Basic ProductRelease metadata. + ProductReleaseMetadata product_release = 8 [json_name = "productRelease"]; + + // Whether authentication is required to access this release's artifacts. + bool authentication_required = 9 [json_name = "authenticationRequired"]; + + // Supported authentication methods (if authentication is required). + repeated AuthMethod auth_methods = 10 [json_name = "authMethods"]; +} + +// ProductReleaseMetadata contains basic info about a ProductRelease +// returned during discovery (before full API access). +message ProductReleaseMetadata { + // UUID of the ProductRelease. + string uuid = 1; + + // Version string. + string version = 2; + + // Product name (if available). + string product_name = 3 [json_name = "productName"]; + + // Vendor name (if available). + string vendor_name = 4 [json_name = "vendorName"]; + + // Release date. + google.protobuf.Timestamp release_date = 5 [json_name = "releaseDate"]; + + // Number of artifacts available. + int32 artifact_count = 6 [json_name = "artifactCount"]; +} + +// ============================================================================ +// Authentication Methods +// ============================================================================ + +// AuthMethod describes a supported authentication method. +enum AuthMethod { + // Unspecified authentication method. + AUTH_METHOD_UNSPECIFIED = 0; + + // HTTP Bearer token authentication. + AUTH_METHOD_BEARER_TOKEN = 1; + + // Mutual TLS with client certificates. + AUTH_METHOD_MTLS = 2; + + // API key authentication. + AUTH_METHOD_API_KEY = 3; + + // OAuth 2.0 authentication. + AUTH_METHOD_OAUTH2 = 4; + + // OpenID Connect authentication. + AUTH_METHOD_OIDC = 5; +} + +// ============================================================================ +// Server Information +// ============================================================================ + +// GetServerInfoRequest is the request for server information. +message GetServerInfoRequest { + // No parameters needed. +} + +// ServerInfo contains metadata about the TEA server. +message ServerInfo { + // Server name/identifier. + string name = 1; + + // Server version. + string version = 2; + + // TEA specification version(s) supported. + repeated string spec_versions = 3 [json_name = "specVersions"]; + + // Server description. + string description = 4; + + // Operator contact information. + string operator_contact = 5 [json_name = "operatorContact"]; + + // Terms of service URL. + string tos_url = 6 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "tosUrl" + ]; + + // Privacy policy URL. + string privacy_url = 7 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "privacyUrl" + ]; + + // Documentation URL. + string documentation_url = 8 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "documentationUrl" + ]; + + // Supported features/capabilities. + ServerCapabilities capabilities = 9; + + // Rate limiting information. + RateLimitInfo rate_limits = 10 [json_name = "rateLimits"]; +} + +// ServerCapabilities describes what features the server supports. +message ServerCapabilities { + // Consumer API is available. + bool consumer_api = 1 [json_name = "consumerApi"]; + + // Publisher API is available. + bool publisher_api = 2 [json_name = "publisherApi"]; + + // Insights/Query API is available. + bool insights_api = 3 [json_name = "insightsApi"]; + + // Collection signing is supported. + bool collection_signing = 4 [json_name = "collectionSigning"]; + + // Sigstore integration is available. + bool sigstore_integration = 5 [json_name = "sigstoreIntegration"]; + + // Search by checksum is supported. + bool checksum_search = 6 [json_name = "checksumSearch"]; + + // CEL query language is supported. + bool cel_queries = 7 [json_name = "celQueries"]; + + // Streaming downloads are supported. + bool streaming_downloads = 8 [json_name = "streamingDownloads"]; + + // Supported artifact types. + repeated ArtifactTypeSupport artifact_types = 9 [json_name = "artifactTypes"]; +} + +// ArtifactTypeSupport describes support for an artifact type. +message ArtifactTypeSupport { + // Artifact type. + string type = 1; + + // Supported formats (MIME types). + repeated string formats = 2; +} + +// RateLimitInfo provides rate limiting details. +message RateLimitInfo { + // Requests per minute for unauthenticated clients. + int32 unauthenticated_rpm = 1 [json_name = "unauthenticatedRpm"]; + + // Requests per minute for authenticated clients. + int32 authenticated_rpm = 2 [json_name = "authenticatedRpm"]; + + // Maximum download bandwidth (bytes per second) per client. + int64 max_bandwidth_bps = 3 [json_name = "maxBandwidthBps"]; +} diff --git a/proto/tea/v1/insights.proto b/proto/tea/v1/insights.proto new file mode 100644 index 0000000..160f387 --- /dev/null +++ b/proto/tea/v1/insights.proto @@ -0,0 +1,791 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; +import "tea/v1/artifact.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// Insights Service +// ============================================================================ + +// InsightsService provides query and analytics capabilities for TEA data. +// This enables "limited transparency" queries without requiring full artifact +// downloads and processing by the consumer. +// +// The service supports: +// - CEL (Common Expression Language) queries +// - Pre-built queries for common use cases +// - Vulnerability analysis +// - Component dependency analysis +// +// This service is an optional part of the TEA specification. +// Authentication is typically required for insights queries. +service InsightsService { + // ========================================================================== + // CEL Query Operations + // ========================================================================== + + // Query executes a CEL expression against TEA data. + // CEL provides a type-safe, sandboxed query language. + // See: https://github.com/google/cel-spec + rpc Query(QueryRequest) returns (QueryResponse) { + option (google.api.http) = { + post: "/v1/insights/query" + body: "*" + }; + } + + // ValidateQuery validates a CEL expression without executing it. + rpc ValidateQuery(ValidateQueryRequest) returns (ValidateQueryResponse) { + option (google.api.http) = { + post: "/v1/insights/query/validate" + body: "*" + }; + } + + // ========================================================================== + // Pre-built Queries + // ========================================================================== + + // GetVulnerabilitySummary returns vulnerability information for a release. + rpc GetVulnerabilitySummary(GetVulnerabilitySummaryRequest) returns (VulnerabilitySummary) { + option (google.api.http) = { + get: "/v1/insights/releases/{release_uuid}/vulnerabilities" + }; + } + + // GetComponentDependencies returns component dependencies for a release. + rpc GetComponentDependencies(GetComponentDependenciesRequest) returns (ComponentDependencies) { + option (google.api.http) = { + get: "/v1/insights/releases/{release_uuid}/dependencies" + }; + } + + // FindAffectedReleases finds releases affected by a specific component. + rpc FindAffectedReleases(FindAffectedReleasesRequest) returns (FindAffectedReleasesResponse) { + option (google.api.http) = { + get: "/v1/insights/affected" + }; + } + + // GetLicenseSummary returns license information for a release. + rpc GetLicenseSummary(GetLicenseSummaryRequest) returns (LicenseSummary) { + option (google.api.http) = { + get: "/v1/insights/releases/{release_uuid}/licenses" + }; + } + + // GetCryptoInventory returns cryptographic assets for a release. + // Based on CBOM (Cryptographic Bill of Materials). + rpc GetCryptoInventory(GetCryptoInventoryRequest) returns (CryptoInventory) { + option (google.api.http) = { + get: "/v1/insights/releases/{release_uuid}/crypto" + }; + } + + // ========================================================================== + // Analytics + // ========================================================================== + + // GetReleaseMetrics returns analytics for a release. + rpc GetReleaseMetrics(GetReleaseMetricsRequest) returns (ReleaseMetrics) { + option (google.api.http) = { + get: "/v1/insights/releases/{release_uuid}/metrics" + }; + } + + // CompareSBOMs compares two SBOMs for drift detection. + rpc CompareSBOMs(CompareSBOMsRequest) returns (SBOMComparison) { + option (google.api.http) = { + post: "/v1/insights/compare/sbom" + body: "*" + }; + } +} + +// ============================================================================ +// CEL Query Messages +// ============================================================================ + +// QueryRequest contains a CEL query to execute. +message QueryRequest { + // CEL expression to evaluate. + // The expression has access to TEA data objects (products, components, + // releases, artifacts, sboms, vex, etc.). + // + // Example expressions: + // - components.exists(c, c.purl == "pkg:maven/org.apache.logging.log4j/log4j-core") + // - sbom.components.filter(c, c.vulnerabilities.size() > 0) + // - releases.filter(r, r.artifacts.exists(a, a.type == "VULNERABILITIES")) + string expression = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 10000, + (buf.validate.field).required = true + ]; + + // Scope of the query (what data to query against). + QueryScope scope = 2; + + // Variables to bind in the expression. + map variables = 3; + + // Maximum execution time (server may enforce limits). + int32 timeout_seconds = 4 [json_name = "timeoutSeconds"]; + + // Pagination for results. + PageRequest pagination = 5; + + // Output format. + QueryOutputFormat output_format = 6 [json_name = "outputFormat"]; +} + +// QueryScope defines what data the query operates on. +message QueryScope { + // Query against a specific release. + optional string release_uuid = 1 [json_name = "releaseUuid"]; + + // Query against a specific product. + optional string product_uuid = 2 [json_name = "productUuid"]; + + // Query against all data (requires special permission). + bool global = 3; + + // Artifact types to include in scope. + repeated ArtifactType artifact_types = 4 [json_name = "artifactTypes"]; + + // Date range for temporal queries. + DateRange date_range = 5 [json_name = "dateRange"]; +} + +// QueryOutputFormat specifies how to format query results. +enum QueryOutputFormat { + // Unspecified - server default (JSON). + QUERY_OUTPUT_FORMAT_UNSPECIFIED = 0; + + // JSON output. + QUERY_OUTPUT_FORMAT_JSON = 1; + + // Protocol Buffer Any. + QUERY_OUTPUT_FORMAT_PROTO = 2; + + // CSV (for tabular results). + QUERY_OUTPUT_FORMAT_CSV = 3; +} + +// QueryResponse contains the results of a CEL query. +message QueryResponse { + // Query result as JSON (when output_format is JSON). + google.protobuf.Struct result = 1; + + // Query result as Any (when output_format is PROTO). + google.protobuf.Any result_proto = 2 [json_name = "resultProto"]; + + // Query result as CSV (when output_format is CSV). + string result_csv = 3 [json_name = "resultCsv"]; + + // Number of results. + int64 result_count = 4 [json_name = "resultCount"]; + + // Execution time in milliseconds. + int64 execution_time_ms = 5 [json_name = "executionTimeMs"]; + + // Pagination for continued results. + PageResponse pagination = 6; + + // Warnings during execution. + repeated string warnings = 7; +} + +// ValidateQueryRequest validates a CEL expression. +message ValidateQueryRequest { + // CEL expression to validate. + string expression = 1 [(buf.validate.field).required = true]; +} + +// ValidateQueryResponse contains validation results. +message ValidateQueryResponse { + // Whether the expression is valid. + bool valid = 1; + + // Validation errors (if any). + repeated QueryValidationError errors = 2; + + // Inferred return type. + string return_type = 3 [json_name = "returnType"]; + + // Estimated complexity score. + int32 complexity_score = 4 [json_name = "complexityScore"]; +} + +// QueryValidationError describes a CEL validation error. +message QueryValidationError { + // Error message. + string message = 1; + + // Position in expression. + int32 position = 2; + + // Line number. + int32 line = 3; + + // Column number. + int32 column = 4; +} + +// ============================================================================ +// Vulnerability Messages +// ============================================================================ + +// GetVulnerabilitySummaryRequest requests vulnerability summary. +message GetVulnerabilitySummaryRequest { + // UUID of the release. + string release_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "releaseUuid" + ]; + + // Include VEX analysis (not just raw vulnerabilities). + bool include_vex_analysis = 2 [json_name = "includeVexAnalysis"]; + + // Minimum severity to include. + VulnerabilitySeverity min_severity = 3 [json_name = "minSeverity"]; +} + +// VulnerabilitySummary contains vulnerability information. +message VulnerabilitySummary { + // UUID of the release. + string release_uuid = 1 [json_name = "releaseUuid"]; + + // Total vulnerabilities found. + int32 total_count = 2 [json_name = "totalCount"]; + + // Counts by severity. + VulnerabilityCounts by_severity = 3 [json_name = "bySeverity"]; + + // Counts by status (from VEX). + VexStatusCounts by_vex_status = 4 [json_name = "byVexStatus"]; + + // Top vulnerabilities (most severe). + repeated VulnerabilityInfo top_vulnerabilities = 5 [json_name = "topVulnerabilities"]; + + // When the analysis was performed. + google.protobuf.Timestamp analyzed_at = 6 [json_name = "analyzedAt"]; + + // Artifacts used for this analysis. + repeated string source_artifact_uuids = 7 [json_name = "sourceArtifactUuids"]; +} + +// VulnerabilitySeverity levels. +enum VulnerabilitySeverity { + // Severity could not be determined. + VULNERABILITY_SEVERITY_UNSPECIFIED = 0; + // No vulnerability is present. + VULNERABILITY_SEVERITY_NONE = 1; + // Vulnerability has low impact. + VULNERABILITY_SEVERITY_LOW = 2; + // Vulnerability has medium impact. + VULNERABILITY_SEVERITY_MEDIUM = 3; + // Vulnerability has high impact. + VULNERABILITY_SEVERITY_HIGH = 4; + // Vulnerability has critical impact. + VULNERABILITY_SEVERITY_CRITICAL = 5; +} + +// VulnerabilityCounts by severity. +message VulnerabilityCounts { + // Number of critical vulnerabilities. + int32 critical = 1; + // Number of high-severity vulnerabilities. + int32 high = 2; + // Number of medium-severity vulnerabilities. + int32 medium = 3; + // Number of low-severity vulnerabilities. + int32 low = 4; + // Number of explicitly non-applicable vulnerabilities. + int32 none = 5; + // Number of vulnerabilities with unknown severity. + int32 unknown = 6; +} + +// VexStatusCounts by VEX status. +message VexStatusCounts { + // Number of affected findings. + int32 affected = 1; + // Number of findings marked not affected. + int32 not_affected = 2; + // Number of findings marked fixed. + int32 fixed = 3; + // Number of findings still under investigation. + int32 under_investigation = 4 [json_name = "underInvestigation"]; +} + +// VulnerabilityInfo contains details about a vulnerability. +message VulnerabilityInfo { + // CVE or other vulnerability ID. + string id = 1; + + // Severity level. + VulnerabilitySeverity severity = 2; + + // CVSS score (if available). + optional float cvss_score = 3 [json_name = "cvssScore"]; + + // Brief description. + string description = 4; + + // Affected component. + Identifier affected_component = 5 [json_name = "affectedComponent"]; + + // VEX status (if VEX is available). + optional VexStatus vex_status = 6 [json_name = "vexStatus"]; + + // VEX justification. + string vex_justification = 7 [json_name = "vexJustification"]; +} + +// VexStatus from VEX documents. +enum VexStatus { + // No VEX status is available. + VEX_STATUS_UNSPECIFIED = 0; + // The product is affected by the vulnerability. + VEX_STATUS_AFFECTED = 1; + // The product is not affected by the vulnerability. + VEX_STATUS_NOT_AFFECTED = 2; + // The vulnerability has been fixed. + VEX_STATUS_FIXED = 3; + // Impact analysis is still in progress. + VEX_STATUS_UNDER_INVESTIGATION = 4; +} + +// ============================================================================ +// Dependency Messages +// ============================================================================ + +// GetComponentDependenciesRequest requests dependency tree. +message GetComponentDependenciesRequest { + // UUID of the release. + string release_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "releaseUuid" + ]; + + // Maximum depth to traverse (0 = unlimited). + int32 max_depth = 2 [json_name = "maxDepth"]; + + // Include transitive dependencies. + bool include_transitive = 3 [json_name = "includeTransitive"]; +} + +// ComponentDependencies contains the dependency tree. +message ComponentDependencies { + // UUID of the release. + string release_uuid = 1 [json_name = "releaseUuid"]; + + // Root component. + DependencyNode root = 2; + + // Total dependency count (including transitive). + int32 total_count = 3 [json_name = "totalCount"]; + + // Direct dependency count. + int32 direct_count = 4 [json_name = "directCount"]; + + // Transitive dependency count. + int32 transitive_count = 5 [json_name = "transitiveCount"]; +} + +// DependencyNode represents a component in the dependency tree. +message DependencyNode { + // Component identifier. + Identifier identifier = 1; + + // Component name. + string name = 2; + + // Component version. + string version = 3; + + // Dependency scope (runtime, dev, test, etc.). + string scope = 4; + + // Direct dependencies. + repeated DependencyNode dependencies = 5; + + // Depth in the tree. + int32 depth = 6; +} + +// FindAffectedReleasesRequest finds releases using a component. +message FindAffectedReleasesRequest { + // Identifier of the component to search for. + Identifier identifier = 1 [(buf.validate.field).required = true]; + + // Version range (VERS format). + string version_range = 2 [json_name = "versionRange"]; + + // Pagination. + PageRequest pagination = 3; +} + +// FindAffectedReleasesResponse contains matching releases. +message FindAffectedReleasesResponse { + // Releases that use the specified component. + repeated AffectedRelease releases = 1; + + // Pagination. + PageResponse pagination = 2; +} + +// AffectedRelease contains info about an affected release. +message AffectedRelease { + // Release UUID. + string release_uuid = 1 [json_name = "releaseUuid"]; + + // Release version. + string version = 2; + + // Product name (if available). + string product_name = 3 [json_name = "productName"]; + + // How the component is used (direct/transitive). + string dependency_type = 4 [json_name = "dependencyType"]; + + // Specific version of the component used. + string component_version = 5 [json_name = "componentVersion"]; +} + +// ============================================================================ +// License Messages +// ============================================================================ + +// GetLicenseSummaryRequest requests license summary. +message GetLicenseSummaryRequest { + // UUID of the release. + string release_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "releaseUuid" + ]; +} + +// LicenseSummary contains license information. +message LicenseSummary { + // UUID of the release. + string release_uuid = 1 [json_name = "releaseUuid"]; + + // Total components analyzed. + int32 total_components = 2 [json_name = "totalComponents"]; + + // Components with identified licenses. + int32 with_license = 3 [json_name = "withLicense"]; + + // Components without identified licenses. + int32 without_license = 4 [json_name = "withoutLicense"]; + + // License distribution. + repeated LicenseCount licenses = 5; + + // License conflicts (if any). + repeated LicenseConflict conflicts = 6; +} + +// LicenseCount represents a license and its usage count. +message LicenseCount { + // SPDX license ID. + string spdx_id = 1 [json_name = "spdxId"]; + + // License name. + string name = 2; + + // Number of components using this license. + int32 count = 3; + + // License category (permissive, copyleft, proprietary). + string category = 4; +} + +// LicenseConflict describes a potential license conflict. +message LicenseConflict { + // First license. + string license1 = 1; + + // Second license. + string license2 = 2; + + // Description of the conflict. + string description = 3; + + // Severity (info, warning, error). + string severity = 4; +} + +// ============================================================================ +// Crypto Messages +// ============================================================================ + +// GetCryptoInventoryRequest requests cryptographic inventory. +message GetCryptoInventoryRequest { + // UUID of the release. + string release_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "releaseUuid" + ]; +} + +// CryptoInventory contains cryptographic assets. +message CryptoInventory { + // UUID of the release. + string release_uuid = 1 [json_name = "releaseUuid"]; + + // Cryptographic algorithms in use. + repeated CryptoAlgorithm algorithms = 2; + + // Certificates found. + repeated CryptoCertificate certificates = 3; + + // Quantum readiness assessment. + QuantumReadiness quantum_readiness = 4 [json_name = "quantumReadiness"]; +} + +// CryptoAlgorithm describes a cryptographic algorithm. +message CryptoAlgorithm { + // Algorithm name (e.g., "AES-256-GCM", "RSA-2048"). + string name = 1; + + // Algorithm type (symmetric, asymmetric, hash, etc.). + string type = 2; + + // Usage (encryption, signing, hashing, key-exchange). + string usage = 3; + + // Key size (if applicable). + int32 key_size = 4 [json_name = "keySize"]; + + // Is quantum-safe. + bool quantum_safe = 5 [json_name = "quantumSafe"]; + + // Components using this algorithm. + repeated string components = 6; +} + +// CryptoCertificate describes a certificate. +message CryptoCertificate { + // Certificate subject. + string subject = 1; + + // Certificate issuer. + string issuer = 2; + + // Expiration date. + google.protobuf.Timestamp expires = 3; + + // Public key algorithm. + string algorithm = 4; + + // Key size. + int32 key_size = 5 [json_name = "keySize"]; +} + +// QuantumReadiness assesses post-quantum cryptography readiness. +message QuantumReadiness { + // Overall readiness score (0-100). + int32 score = 1; + + // Number of quantum-vulnerable algorithms. + int32 vulnerable_count = 2 [json_name = "vulnerableCount"]; + + // Number of quantum-safe algorithms. + int32 safe_count = 3 [json_name = "safeCount"]; + + // Recommendations. + repeated string recommendations = 4; +} + +// ============================================================================ +// Metrics Messages +// ============================================================================ + +// GetReleaseMetricsRequest requests release metrics. +message GetReleaseMetricsRequest { + // UUID of the release. + string release_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "releaseUuid" + ]; +} + +// ReleaseMetrics contains analytics for a release. +message ReleaseMetrics { + // UUID of the release. + string release_uuid = 1 [json_name = "releaseUuid"]; + + // Component metrics. + ComponentMetrics components = 2; + + // Artifact metrics. + ArtifactMetrics artifacts = 3; + + // Collection metrics. + CollectionMetrics collection = 4; +} + +// ComponentMetrics contains component-related metrics. +message ComponentMetrics { + // Total components. + int32 total = 1; + + // Direct dependencies. + int32 direct = 2; + + // Transitive dependencies. + int32 transitive = 3; + + // By type (library, framework, etc.). + map by_type = 4 [json_name = "byType"]; + + // By ecosystem (maven, npm, etc.). + map by_ecosystem = 5 [json_name = "byEcosystem"]; +} + +// ArtifactMetrics contains artifact-related metrics. +message ArtifactMetrics { + // Total artifacts. + int32 total = 1; + + // By type. + map by_type = 2 [json_name = "byType"]; + + // Total size in bytes. + int64 total_size_bytes = 3 [json_name = "totalSizeBytes"]; +} + +// CollectionMetrics contains collection-related metrics. +message CollectionMetrics { + // Collection version. + int32 version = 1; + + // Number of updates. + int32 update_count = 2 [json_name = "updateCount"]; + + // Last update timestamp. + google.protobuf.Timestamp last_updated = 3 [json_name = "lastUpdated"]; + + // Is signed. + bool is_signed = 4 [json_name = "isSigned"]; +} + +// ============================================================================ +// SBOM Comparison Messages +// ============================================================================ + +// CompareSBOMsRequest compares two SBOMs. +message CompareSBOMsRequest { + // First SBOM (base). + oneof base { + // Base release UUID. + string base_release_uuid = 1; + // Base artifact UUID. + string base_artifact_uuid = 2; + } + + // Second SBOM (target). + oneof target { + // Target release UUID. + string target_release_uuid = 3; + // Target artifact UUID. + string target_artifact_uuid = 4; + } + + // Include detailed component changes. + bool include_details = 5 [json_name = "includeDetails"]; +} + +// SBOMComparison contains comparison results. +message SBOMComparison { + // Components added in target. + repeated ComponentChange added = 1; + + // Components removed in target. + repeated ComponentChange removed = 2; + + // Components with version changes. + repeated ComponentVersionChange updated = 3; + + // Components unchanged. + int32 unchanged_count = 4 [json_name = "unchangedCount"]; + + // Summary statistics. + ComparisonSummary summary = 5; +} + +// ComponentChange describes an added or removed component. +message ComponentChange { + // Component identifier. + Identifier identifier = 1; + + // Component name. + string name = 2; + + // Component version. + string version = 3; +} + +// ComponentVersionChange describes a version change. +message ComponentVersionChange { + // Component identifier. + Identifier identifier = 1; + + // Component name. + string name = 2; + + // Previous version. + string old_version = 3 [json_name = "oldVersion"]; + + // New version. + string new_version = 4 [json_name = "newVersion"]; + + // Is upgrade (true) or downgrade (false). + bool is_upgrade = 5 [json_name = "isUpgrade"]; +} + +// ComparisonSummary contains comparison statistics. +message ComparisonSummary { + // Components in base SBOM. + int32 base_count = 1 [json_name = "baseCount"]; + + // Components in target SBOM. + int32 target_count = 2 [json_name = "targetCount"]; + + // Components added. + int32 added_count = 3 [json_name = "addedCount"]; + + // Components removed. + int32 removed_count = 4 [json_name = "removedCount"]; + + // Components updated. + int32 updated_count = 5 [json_name = "updatedCount"]; + + // Components unchanged. + int32 unchanged_count = 6 [json_name = "unchangedCount"]; + + // Similarity percentage. + float similarity_percent = 7 [json_name = "similarityPercent"]; +} diff --git a/proto/tea/v1/product.proto b/proto/tea/v1/product.proto new file mode 100644 index 0000000..25840d8 --- /dev/null +++ b/proto/tea/v1/product.proto @@ -0,0 +1,302 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// TEA Product +// ============================================================================ + +// Product represents a TEA Product - an optional higher-level object that +// groups multiple Product Releases for a product line or family. +// +// Products can be discovered and browsed; releases are accessed via the +// product releases endpoint. +// +// Example: "Apache Log4j 2" is a Product containing multiple ProductReleases +// like 2.24.3, 2.24.2, etc. +message Product { + // Unique identifier for this TEA Product. + // Format: UUID v4 or v7. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // Human-readable name of the product. + string name = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512 + ]; + + // Optional description of the product. + string description = 3 [(buf.validate.field).string.max_len = 4096]; + + // List of identifiers for this product. + // A product may have multiple identifiers (e.g., both CPE and PURL). + repeated Identifier identifiers = 4; + + // Vendor/publisher information. + Vendor vendor = 5; + + // Timestamp when this product was created in the TEA system. + google.protobuf.Timestamp created_date = 6 [json_name = "createdDate"]; + + // Timestamp when this product was last modified. + google.protobuf.Timestamp modified_date = 7 [json_name = "modifiedDate"]; + + // Optional URL to the product's homepage. + string homepage_url = 8 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "homepageUrl" + ]; + + // Optional URL to the product's documentation. + string documentation_url = 9 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "documentationUrl" + ]; + + // Optional URL to the product's VCS repository. + string vcs_url = 10 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + json_name = "vcsUrl" + ]; + + // Optional deprecation information. + optional Deprecation deprecation = 11; +} + +// Vendor represents a product vendor or publisher. +message Vendor { + // Vendor name. + string name = 1 [(buf.validate.field).string.max_len = 512]; + + // Optional vendor UUID (if registered in this TEA instance). + optional string uuid = 2 [(buf.validate.field).string.uuid = true]; + + // Optional vendor URL. + string url = 3 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; + + // Optional contact information. + repeated Contact contacts = 4; +} + +// Contact represents contact information for a vendor or organization. +message Contact { + // Contact name. + string name = 1 [(buf.validate.field).string.max_len = 256]; + + // Contact email address. + string email = 2 [ + (buf.validate.field).string.email = true, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; + + // Contact phone number. + string phone = 3 [(buf.validate.field).string.max_len = 64]; +} + +// ============================================================================ +// TEA Product Release +// ============================================================================ + +// ProductRelease represents a specific versioned release of a TEA Product. +// It is the primary resolvable entity via TEI and the entry point for +// discovery of included components and related collections of security artifacts. +// +// A ProductRelease is what a customer acquires or downloads - hardware and/or +// software. It can be a bundle of many digital devices or software applications. +message ProductRelease { + // Unique identifier for this product release. + // Format: UUID v4 or v7. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // UUID of the TEA Product this release belongs to. + // Optional - a release may exist without a parent product. + optional string product = 2 [(buf.validate.field).string.uuid = true]; + + // Human-readable version string of the product release. + // Can follow any versioning scheme (semver, calver, etc.). + string version = 3 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 256 + ]; + + // Timestamp when this product release was created in the TEA system. + google.protobuf.Timestamp created_date = 4 [json_name = "createdDate"]; + + // Upstream product release timestamp. + // This is the actual release date, not when it was added to TEA. + google.protobuf.Timestamp release_date = 5 [json_name = "releaseDate"]; + + // Indicates if this is a pre-release (beta, RC, milestone, etc.). + // Pre-releases may have limited support or stability guarantees. + // This flag can be disabled (set to false) after creation, but not enabled. + bool pre_release = 6 [json_name = "preRelease"]; + + // List of identifiers for this product release. + // Must include at least one TEI for discovery. + repeated Identifier identifiers = 7; + + // List of TEA Components included in this product release. + // For composed products, this lists all bundled components. + repeated ComponentRef components = 8; + + // Optional lifecycle status (if CLE is integrated). + LifecycleStatus lifecycle_status = 9 [json_name = "lifecycleStatus"]; + + // Optional deprecation information. + optional Deprecation deprecation = 10; +} + +// ComponentRef is a reference to a TEA Component included in a ProductRelease. +message ComponentRef { + // UUID of the TEA Component. + string uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true + ]; + + // Optional UUID of a specific TEA Component Release to pin. + // If omitted, the product release may include any/multiple releases + // of the referenced component. + optional string release = 2 [(buf.validate.field).string.uuid = true]; +} + +// ============================================================================ +// Lifecycle Status (CLE Integration) +// ============================================================================ + +// LifecyclePhase represents the current phase in a product's lifecycle. +// Based on OWASP Common Lifecycle Enumeration (CLE). +enum LifecyclePhase { + // Unspecified phase. + LIFECYCLE_PHASE_UNSPECIFIED = 0; + + // Product is in active development. + LIFECYCLE_PHASE_DEVELOPMENT = 1; + + // Product is released and actively maintained. + LIFECYCLE_PHASE_ACTIVE = 2; + + // Product is still supported but no new features. + LIFECYCLE_PHASE_MAINTENANCE = 3; + + // Product has reached end of life. + LIFECYCLE_PHASE_END_OF_LIFE = 4; + + // Product has been deprecated in favor of a successor. + LIFECYCLE_PHASE_DEPRECATED = 5; + + // Product has been superseded by another product. + LIFECYCLE_PHASE_SUPERSEDED = 6; +} + +// LifecycleStatus contains lifecycle information for a product or release. +message LifecycleStatus { + // Current lifecycle phase. + LifecyclePhase phase = 1; + + // End of active support date (if known). + google.protobuf.Timestamp end_of_support = 2 [json_name = "endOfSupport"]; + + // End of life date (if known). + google.protobuf.Timestamp end_of_life = 3 [json_name = "endOfLife"]; + + // UUID of the successor product/release (if superseded). + optional string successor_uuid = 4 [json_name = "successorUuid"]; + + // Human-readable lifecycle notes. + string notes = 5; +} + +// ============================================================================ +// Request/Response Messages +// ============================================================================ + +// ListProductsRequest is the request for listing products. +message ListProductsRequest { + // Pagination parameters. + PageRequest pagination = 1; + + // Optional filter by vendor UUID. + optional string vendor_uuid = 2 [(buf.validate.field).string.uuid = true, json_name = "vendorUuid"]; + + // Optional search query (matches name, description). + string query = 3 [(buf.validate.field).string.max_len = 256]; + + // Optional sort specification. + SortSpec sort = 4; + + // Filter by identifier. + optional Identifier identifier = 5; +} + +// ListProductsResponse is the response for listing products. +message ListProductsResponse { + // List of products. + repeated Product products = 1; + + // Pagination information. + PageResponse pagination = 2; +} + +// GetProductRequest is the request for getting a single product. +message GetProductRequest { + // UUID of the product to retrieve. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} + +// ListProductReleasesRequest is the request for listing releases of a product. +message ListProductReleasesRequest { + // UUID of the product. + string product_uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true, json_name = "productUuid"]; + + // Pagination parameters. + PageRequest pagination = 2; + + // Include pre-releases in the response. + bool include_pre_releases = 3 [json_name = "includePreReleases"]; + + // Filter by release date range. + DateRange release_date_range = 4 [json_name = "releaseDateRange"]; + + // Optional sort specification. + SortSpec sort = 5; +} + +// ListProductReleasesResponse is the response for listing product releases. +message ListProductReleasesResponse { + // List of product releases. + repeated ProductRelease releases = 1; + + // Pagination information. + PageResponse pagination = 2; +} + +// GetProductReleaseRequest is the request for getting a single product release. +message GetProductReleaseRequest { + // UUID of the product release to retrieve. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} \ No newline at end of file diff --git a/proto/tea/v1/publisher.proto b/proto/tea/v1/publisher.proto new file mode 100644 index 0000000..93ace40 --- /dev/null +++ b/proto/tea/v1/publisher.proto @@ -0,0 +1,792 @@ +// Copyright 2024-2026 CycloneDX Contributors +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package tea.v1; + +import "buf/validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; +import "tea/v1/common.proto"; +import "tea/v1/product.proto"; +import "tea/v1/component.proto"; +import "tea/v1/collection.proto"; +import "tea/v1/artifact.proto"; + +option go_package = "github.com/CycloneDX/transparency-exchange-api/gen/go/tea/v1;teav1"; +option java_package = "org.cyclonedx.tea.v1"; +option java_multiple_files = true; +option csharp_namespace = "CycloneDX.Tea.V1"; + +// ============================================================================ +// Publisher Service +// ============================================================================ + +// PublisherService provides write access for artifact publishers to manage +// transparency artifacts in the TEA server. +// +// All endpoints require authentication and appropriate authorization. +// The exact auth mechanism and scope vocabulary are deployment-specific, but +// implementations MUST reject unauthorized writes. +// +// This service implements the TEA Publisher API specification. +// Note: The Publisher API is a recommended (optional) part of the TEA spec. +// Implementations MAY expose only a subset of these RPCs, but unsupported +// operations MUST fail explicitly rather than partially succeeding. +service PublisherService { + // ========================================================================== + // Product Management + // ========================================================================== + + // CreateProduct creates a new product. + rpc CreateProduct(CreateProductRequest) returns (Product) { + option (google.api.http) = { + post: "/v1/publisher/products" + body: "*" + }; + } + + // UpdateProduct updates an existing product. + // Fields not listed in update_mask MUST be preserved as-is. + rpc UpdateProduct(UpdateProductRequest) returns (Product) { + option (google.api.http) = { + put: "/v1/publisher/products/{uuid}" + body: "*" + }; + } + + // DeleteProduct removes a product and optionally its releases. + // Implementations SHOULD fail with FAILED_PRECONDITION when cascade=false and + // dependent releases still exist. + rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse) { + option (google.api.http) = { + delete: "/v1/publisher/products/{uuid}" + }; + } + + // CreateProductRelease creates a new release for a product. + rpc CreateProductRelease(CreateProductReleaseRequest) returns (ProductRelease) { + option (google.api.http) = { + post: "/v1/publisher/product-releases" + body: "*" + }; + } + + // UpdateProductRelease updates an existing product release. + // Fields not listed in update_mask MUST be preserved as-is. + rpc UpdateProductRelease(UpdateProductReleaseRequest) returns (ProductRelease) { + option (google.api.http) = { + put: "/v1/publisher/product-releases/{uuid}" + body: "*" + }; + } + + // DeleteProductRelease removes a product release. + rpc DeleteProductRelease(DeleteProductReleaseRequest) returns (DeleteProductReleaseResponse) { + option (google.api.http) = { + delete: "/v1/publisher/product-releases/{uuid}" + }; + } + + // ========================================================================== + // Component Management + // ========================================================================== + + // CreateComponent creates a new component. + rpc CreateComponent(CreateComponentRequest) returns (Component) { + option (google.api.http) = { + post: "/v1/publisher/components" + body: "*" + }; + } + + // UpdateComponent updates an existing component. + // Fields not listed in update_mask MUST be preserved as-is. + rpc UpdateComponent(UpdateComponentRequest) returns (Component) { + option (google.api.http) = { + put: "/v1/publisher/components/{uuid}" + body: "*" + }; + } + + // DeleteComponent removes a component and optionally its releases. + // Implementations SHOULD fail with FAILED_PRECONDITION when cascade=false and + // dependent releases still exist. + rpc DeleteComponent(DeleteComponentRequest) returns (DeleteComponentResponse) { + option (google.api.http) = { + delete: "/v1/publisher/components/{uuid}" + }; + } + + // CreateComponentRelease creates a new release for a component. + rpc CreateComponentRelease(CreateComponentReleaseRequest) returns (ComponentRelease) { + option (google.api.http) = { + post: "/v1/publisher/component-releases" + body: "*" + }; + } + + // UpdateComponentRelease updates an existing component release. + // Note: pre_release can only be changed from true to false, not vice versa. + // Fields not listed in update_mask MUST be preserved as-is. + rpc UpdateComponentRelease(UpdateComponentReleaseRequest) returns (ComponentRelease) { + option (google.api.http) = { + put: "/v1/publisher/component-releases/{uuid}" + body: "*" + }; + } + + // DeleteComponentRelease removes a component release. + rpc DeleteComponentRelease(DeleteComponentReleaseRequest) returns (DeleteComponentReleaseResponse) { + option (google.api.http) = { + delete: "/v1/publisher/component-releases/{uuid}" + }; + } + + // ========================================================================== + // Artifact Management + // ========================================================================== + + // UploadArtifact uploads a new artifact. + // Artifacts are immutable once created. The first streamed frame MUST contain + // metadata; subsequent frames carry content bytes. + rpc UploadArtifact(stream UploadArtifactRequest) returns (Artifact) { + option (google.api.http) = { + post: "/v1/publisher/artifacts" + body: "*" + }; + } + + // CreateArtifactFromUrl creates an artifact from an external URL. + // The server fetches the content, verifies the declared checksums, and then + // registers the artifact metadata. Implementations MAY reject non-HTTPS URLs + // or private-network source URLs for safety. + rpc CreateArtifactFromUrl(CreateArtifactFromUrlRequest) returns (Artifact) { + option (google.api.http) = { + post: "/v1/publisher/artifacts/from-url" + body: "*" + }; + } + + // DeleteArtifact removes an artifact. + // Artifacts in use by collections MUST NOT be deleted unless the + // implementation can safely and explicitly handle force deletion. + rpc DeleteArtifact(DeleteArtifactRequest) returns (DeleteArtifactResponse) { + option (google.api.http) = { + delete: "/v1/publisher/artifacts/{uuid}" + }; + } + + // ========================================================================== + // Collection Management + // ========================================================================== + + // CreateCollection creates version 1 of a logical collection stream. + // All referenced artifacts MUST already exist. + rpc CreateCollection(CreateCollectionRequest) returns (Collection) { + option (google.api.http) = { + post: "/v1/publisher/collections" + body: "*" + }; + } + + // UpdateCollection creates a new immutable version of an existing collection. + // Prior versions remain addressable. + rpc UpdateCollection(UpdateCollectionRequest) returns (Collection) { + option (google.api.http) = { + post: "/v1/publisher/collections/{uuid}/versions" + body: "*" + }; + } + + // SignCollection signs one collection version and returns the updated + // collection metadata. + rpc SignCollection(SignCollectionRequest) returns (Collection) { + option (google.api.http) = { + post: "/v1/publisher/collections/{uuid}/sign" + body: "*" + }; + } + + // ========================================================================== + // Batch Operations + // ========================================================================== + + // BatchUploadArtifacts uploads multiple artifacts in one session. + // This is an advanced optional publisher capability. + rpc BatchUploadArtifacts(stream BatchUploadArtifactsRequest) returns (BatchUploadArtifactsResponse) { + option (google.api.http) = { + post: "/v1/publisher/artifacts/batch" + body: "*" + }; + } + + // ImportCollection imports complete collection + artifact data. + // This is an advanced optional publisher capability used for bulk migration. + rpc ImportCollection(stream ImportCollectionRequest) returns (ImportCollectionResponse) { + option (google.api.http) = { + post: "/v1/publisher/import" + body: "*" + }; + } +} + +// ============================================================================ +// Product Request/Response Messages +// ============================================================================ + +// CreateProductRequest is the request to create a product. +message CreateProductRequest { + // Human-readable name. + string name = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 512, + (buf.validate.field).required = true + ]; + + // Optional description. + string description = 2; + + // Identifiers for the product. + repeated Identifier identifiers = 3; + + // Vendor information. + Vendor vendor = 4; + + // Optional homepage URL. + string homepage_url = 5 [json_name = "homepageUrl"]; + + // Optional documentation URL. + string documentation_url = 6 [json_name = "documentationUrl"]; + + // Optional VCS URL. + string vcs_url = 7 [json_name = "vcsUrl"]; + + // Client-provided UUID (optional, server generates if not provided). + optional string uuid = 8 [(buf.validate.field).string.uuid = true]; +} + +// UpdateProductRequest is the request to update a product. +message UpdateProductRequest { + // UUID of the product to update. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Fields to update. + google.protobuf.FieldMask update_mask = 2 [json_name = "updateMask"]; + + // Updated name. + string name = 3; + + // Updated description. + string description = 4; + + // Updated identifiers (replaces existing). + repeated Identifier identifiers = 5; + + // Updated vendor information. + Vendor vendor = 6; + + // Updated homepage URL. + string homepage_url = 7 [json_name = "homepageUrl"]; + + // Updated documentation URL. + string documentation_url = 8 [json_name = "documentationUrl"]; + + // Updated VCS URL. + string vcs_url = 9 [json_name = "vcsUrl"]; +} + +// DeleteProductRequest is the request to delete a product. +message DeleteProductRequest { + // UUID of the product to delete. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // If true, delete all releases as well. + bool cascade = 2; +} + +// DeleteProductResponse is the response from deleting a product. +message DeleteProductResponse { + // UUID of the deleted product. + string uuid = 1; + + // Number of releases deleted (if cascade=true). + int32 releases_deleted = 2 [json_name = "releasesDeleted"]; +} + +// CreateProductReleaseRequest is the request to create a product release. +message CreateProductReleaseRequest { + // UUID of the parent product (optional). + optional string product_uuid = 1 [(buf.validate.field).string.uuid = true, json_name = "productUuid"]; + + // Version string. + string version = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).required = true + ]; + + // Release date. + google.protobuf.Timestamp release_date = 3 [json_name = "releaseDate"]; + + // Pre-release flag. + bool pre_release = 4 [json_name = "preRelease"]; + + // Identifiers for the release. + repeated Identifier identifiers = 5; + + // Component references. + repeated ComponentRef components = 6; + + // Client-provided UUID (optional). + optional string uuid = 7 [(buf.validate.field).string.uuid = true]; +} + +// UpdateProductReleaseRequest is the request to update a product release. +message UpdateProductReleaseRequest { + // UUID of the product release to update. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Fields to update. + google.protobuf.FieldMask update_mask = 2 [json_name = "updateMask"]; + + // Updated version. + string version = 3; + + // Updated release date. + google.protobuf.Timestamp release_date = 4 [json_name = "releaseDate"]; + + // Pre-release flag (can only change from true to false). + bool pre_release = 5 [json_name = "preRelease"]; + + // Updated identifiers. + repeated Identifier identifiers = 6; + + // Updated component references. + repeated ComponentRef components = 7; +} + +// DeleteProductReleaseRequest is the request to delete a product release. +message DeleteProductReleaseRequest { + // UUID of the product release to delete. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} + +// DeleteProductReleaseResponse is the response from deleting a product release. +message DeleteProductReleaseResponse { + // UUID of the deleted product release. + string uuid = 1; +} + +// ============================================================================ +// Component Request/Response Messages +// ============================================================================ + +// CreateComponentRequest is the request to create a component. +message CreateComponentRequest { + // Human-readable name. + string name = 1 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).required = true + ]; + + // Optional description. + string description = 2; + + // Identifiers for the component. + repeated Identifier identifiers = 3; + + // Component type. + ComponentType component_type = 4 [json_name = "componentType"]; + + // License information. + repeated LicenseInfo licenses = 5; + + // Publisher name. + string publisher = 6; + + // Homepage URL. + string homepage_url = 7 [json_name = "homepageUrl"]; + + // VCS URL. + string vcs_url = 8 [json_name = "vcsUrl"]; + + // Client-provided UUID (optional). + optional string uuid = 9 [(buf.validate.field).string.uuid = true]; +} + +// UpdateComponentRequest is the request to update a component. +message UpdateComponentRequest { + // UUID of the component to update. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Fields to update. + google.protobuf.FieldMask update_mask = 2 [json_name = "updateMask"]; + + // Updated name. + string name = 3; + + // Updated description. + string description = 4; + + // Updated identifiers. + repeated Identifier identifiers = 5; + + // Updated component type. + ComponentType component_type = 6 [json_name = "componentType"]; + + // Updated licenses. + repeated LicenseInfo licenses = 7; + + // Updated publisher. + string publisher = 8; + + // Updated homepage URL. + string homepage_url = 9 [json_name = "homepageUrl"]; + + // Updated VCS URL. + string vcs_url = 10 [json_name = "vcsUrl"]; +} + +// DeleteComponentRequest is the request to delete a component. +message DeleteComponentRequest { + // UUID of the component to delete. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // If true, delete all releases as well. + bool cascade = 2; +} + +// DeleteComponentResponse is the response from deleting a component. +message DeleteComponentResponse { + // UUID of the deleted component. + string uuid = 1; + + // Number of releases deleted (if cascade=true). + int32 releases_deleted = 2 [json_name = "releasesDeleted"]; +} + +// CreateComponentReleaseRequest is the request to create a component release. +message CreateComponentReleaseRequest { + // UUID of the parent component. + string component_uuid = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).required = true, + json_name = "componentUuid" + ]; + + // Version string. + string version = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).required = true + ]; + + // Release date. + google.protobuf.Timestamp release_date = 3 [json_name = "releaseDate"]; + + // Pre-release flag. + bool pre_release = 4 [json_name = "preRelease"]; + + // Identifiers for the release. + repeated Identifier identifiers = 5; + + // Distributions. + repeated Distribution distributions = 6; + + // Client-provided UUID (optional). + optional string uuid = 7 [(buf.validate.field).string.uuid = true]; +} + +// UpdateComponentReleaseRequest is the request to update a component release. +message UpdateComponentReleaseRequest { + // UUID of the component release to update. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Fields to update. + google.protobuf.FieldMask update_mask = 2 [json_name = "updateMask"]; + + // Updated version. + string version = 3; + + // Updated release date. + google.protobuf.Timestamp release_date = 4 [json_name = "releaseDate"]; + + // Pre-release flag (can only change from true to false). + bool pre_release = 5 [json_name = "preRelease"]; + + // Updated identifiers. + repeated Identifier identifiers = 6; + + // Updated distributions. + repeated Distribution distributions = 7; +} + +// DeleteComponentReleaseRequest is the request to delete a component release. +message DeleteComponentReleaseRequest { + // UUID of the component release to delete. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; +} + +// DeleteComponentReleaseResponse is the response from deleting a component release. +message DeleteComponentReleaseResponse { + // UUID of the deleted component release. + string uuid = 1; +} + +// ============================================================================ +// Artifact Request/Response Messages +// ============================================================================ + +// UploadArtifactRequest is used for streaming artifact uploads. +message UploadArtifactRequest { + // Streaming payload frames for an artifact upload. + oneof data { + // Metadata (must be sent first). + ArtifactMetadata metadata = 1; + + // Content chunk. + bytes content = 2; + } +} + +// ArtifactMetadata contains metadata for artifact upload or URL registration. +message ArtifactMetadata { + // Human-readable name. + string name = 1 [(buf.validate.field).required = true]; + + // Artifact type. + ArtifactType type = 2 [(buf.validate.field).enum = {defined_only: true, not_in: [0]}]; + + // MIME type of the content. + string mime_type = 3 [json_name = "mimeType", (buf.validate.field).required = true]; + + // Optional description. + string description = 4; + + // Distribution types this applies to. + repeated string component_distributions = 5 [json_name = "componentDistributions"]; + + // Subject of the artifact. + ArtifactSubject subject = 6; + + // Spec version (e.g., "1.5" for CycloneDX). + string spec_version = 7 [json_name = "specVersion"]; + + // Client-provided UUID (optional). + optional string uuid = 8 [(buf.validate.field).string.uuid = true]; + + // Expected checksums for verification. + // Implementations SHOULD verify uploaded or fetched content against these + // values before persisting the artifact record. + repeated Checksum expected_checksums = 9 [json_name = "expectedChecksums"]; +} + +// CreateArtifactFromUrlRequest creates an artifact from an external URL. +message CreateArtifactFromUrlRequest { + // Artifact metadata. + ArtifactMetadata metadata = 1 [(buf.validate.field).required = true]; + + // URL to fetch the content from. + // This SHOULD point to immutable content. Implementations MAY enforce HTTPS + // and MAY reject private-network targets. + string source_url = 2 [ + (buf.validate.field).string.uri = true, + (buf.validate.field).required = true, + json_name = "sourceUrl" + ]; + + // Expected checksums for verification. + // If both metadata.expected_checksums and this field are populated, they MUST + // describe the same checksum set or the request should be rejected. + repeated Checksum expected_checksums = 3 [json_name = "expectedChecksums"]; + + // Optional signature URL. + string signature_url = 4 [json_name = "signatureUrl"]; +} + +// DeleteArtifactRequest is the request to delete an artifact. +message DeleteArtifactRequest { + // UUID of the artifact to delete. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Force deletion even if artifact is in use. + // Implementations MAY reject this flag when referential cleanup is not + // supported safely. + bool force = 2; +} + +// DeleteArtifactResponse is the response from deleting an artifact. +message DeleteArtifactResponse { + // UUID of the deleted artifact. + string uuid = 1; + + // Collections that referenced this artifact (if forced). + repeated string affected_collection_uuids = 2 [json_name = "affectedCollectionUuids"]; +} + +// ============================================================================ +// Collection Request/Response Messages +// ============================================================================ + +// CreateCollectionRequest is the request to create a collection. +message CreateCollectionRequest { + // Logical collection UUID. + // For release-scoped collections this is typically the referenced release + // UUID and remains stable across collection versions. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Scope of the collection. + CollectionScope belongs_to = 2 [json_name = "belongsTo", (buf.validate.field).required = true]; + + // UUIDs of artifacts to include in version 1. + repeated string artifact_uuids = 3 [json_name = "artifactUuids"]; + + // Update reason (typically INITIAL_RELEASE for version 1). + UpdateReason update_reason = 4 [json_name = "updateReason"]; +} + +// UpdateCollectionRequest creates a new version of a collection. +message UpdateCollectionRequest { + // UUID of the collection to update. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Full replacement set of artifacts in the new version. + repeated string artifact_uuids = 2 [json_name = "artifactUuids"]; + + // Reason for the update. + UpdateReason update_reason = 3 [json_name = "updateReason", (buf.validate.field).required = true]; +} + +// SignCollectionRequest signs a collection version. +message SignCollectionRequest { + // UUID of the collection to sign. + string uuid = 1 [(buf.validate.field).string.uuid = true, (buf.validate.field).required = true]; + + // Version to sign (latest if omitted). + optional int32 version = 2; + + // Key ID to use for signing. + string key_id = 3 [json_name = "keyId"]; + + // Use Sigstore for signing. + bool use_sigstore = 4 [json_name = "useSigstore"]; +} + +// ============================================================================ +// Batch Operations +// ============================================================================ + +// BatchUploadArtifactsRequest is used for batch artifact uploads. +message BatchUploadArtifactsRequest { + // Streaming payload frames for a batch upload. + oneof data { + // Batch metadata (must be sent first). + BatchArtifactMetadata batch_metadata = 1; + + // Single artifact data. + UploadArtifactRequest artifact = 2; + } +} + +// BatchArtifactMetadata contains metadata for batch upload. +message BatchArtifactMetadata { + // Total number of artifacts in this batch. + int32 total_count = 1 [json_name = "totalCount"]; + + // Target collection UUID (optional). + optional string collection_uuid = 2 [json_name = "collectionUuid"]; +} + +// BatchUploadArtifactsResponse contains results of batch upload. +message BatchUploadArtifactsResponse { + // Successfully uploaded artifacts. + repeated Artifact artifacts = 1; + + // Failed uploads. + repeated BatchUploadError errors = 2; +} + +// BatchUploadError describes a failed upload in a batch. +message BatchUploadError { + // Index in the batch. + int32 index = 1; + + // Artifact name (if available). + string name = 2; + + // Error details. + ErrorDetail error = 3; +} + +// ImportCollectionRequest is used for bulk import. +message ImportCollectionRequest { + // Streaming payload frames for a collection import. + oneof data { + // Import metadata (must be sent first). + ImportMetadata import_metadata = 1; + + // Collection data. + Collection collection = 2; + + // Artifact with content. + ArtifactWithContent artifact = 3; + } +} + +// ImportMetadata contains metadata for bulk import. +message ImportMetadata { + // Source system identifier. + string source_system = 1 [json_name = "sourceSystem"]; + + // Total collections in import. + int32 total_collections = 2 [json_name = "totalCollections"]; + + // Total artifacts in import. + int32 total_artifacts = 3 [json_name = "totalArtifacts"]; + + // Overwrite existing data. + bool overwrite = 4; +} + +// ArtifactWithContent includes artifact metadata and content. +message ArtifactWithContent { + // Artifact metadata. + Artifact artifact = 1; + + // Content for each format. + repeated ArtifactFormatContent format_contents = 2 [json_name = "formatContents"]; +} + +// ArtifactFormatContent contains content for one format. +message ArtifactFormatContent { + // MIME type. + string mime_type = 1 [json_name = "mimeType"]; + + // Content bytes. + bytes content = 2; +} + +// ImportCollectionResponse contains results of bulk import. +message ImportCollectionResponse { + // Number of collections imported. + int32 collections_imported = 1 [json_name = "collectionsImported"]; + + // Number of artifacts imported. + int32 artifacts_imported = 2 [json_name = "artifactsImported"]; + + // Errors during import. + repeated ImportError errors = 3; +} + +// ImportError describes an error during import. +message ImportError { + // Type of entity that failed. + string entity_type = 1 [json_name = "entityType"]; + + // UUID of the entity. + string uuid = 2; + + // Error details. + ErrorDetail error = 3; +} diff --git a/schemas/json/artifact/artifact-format.schema.json b/schemas/json/artifact/artifact-format.schema.json new file mode 100644 index 0000000..c676f0d --- /dev/null +++ b/schemas/json/artifact/artifact-format.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/artifact-format.schema.json", + "type": "object", + "title": "TEA Artifact Format", + "description": "Represents a specific encoding/format of an artifact.", + "properties": { + "mimeType": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "MIME type of the document." + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "Human-readable description of this format." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Direct download URL for the artifact in this format." + }, + "signatureUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL for a detached digital signature." + }, + "checksums": { + "type": "array", + "items": { + "$ref": "../common/checksum.schema.json" + }, + "minItems": 1, + "description": "Checksums for integrity verification." + }, + "sizeBytes": { + "type": "integer", + "minimum": 0, + "description": "File size in bytes." + }, + "encoding": { + "type": "string", + "description": "Encoding of the content (e.g., 'utf-8', 'base64')." + }, + "specVersion": { + "type": "string", + "description": "Specification version (for typed artifacts)." + } + }, + "required": [ + "mimeType", + "url", + "checksums" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/artifact/artifact.schema.json b/schemas/json/artifact/artifact.schema.json new file mode 100644 index 0000000..29ba648 --- /dev/null +++ b/schemas/json/artifact/artifact.schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/artifact.schema.json", + "type": "object", + "title": "TEA Artifact", + "description": "Represents a TEA Artifact - a security-related document or file linked to a component release.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this TEA Artifact." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Human-readable name for the artifact." + }, + "type": { + "type": "string", + "enum": [ + "ARTIFACT_TYPE_UNSPECIFIED", + "ARTIFACT_TYPE_ATTESTATION", + "ARTIFACT_TYPE_BOM", + "ARTIFACT_TYPE_BUILD_META", + "ARTIFACT_TYPE_CERTIFICATION", + "ARTIFACT_TYPE_FORMULATION", + "ARTIFACT_TYPE_LICENSE", + "ARTIFACT_TYPE_RELEASE_NOTES", + "ARTIFACT_TYPE_SECURITY_TXT", + "ARTIFACT_TYPE_THREAT_MODEL", + "ARTIFACT_TYPE_VULNERABILITIES", + "ARTIFACT_TYPE_CLE", + "ARTIFACT_TYPE_CDXA", + "ARTIFACT_TYPE_CBOM", + "ARTIFACT_TYPE_MODEL_CARD", + "ARTIFACT_TYPE_STATIC_ANALYSIS", + "ARTIFACT_TYPE_DYNAMIC_ANALYSIS", + "ARTIFACT_TYPE_PENTEST_REPORT", + "ARTIFACT_TYPE_RISK_ASSESSMENT", + "ARTIFACT_TYPE_POAM", + "ARTIFACT_TYPE_QUALITY_METRICS", + "ARTIFACT_TYPE_HARNESS", + "ARTIFACT_TYPE_CONFORMANCE", + "ARTIFACT_TYPE_OTHER" + ], + "description": "Type of the artifact." + }, + "componentDistributions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Distribution types this artifact applies to." + }, + "formats": { + "type": "array", + "items": { + "$ref": "artifact-format.schema.json" + }, + "minItems": 1, + "description": "Available formats for this artifact." + }, + "createdDate": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this artifact was created in the TEA system." + }, + "description": { + "type": "string", + "maxLength": 4096, + "description": "Optional description of the artifact." + }, + "subject": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "SUBJECT_TYPE_UNSPECIFIED", + "SUBJECT_TYPE_COMPONENT", + "SUBJECT_TYPE_PRODUCT", + "SUBJECT_TYPE_SERVICE", + "SUBJECT_TYPE_ORGANIZATION", + "SUBJECT_TYPE_BUILD" + ], + "description": "Type of subject." + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "../common/identifier.schema.json" + }, + "description": "Identifiers for the subject." + }, + "name": { + "type": "string", + "description": "Human-readable name of the subject." + }, + "version": { + "type": "string", + "description": "Version of the subject (if applicable)." + } + }, + "additionalProperties": false, + "description": "Subject of the artifact (what it describes)." + } + }, + "required": [ + "uuid", + "name", + "type", + "formats" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/collection/collection.schema.json b/schemas/json/collection/collection.schema.json new file mode 100644 index 0000000..7b194ec --- /dev/null +++ b/schemas/json/collection/collection.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/collection.schema.json", + "type": "object", + "title": "TEA Collection", + "description": "Represents a TEA Collection - a versioned set of artifacts associated with a release.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this TEA Collection." + }, + "version": { + "type": "integer", + "minimum": 1, + "description": "Version number of this collection." + }, + "date": { + "type": "string", + "format": "date-time", + "description": "Date when this collection version was created." + }, + "belongsTo": { + "type": "string", + "enum": [ + "COLLECTION_SCOPE_UNSPECIFIED", + "COLLECTION_SCOPE_RELEASE", + "COLLECTION_SCOPE_PRODUCT_RELEASE" + ], + "description": "Scope of the collection (component release or product release)." + }, + "updateReason": { + "$ref": "update-reason.schema.json" + }, + "artifacts": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "UUIDs of artifacts in this collection." + }, + "createdDate": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this collection was created." + }, + "modifiedDate": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this collection was last modified." + } + }, + "required": [ + "uuid", + "version", + "belongsTo", + "updateReason" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/collection/update-reason.schema.json b/schemas/json/collection/update-reason.schema.json new file mode 100644 index 0000000..25db210 --- /dev/null +++ b/schemas/json/collection/update-reason.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/update-reason.schema.json", + "type": "string", + "title": "TEA Update Reason", + "description": "Reason for updating a collection version.", + "enum": [ + "UPDATE_REASON_TYPE_UNSPECIFIED", + "UPDATE_REASON_TYPE_INITIAL_RELEASE", + "UPDATE_REASON_TYPE_VEX_UPDATED", + "UPDATE_REASON_TYPE_ARTIFACT_UPDATED", + "UPDATE_REASON_TYPE_ARTIFACT_REMOVED", + "UPDATE_REASON_TYPE_ARTIFACT_ADDED" + ] +} \ No newline at end of file diff --git a/schemas/json/common/checksum.schema.json b/schemas/json/common/checksum.schema.json new file mode 100644 index 0000000..1382c4d --- /dev/null +++ b/schemas/json/common/checksum.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/checksum.schema.json", + "type": "object", + "title": "TEA Checksum", + "description": "Represents a cryptographic hash of content for integrity verification.", + "properties": { + "algType": { + "type": "string", + "enum": [ + "CHECKSUM_ALGORITHM_UNSPECIFIED", + "CHECKSUM_ALGORITHM_MD5", + "CHECKSUM_ALGORITHM_SHA1", + "CHECKSUM_ALGORITHM_SHA256", + "CHECKSUM_ALGORITHM_SHA384", + "CHECKSUM_ALGORITHM_SHA512", + "CHECKSUM_ALGORITHM_SHA3_256", + "CHECKSUM_ALGORITHM_SHA3_384", + "CHECKSUM_ALGORITHM_SHA3_512", + "CHECKSUM_ALGORITHM_BLAKE2B_256", + "CHECKSUM_ALGORITHM_BLAKE2B_384", + "CHECKSUM_ALGORITHM_BLAKE2B_512", + "CHECKSUM_ALGORITHM_BLAKE3" + ], + "description": "Algorithm used to compute the checksum." + }, + "algValue": { + "type": "string", + "pattern": "^[a-f0-9]+$", + "minLength": 32, + "maxLength": 256, + "description": "Hexadecimal-encoded checksum value (lowercase)." + } + }, + "required": [ + "algType", + "algValue" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/common/error.schema.json b/schemas/json/common/error.schema.json new file mode 100644 index 0000000..2615c65 --- /dev/null +++ b/schemas/json/common/error.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/error.schema.json", + "type": "object", + "title": "TEA Error", + "description": "Standard error response format for TEA APIs.", + "properties": { + "code": { + "type": "string", + "enum": [ + "ERROR_CODE_UNSPECIFIED", + "ERROR_CODE_INVALID_ARGUMENT", + "ERROR_CODE_UNAUTHENTICATED", + "ERROR_CODE_PERMISSION_DENIED", + "ERROR_CODE_NOT_FOUND", + "ERROR_CODE_ALREADY_EXISTS", + "ERROR_CODE_RESOURCE_EXHAUSTED", + "ERROR_CODE_CANCELLED", + "ERROR_CODE_INTERNAL", + "ERROR_CODE_UNAVAILABLE" + ], + "description": "Machine-readable error code." + }, + "message": { + "type": "string", + "description": "Human-readable error message." + }, + "field": { + "type": "string", + "description": "Field that caused the error (for validation errors)." + }, + "details": { + "type": "array", + "items": { + "$ref": "#/$defs/ErrorDetail" + }, + "description": "Detailed error information." + }, + "requestId": { + "type": "string", + "description": "Unique request ID for tracing." + }, + "documentationUrl": { + "type": "string", + "format": "uri", + "description": "Optional documentation URL for this error type." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": false, + "$defs": { + "ErrorDetail": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "ERROR_CODE_UNSPECIFIED", + "ERROR_CODE_INVALID_ARGUMENT", + "ERROR_CODE_UNAUTHENTICATED", + "ERROR_CODE_PERMISSION_DENIED", + "ERROR_CODE_NOT_FOUND", + "ERROR_CODE_ALREADY_EXISTS", + "ERROR_CODE_RESOURCE_EXHAUSTED", + "ERROR_CODE_CANCELLED", + "ERROR_CODE_INTERNAL", + "ERROR_CODE_UNAVAILABLE" + ] + }, + "message": { + "type": "string" + }, + "field": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/schemas/json/common/identifier.schema.json b/schemas/json/common/identifier.schema.json new file mode 100644 index 0000000..b20165b --- /dev/null +++ b/schemas/json/common/identifier.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/identifier.schema.json", + "type": "object", + "title": "TEA Identifier", + "description": "Represents a typed identifier for a TEA entity. Identifiers are immutable and globally unique within their type namespace.", + "properties": { + "idType": { + "type": "string", + "enum": [ + "IDENTIFIER_TYPE_UNSPECIFIED", + "IDENTIFIER_TYPE_TEI", + "IDENTIFIER_TYPE_PURL", + "IDENTIFIER_TYPE_CPE", + "IDENTIFIER_TYPE_SWID", + "IDENTIFIER_TYPE_GAV", + "IDENTIFIER_TYPE_GTIN", + "IDENTIFIER_TYPE_GMN", + "IDENTIFIER_TYPE_UDI", + "IDENTIFIER_TYPE_ASIN", + "IDENTIFIER_TYPE_HASH", + "IDENTIFIER_TYPE_CONFORMANCE" + ], + "description": "Type of the identifier." + }, + "idValue": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Value of the identifier in its canonical string form. Must conform to the format specification of the identifier type." + } + }, + "required": ["idType", "idValue"], + "additionalProperties": false +} diff --git a/schemas/json/common/pagination.schema.json b/schemas/json/common/pagination.schema.json new file mode 100644 index 0000000..4acbd8a --- /dev/null +++ b/schemas/json/common/pagination.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/pagination.schema.json", + "type": "object", + "title": "TEA Pagination", + "description": "Pagination parameters for list operations.", + "properties": { + "pageSize": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Maximum number of items to return per page. Server may return fewer items. Default is 20, maximum is 100." + }, + "pageToken": { + "type": "string", + "description": "Opaque token for fetching the next page of results. Obtained from PageResponse.next_page_token of a previous request. Omit for the first page." + }, + "nextPageToken": { + "type": "string", + "description": "Token to retrieve the next page of results. Empty if there are no more results." + }, + "totalCount": { + "type": "integer", + "minimum": 0, + "description": "Total number of items across all pages (if known). May be omitted if the total is expensive to compute." + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/component/component-release.schema.json b/schemas/json/component/component-release.schema.json new file mode 100644 index 0000000..97b0e85 --- /dev/null +++ b/schemas/json/component/component-release.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/component-release.schema.json", + "type": "object", + "title": "TEA Component Release", + "description": "Represents a TEA Component Release - a specific version of a component with associated distributions.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this TEA Component Release." + }, + "componentUuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the parent component." + }, + "version": { + "type": "string", + "minLength": 1, + "description": "Version string of this release." + }, + "releaseDate": { + "type": "string", + "format": "date-time", + "description": "Release date." + }, + "preRelease": { + "type": "boolean", + "description": "Whether this is a pre-release version." + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "../common/identifier.schema.json" + }, + "description": "List of identifiers for this release." + }, + "distributions": { + "type": "array", + "items": { + "$ref": "distribution.schema.json" + }, + "description": "Available distributions for this release." + } + }, + "required": [ + "uuid", + "componentUuid", + "version" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/component/component.schema.json b/schemas/json/component/component.schema.json new file mode 100644 index 0000000..66df2ae --- /dev/null +++ b/schemas/json/component/component.schema.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/component.schema.json", + "type": "object", + "title": "TEA Component", + "description": "Represents a TEA Component - a software or hardware element that can be released independently.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this TEA Component." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Human-readable name of the component." + }, + "description": { + "type": "string", + "maxLength": 4096, + "description": "Optional description of the component." + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "../common/identifier.schema.json" + }, + "description": "List of identifiers for this component." + }, + "componentType": { + "type": "string", + "enum": [ + "COMPONENT_TYPE_UNSPECIFIED", + "COMPONENT_TYPE_APPLICATION", + "COMPONENT_TYPE_FRAMEWORK", + "COMPONENT_TYPE_LIBRARY", + "COMPONENT_TYPE_CONTAINER", + "COMPONENT_TYPE_OPERATING_SYSTEM", + "COMPONENT_TYPE_DEVICE", + "COMPONENT_TYPE_FILE", + "COMPONENT_TYPE_FIRMWARE", + "COMPONENT_TYPE_OTHER" + ], + "description": "Type of the component." + }, + "licenses": { + "type": "array", + "items": { + "$ref": "license.schema.json" + }, + "description": "License information for the component." + }, + "publisher": { + "type": "string", + "description": "Name of the component publisher." + }, + "homepageUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL to the component's homepage." + }, + "vcsUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL to the component's VCS repository." + } + }, + "required": [ + "uuid", + "name" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/component/distribution.schema.json b/schemas/json/component/distribution.schema.json new file mode 100644 index 0000000..2cf1294 --- /dev/null +++ b/schemas/json/component/distribution.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/distribution.schema.json", + "type": "object", + "title": "TEA Distribution", + "description": "Represents a distribution channel or package format for a component release.", + "properties": { + "distributionType": { + "type": "string", + "minLength": 1, + "description": "Type of distribution (e.g., 'deb', 'rpm', 'maven', 'npm')." + }, + "description": { + "type": "string", + "description": "Human-readable description of this distribution." + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "../common/identifier.schema.json" + }, + "description": "Identifiers for this distribution." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL where this distribution can be downloaded." + }, + "signatureUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL for a detached digital signature." + }, + "checksums": { + "type": "array", + "items": { + "$ref": "../common/checksum.schema.json" + }, + "minItems": 1, + "description": "Checksums for integrity verification." + } + }, + "required": [ + "distributionType", + "checksums" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/component/license.schema.json b/schemas/json/component/license.schema.json new file mode 100644 index 0000000..46c0074 --- /dev/null +++ b/schemas/json/component/license.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/license.schema.json", + "type": "object", + "title": "TEA License Info", + "description": "Represents license information for a component.", + "properties": { + "licenseType": { + "type": "string", + "enum": [ + "LICENSE_TYPE_UNSPECIFIED", + "LICENSE_TYPE_SPDX", + "LICENSE_TYPE_OTHER" + ], + "description": "Type of license identifier." + }, + "licenseId": { + "type": "string", + "minLength": 1, + "description": "License identifier (SPDX license ID or custom)." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL to the full license text." + } + }, + "required": [ + "licenseType", + "licenseId" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/discovery/discovery-response.schema.json b/schemas/json/discovery/discovery-response.schema.json new file mode 100644 index 0000000..eab8cc9 --- /dev/null +++ b/schemas/json/discovery/discovery-response.schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/discovery-response.schema.json", + "type": "object", + "title": "TEA Discovery Response", + "description": "Response from TEI discovery and well-known endpoint queries.", + "oneOf": [ + { + "properties": { + "productReleaseUuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the product release that the TEI resolves to." + } + }, + "additionalProperties": false + }, + { + "properties": { + "schemaVersion": { + "type": "integer", + "const": 1, + "description": "Schema version for the TEA .well-known discovery document. Currently always 1." + }, + "endpoints": { + "type": "array", + "description": "List of available TEA service endpoints and their supported versions.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base URL of the TEA API endpoint (no trailing slash)." + }, + "versions": { + "type": "array", + "description": "Supported TEA API versions for this endpoint. Use with the /v{version} prefix when constructing requests.", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^\\d+\\.\\d+(?:\\.\\d+)?(?:-[0-9A-Za-z.-]+)?$", + "examples": [ + "0.1.0-beta.1", + "0.2.0-beta.2", + "1.0.0" + ], + "description": "TEA OpenAPI Spec Version identifier, conforms to SemVer 2.0 (https://semver.org/)." + } + }, + "priority": { + "type": "integer", + "minimum": 0, + "description": "Optional priority for load balancing or client selection. Lower values have higher priority." + }, + "description": { + "type": "string", + "description": "Optional human-readable description of this endpoint." + } + }, + "required": [ + "url", + "versions" + ], + "additionalProperties": false + } + } + }, + "required": [ + "schemaVersion", + "endpoints" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/schemas/json/product/component-ref.schema.json b/schemas/json/product/component-ref.schema.json new file mode 100644 index 0000000..3870ed8 --- /dev/null +++ b/schemas/json/product/component-ref.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/component-ref.schema.json", + "type": "object", + "title": "TEA Component Reference", + "description": "Reference to a component and its release within a product release.", + "properties": { + "componentUuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the referenced component." + }, + "releaseUuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the referenced component release." + } + }, + "required": [ + "componentUuid", + "releaseUuid" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/product/contact.schema.json b/schemas/json/product/contact.schema.json new file mode 100644 index 0000000..d38caf8 --- /dev/null +++ b/schemas/json/product/contact.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/contact.schema.json", + "type": "object", + "title": "TEA Contact", + "description": "Represents contact information for a vendor or organization.", + "properties": { + "type": { + "type": "string", + "enum": [ + "CONTACT_TYPE_UNSPECIFIED", + "CONTACT_TYPE_EMAIL", + "CONTACT_TYPE_PHONE", + "CONTACT_TYPE_URL", + "CONTACT_TYPE_OTHER" + ], + "description": "Type of contact information." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Contact value (email address, phone number, URL, etc.)." + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/product/product-release.schema.json b/schemas/json/product/product-release.schema.json new file mode 100644 index 0000000..33a9dea --- /dev/null +++ b/schemas/json/product/product-release.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/product-release.schema.json", + "type": "object", + "title": "TEA Product Release", + "description": "Represents a TEA Product Release - a specific version of a product with associated components.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this TEA Product Release." + }, + "productUuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the parent product." + }, + "version": { + "type": "string", + "minLength": 1, + "description": "Version string of this release." + }, + "releaseDate": { + "type": "string", + "format": "date-time", + "description": "Release date." + }, + "preRelease": { + "type": "boolean", + "description": "Whether this is a pre-release version." + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "../common/identifier.schema.json" + }, + "description": "List of identifiers for this release." + }, + "components": { + "type": "array", + "items": { + "$ref": "component-ref.schema.json" + }, + "description": "References to components included in this product release." + } + }, + "required": [ + "uuid", + "version" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/product/product.schema.json b/schemas/json/product/product.schema.json new file mode 100644 index 0000000..040bd77 --- /dev/null +++ b/schemas/json/product/product.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/product.schema.json", + "type": "object", + "title": "TEA Product", + "description": "Represents a TEA Product - an optional higher-level object that groups multiple Product Releases for a product line or family.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for this TEA Product." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Human-readable name of the product." + }, + "description": { + "type": "string", + "maxLength": 4096, + "description": "Optional description of the product." + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "../common/identifier.schema.json" + }, + "description": "List of identifiers for this product." + }, + "vendor": { + "$ref": "vendor.schema.json" + }, + "createdDate": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this product was created in the TEA system." + }, + "modifiedDate": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this product was last modified." + }, + "homepageUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL to the product's homepage." + }, + "documentationUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL to the product's documentation." + }, + "vcsUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL to the product's VCS repository." + } + }, + "required": [ + "uuid", + "name" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemas/json/product/vendor.schema.json b/schemas/json/product/vendor.schema.json new file mode 100644 index 0000000..64f5547 --- /dev/null +++ b/schemas/json/product/vendor.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cyclonedx.org/schema/tea/v1/vendor.schema.json", + "type": "object", + "title": "TEA Vendor", + "description": "Represents a product vendor or publisher.", + "properties": { + "name": { + "type": "string", + "maxLength": 512, + "description": "Vendor name." + }, + "uuid": { + "type": "string", + "format": "uuid", + "description": "Optional vendor UUID (if registered in this TEA instance)." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional vendor URL." + }, + "contacts": { + "type": "array", + "items": { + "$ref": "contact.schema.json" + }, + "description": "Optional contact information." + } + }, + "additionalProperties": false +} \ No newline at end of file