diff --git a/.gitattributes b/.gitattributes index a2363ebf92..279878b8e4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ cmd/build.go export-subst *.go diff=golang +Documentation/reference/api.md linguist-generated diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f031ede90a..736ccb2be3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,11 +38,6 @@ jobs: printf '::error file=%s,title=Bad Filename::Disallowed character in file name\n' "$file" done exit $(git ls-files -- ':/:*[<>:"|?*]*' | wc -l) - - name: Check API Reference - if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }} - run: | - npx widdershins --search false --language_tabs 'python:Python' 'go:Golang' 'javascript:Javascript' --summary ./openapi.yaml -o ./Documentation/reference/api.md - git diff --exit-code - name: Check Container Versions if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }} run: | diff --git a/Documentation/SUMMARY.md b/Documentation/SUMMARY.md index 11da1cd4b8..8899452eb3 100644 --- a/Documentation/SUMMARY.md +++ b/Documentation/SUMMARY.md @@ -18,6 +18,7 @@ - [Building](./contribution/building.md) - [Commit Style](./contribution/commit_style.md) - [Releases](./contribution/releases.md) + - [OpenAPI](./contribution/openapi.md) - [Reference](./reference.md) - [Api](./reference/api.md) - [Clairctl](./reference/clairctl.md) diff --git a/Documentation/contribution.md b/Documentation/contribution.md index 1f6f321e44..eb64fb6aff 100644 --- a/Documentation/contribution.md +++ b/Documentation/contribution.md @@ -4,3 +4,4 @@ The following sections provides information on how to contribute to Clair. - [Building](./contribution/building.md) - [Commit Style](./contribution/commit_style.md) - [Releases](./contribution/releases.md) +- [OpenAPI](./contribution/openapi.md) diff --git a/Documentation/contribution/openapi.md b/Documentation/contribution/openapi.md new file mode 100644 index 0000000000..6ff3fbe3fc --- /dev/null +++ b/Documentation/contribution/openapi.md @@ -0,0 +1,40 @@ +# OpenAPI + +The [OpenAPI specification] has moved from `openapi.yaml` to the `openapi.json` +and `openapi.yaml` files in `httptransport/api/v1`. + +These files are autogenerated from files in `httptransport/api` and +`httptransport/types` via the `httptransport/api/openapi.zsh` script. This +script requires `zsh` to run and `sha256sum`, `git`, `jq`, `yq`, and `npx` +commands in `PATH`. + +## Modifications + +To modify the OpenAPI spec, edit the relevant `httptransport/api/*/openapi.jq` +file (a [`jq`] script) or the `httptransport/api/lib/oapi.jq` library as needed, +then run `go generate ./httptransport`. + +The `go generate` command also needs to be run if files in `httptransport/types` +are modified. + +## Script + +The `openapi.zsh` script works by: +- for each `v*` directory under `httptransport/api`: + - for all [JSON Schema] files in the corresponding `httptransport/types/v*` directory: + - lint the schema + - slip-steam examples from the corresponding `examples` file + - amalgamate all the files from the previous step + - run the `openapi.jq` script with `null` input + - merge the outputs of the previous two steps + - ~~validate and lint the OpenAPI spec~~[^note] + - generate a `yaml` representation + - write out a `sha256` checksum in [Etag] format + +[^note]: The OpenAPI spec _should_ also be validated or linted, but there's not a known tool that handles JSON Schema [reference resolution] correctly. + +[OpenAPI specification]: https://spec.openapis.org/oas/v3.1.0.html +[`jq`]: https://jqlang.org/manual/ +[JSON Schema]: https://json-schema.org/ +[Etag]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag +[reference resolution]: https://www.learnjsonschema.com/2020-12/core/ref/ diff --git a/Documentation/howto/api.md b/Documentation/howto/api.md index a922e561d2..f64f982b75 100644 --- a/Documentation/howto/api.md +++ b/Documentation/howto/api.md @@ -1,6 +1,6 @@ # API Definition -Clair provides its API definition via an OpenAPI specification. You can view our OpenAPI spec [here](https://raw.githubusercontent.com/quay/clair/main/openapi.yaml) +Clair provides its API definition via an OpenAPI specification. You can view our OpenAPI spec [here][openapi_v1]. The OpenAPI spec can be used in a variety of ways. * Generating http clients for your application @@ -11,3 +11,5 @@ The OpenAPI spec can be used in a variety of ways. See [Testing Clair](./testing.md) to learn how the local dev tooling starts a local swagger editor. This is handy for making changes to the spec in real time. See [API Reference](../reference/api.md) for a markdown rendered API reference. + +[openapi_v1]: https://github.com/quay/clair/tree/main/httptransport/api/v1 diff --git a/Documentation/reference/api.md b/Documentation/reference/api.md index ebc74ab43e..986fca82f0 100644 --- a/Documentation/reference/api.md +++ b/Documentation/reference/api.md @@ -1,5 +1,5 @@ --- -title: ClairV4 v1.1 +title: Clair Container Analyzer v1.2.0 language_tabs: - python: Python - go: Golang @@ -8,7 +8,8 @@ language_clients: - python: "" - go: "" - javascript: "" -toc_footers: [] +toc_footers: + - External documentation includes: [] search: false highlight_theme: darkula @@ -18,267 +19,30 @@ headingLevel: 2 -

ClairV4 v1.1

+

Clair Container Analyzer v1.2.0

> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. -ClairV4 is a set of cooperating microservices which scan, index, and match your container's content with known vulnerabilities. +Clair is a set of cooperating microservices which can index and match a container image's content with known vulnerabilities. -Email: Clair Team Web: Clair Team -License: Apache License 2.0 - -

Notifier

- -## DeleteNotification - - - -> Code samples - -```python -import requests -headers = { - 'Accept': 'application/json' -} - -r = requests.delete('/notifier/api/v1/notification/{notification_id}', headers = headers) - -print(r.json()) - -``` - -```go -package main - -import ( - "bytes" - "net/http" -) - -func main() { - - headers := map[string][]string{ - "Accept": []string{"application/json"}, - } - - data := bytes.NewBuffer([]byte{jsonReq}) - req, err := http.NewRequest("DELETE", "/notifier/api/v1/notification/{notification_id}", data) - req.Header = headers - - client := &http.Client{} - resp, err := client.Do(req) - // ... -} - -``` - -```javascript - -const headers = { - 'Accept':'application/json' -}; - -fetch('/notifier/api/v1/notification/{notification_id}', -{ - method: 'DELETE', - - headers: headers -}) -.then(function(res) { - return res.json(); -}).then(function(body) { - console.log(body); -}); - -``` - -`DELETE /notifier/api/v1/notification/{notification_id}` - -Issues a delete of the provided notification id and all associated notifications. After this delete clients will no longer be able to retrieve notifications. - -

Parameters

- -|Name|In|Type|Required|Description| -|---|---|---|---|---| -|notification_id|path|string|false|A notification ID returned by a callback| - -> Example responses - -> 400 Response - -```json -{ - "code": "string", - "message": "string" -} -``` - -

Responses

- -|Status|Meaning|Description|Schema| -|---|---|---|---| -|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| -|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[Error](#schemaerror)| -|405|[Method Not Allowed](https://tools.ietf.org/html/rfc7231#section-6.5.5)|Method Not Allowed|[Error](#schemaerror)| -|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error|[Error](#schemaerror)| - - - -## Retrieve a paginated result of notifications for the provided id. - - - -> Code samples - -```python -import requests -headers = { - 'Accept': 'application/json' -} - -r = requests.get('/notifier/api/v1/notification/{notification_id}', headers = headers) - -print(r.json()) - -``` - -```go -package main - -import ( - "bytes" - "net/http" -) - -func main() { - - headers := map[string][]string{ - "Accept": []string{"application/json"}, - } - - data := bytes.NewBuffer([]byte{jsonReq}) - req, err := http.NewRequest("GET", "/notifier/api/v1/notification/{notification_id}", data) - req.Header = headers - - client := &http.Client{} - resp, err := client.Do(req) - // ... -} - -``` - -```javascript - -const headers = { - 'Accept':'application/json' -}; - -fetch('/notifier/api/v1/notification/{notification_id}', -{ - method: 'GET', - - headers: headers -}) -.then(function(res) { - return res.json(); -}).then(function(body) { - console.log(body); -}); - -``` - -`GET /notifier/api/v1/notification/{notification_id}` - -By performing a GET with a notification_id as a path parameter, the client will retrieve a paginated response of notification objects. - -

Parameters

+**Note:** Any endpoints tagged "internal" are documented for completeness but are considered exempt from versioning. -|Name|In|Type|Required|Description| -|---|---|---|---|---| -|notification_id|path|string|false|A notification ID returned by a callback| -|page_size|query|int|false|The maximum number of notifications to deliver in a single page.| -|next|query|string|false|The next page to fetch via id. Typically this number is provided on initial response in the page.next field. The first GET request may omit this field.| - -> Example responses +Email: Clair Team Web: Clair Team +License: Apache License 2.0 -> 200 Response +# Authentication -```json -{ - "page": { - "size": 100, - "next": "1b4d0db2-e757-4150-bbbb-543658144205" - }, - "notifications": [ - { - "id": "5e4b387e-88d3-4364-86fd-063447a6fad2", - "manifest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", - "reason": "added", - "vulnerability": { - "name": "CVE-2009-5155", - "fixed_in_version": "v0.0.1", - "links": "http://link-to-advisory", - "description": "In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\"", - "normalized_severity": "Unknown", - "package": { - "id": "10", - "name": "libapt-pkg5.0", - "version": "1.6.11", - "kind": "binary", - "normalized_version": "", - "arch": "x86", - "module": "", - "cpe": "", - "source": { - "id": "9", - "name": "apt", - "version": "1.6.11", - "kind": "source", - "source": null - } - }, - "distribution": { - "id": "1", - "did": "ubuntu", - "name": "Ubuntu", - "version": "18.04.3 LTS (Bionic Beaver)", - "version_code_name": "bionic", - "version_id": "18.04", - "arch": "", - "cpe": "", - "pretty_name": "Ubuntu 18.04.3 LTS" - }, - "repository": { - "id": "string", - "name": "string", - "key": "string", - "uri": "string", - "cpe": "string" - } - } - } - ] -} -``` +- HTTP Authentication, scheme: bearer Clair's authentication scheme. -

Responses

+This is a [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signed with a configured pre-shared key containing an allowlisted `iss` claim. -|Status|Meaning|Description|Schema| -|---|---|---|---| -|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|A paginated list of notifications|[PagedNotifications](#schemapagednotifications)| -|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[Error](#schemaerror)| -|405|[Method Not Allowed](https://tools.ietf.org/html/rfc7231#section-6.5.5)|Method Not Allowed|[Error](#schemaerror)| -|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error|[Error](#schemaerror)| +

indexer

- +Indexer service endpoints. -

Indexer

+These are responsible for determining the contents of containers. -## Index the contents of a Manifest +## Index a Manifest @@ -287,8 +51,8 @@ This operation does not require authentication ```python import requests headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' + 'Content-Type': 'application/vnd.clair.manifest.v1+json', + 'Accept': 'application/vnd.clair.index_report.v1+json' } r = requests.post('/indexer/api/v1/index_report', headers = headers) @@ -308,8 +72,8 @@ import ( func main() { headers := map[string][]string{ - "Content-Type": []string{"application/json"}, - "Accept": []string{"application/json"}, + "Content-Type": []string{"application/vnd.clair.manifest.v1+json"}, + "Accept": []string{"application/vnd.clair.index_report.v1+json"}, } data := bytes.NewBuffer([]byte{jsonReq}) @@ -328,22 +92,19 @@ const inputBody = '{ "hash": "sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3", "layers": [ { - "hash": "sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3", + "hash": "sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36", "uri": "https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36", "headers": { - "property1": [ - "string" - ], - "property2": [ - "string" + "Authoriztion": [ + "Bearer hunter2" ] } } ] }'; const headers = { - 'Content-Type':'application/json', - 'Accept':'application/json' + 'Content-Type':'application/vnd.clair.manifest.v1+json', + 'Accept':'application/vnd.clair.index_report.v1+json' }; fetch('/indexer/api/v1/index_report', @@ -371,14 +132,11 @@ By submitting a Manifest object to this endpoint Clair will fetch the layers, sc "hash": "sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3", "layers": [ { - "hash": "sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3", + "hash": "sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36", "uri": "https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36", "headers": { - "property1": [ - "string" - ], - "property2": [ - "string" + "Authoriztion": [ + "Bearer hunter2" ] } } @@ -386,11 +144,11 @@ By submitting a Manifest object to this endpoint Clair will fetch the layers, sc } ``` -

Parameters

+

Parameters

|Name|In|Type|Required|Description| |---|---|---|---|---| -|body|body|[Manifest](#schemamanifest)|true|none| +|body|body|[manifest](#schemamanifest)|true|Manifest to index.| > Example responses @@ -398,68 +156,45 @@ By submitting a Manifest object to this endpoint Clair will fetch the layers, sc ```json { - "manifest_hash": "sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3", - "state": "IndexFinished", - "packages": { - "10": { - "id": "10", - "name": "libapt-pkg5.0", - "version": "1.6.11", - "kind": "binary", - "normalized_version": "", - "arch": "x86", - "module": "", - "cpe": "", - "source": { - "id": "9", - "name": "apt", - "version": "1.6.11", - "kind": "source", - "source": null - } - } - }, - "distributions": { - "1": { - "id": "1", - "did": "ubuntu", - "name": "Ubuntu", - "version": "18.04.3 LTS (Bionic Beaver)", - "version_code_name": "bionic", - "version_id": "18.04", - "arch": "", - "cpe": "", - "pretty_name": "Ubuntu 18.04.3 LTS" - } - }, - "environments": { - "10": [ - { - "package_db": "var/lib/dpkg/status", - "introduced_in": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", - "distribution_id": "1" - } - ] - }, + "manifest_hash": null, + "state": "string", + "err": "string", "success": true, - "err": "" + "packages": {}, + "distributions": {}, + "repository": {}, + "environments": { + "property1": [], + "property2": [] + } } ``` -

Responses

+

Responses

|Status|Meaning|Description|Schema| |---|---|---|---| -|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|IndexReport Created|[IndexReport](#schemaindexreport)| -|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[Error](#schemaerror)| -|405|[Method Not Allowed](https://tools.ietf.org/html/rfc7231#section-6.5.5)|Method Not Allowed|[Error](#schemaerror)| -|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error|[Error](#schemaerror)| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|IndexReport created. + +Clients may want to avoid reading the body if simply submitting the manifest for later vulnerability reporting.|[index_report](#schemaindex_report)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)| +|412|[Precondition Failed](https://tools.ietf.org/html/rfc7232#section-4.2)|Precondition Failed|None| +|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)| +|default|Default|Internal Server Error|[error](#schemaerror)| + +### Response Headers + +|Status|Header|Type|Format|Description| +|---|---|---|---|---| +|201|Location|string||HTTP [Location header](https://httpwg.org/specs/rfc9110.html#field.location)| +|201|Link|string||Web Linking [Link header](https://httpwg.org/specs/rfc8288.html#header)| -