Compare commits
17 Commits
master
...
release-2.
Author | SHA1 | Date |
---|---|---|
Jimmy Zelinskie | aa5729df66 | 6 years ago |
Henry Muru Paenga | a80ca551cf | 6 years ago |
Jimmy Zelinskie | ee1b3ff7f1 | 6 years ago |
Jean Michel MacKay | af7d890865 | 6 years ago |
Jimmy Zelinskie | 8bea33576d | 6 years ago |
Jimmy Zelinskie | 5d5c193089 | 6 years ago |
Nick Johns | 086f92a617 | 6 years ago |
Nick Johns | d1cadb4cdc | 6 years ago |
Jimmy Zelinskie | f882e1c210 | 6 years ago |
Jimmy Zelinskie | 47450faf88 | 6 years ago |
usr42 | ad98b97a6d | 6 years ago |
Brad Ison | 21715797c9 | 6 years ago |
Joe Ray | e650d58583 | 6 years ago |
Jimmy Zelinskie | f8a1359a60 | 7 years ago |
Jimmy Zelinskie | b2519a044a | 7 years ago |
Jimmy Zelinskie | 2453d67c36 | 7 years ago |
Jimmy Zelinskie | 4f8d6bee1e | 7 years ago |
@ -1,47 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: publish
|
||||
pull: default
|
||||
image: plugins/docker:18.09
|
||||
settings:
|
||||
registry: https://registry.nixaid.com
|
||||
repo: "registry.nixaid.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}"
|
||||
tags:
|
||||
- latest
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
# storage_path: /drone/docker
|
||||
# storage_driver: aufs
|
||||
# ipv6: false
|
||||
# debug: true
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
event:
|
||||
- push
|
||||
- tag
|
||||
|
||||
- name: notify
|
||||
pull: default
|
||||
image: drillster/drone-email:latest
|
||||
settings:
|
||||
from: "Drone CI <noreply@nixaid.com>"
|
||||
host: mx.nixaid.com
|
||||
port: 587
|
||||
subject: "NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- tag
|
||||
status:
|
||||
- success
|
||||
- failure
|
@ -1,36 +0,0 @@
|
||||
<!--
|
||||
|
||||
The Clair developers only support the Clair API.
|
||||
They do not support or operate any third party client (e.g. clairctl, klar, harbor).
|
||||
If you are using a third party client, please create issues on their respective repositories.
|
||||
|
||||
Are you using a development build of Clair (e.g. quay.io/coreos/clair-git)?
|
||||
Your problem might be solved by switching to a stable release (e.g. quay.io/coreos/clair).
|
||||
|
||||
Issues that do not contain the Environment section will be automatically closed.
|
||||
If you're making a feature request, please specify "N/A" under the environment section.
|
||||
Nobody can help you without context.
|
||||
|
||||
-->
|
||||
|
||||
### Description of Problem / Feature Request
|
||||
|
||||
<!--- your content here --->
|
||||
|
||||
### Expected Outcome
|
||||
|
||||
<!--- your content here --->
|
||||
|
||||
### Actual Outcome
|
||||
|
||||
<!--- your content here --->
|
||||
|
||||
### Environment
|
||||
|
||||
- Clair version/image:
|
||||
- Clair client name/version:
|
||||
- Host OS:
|
||||
- Kernel (e.g. `uname -a`):
|
||||
- Kubernetes version (use `kubectl version`):
|
||||
- Helm version (use `helm version`):
|
||||
- Network/Firewall setup:
|
@ -1,4 +0,0 @@
|
||||
comment: "This issue is closed because it does not meet our issue template. Please read it."
|
||||
issueConfigs:
|
||||
- content:
|
||||
- "### Environment"
|
@ -1,12 +0,0 @@
|
||||
daysUntilStale: 60
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- lifecycle/preserve
|
||||
exemptProjects: true
|
||||
exemptMilestones: true
|
||||
staleLabel: lifecycle/stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
limitPerRun: 30
|
@ -0,0 +1,695 @@
|
||||
# Clair v1 API
|
||||
|
||||
- [Error Handling](#error-handling)
|
||||
- [Layers](#layers)
|
||||
- [POST](#post-layers)
|
||||
- [GET](#get-layersname)
|
||||
- [DELETE](#delete-layersname)
|
||||
- [Namespaces](#namespaces)
|
||||
- [GET](#get-namespaces)
|
||||
- [Vulnerabilities](#vulnerabilities)
|
||||
- [List](#get-namespacesnsnamevulnerabilities)
|
||||
- [POST](#post-namespacesnamevulnerabilities)
|
||||
- [GET](#get-namespacesnsnamevulnerabilitiesvulnname)
|
||||
- [PUT](#put-namespacesnsnamevulnerabilitiesvulnname)
|
||||
- [DELETE](#delete-namespacesnsnamevulnerabilitiesvulnname)
|
||||
- [Fixes](#fixes)
|
||||
- [GET](#get-namespacesnsnamevulnerabilitiesvulnnamefixes)
|
||||
- [PUT](#put-namespacesnsnamevulnerabilitiesvulnnamefixesfeaturename)
|
||||
- [DELETE](#delete-namespacesnsnamevulnerabilitiesvulnnamefixesfeaturename)
|
||||
- [Notifications](#notifications)
|
||||
- [GET](#get-notificationsname)
|
||||
- [DELETE](#delete-notificationsname)
|
||||
|
||||
## Error Handling
|
||||
|
||||
#### Description
|
||||
|
||||
Every route can optionally provide an `Error` property on the response object.
|
||||
The HTTP status code of the response should indicate what type of failure occurred and how the client should react.
|
||||
|
||||
#### Client Retry Behavior
|
||||
|
||||
| Code | Name | Retry Behavior |
|
||||
|------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 400 | Bad Request | The body of the request invalid. The request either must be changed before being retried or depends on another request being processed before it. |
|
||||
| 404 | Not Found | The requested resource could not be found. The request must be changed before being retried. |
|
||||
| 422 | Unprocessable Entity | The request body is valid, but unsupported. This request should never be retried. |
|
||||
| 500 | Internal Server Error | The server encountered an error while processing the request. This request should be retried without change. |
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Error": {
|
||||
"Message": "example error message"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Layers
|
||||
|
||||
### POST /layers
|
||||
|
||||
#### Description
|
||||
|
||||
The POST route for the Layers resource performs the analysis of a Layer from the provided path.
|
||||
|
||||
In order to analyze a container image, this route has to be called for each layer that [composes](https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/) it, in the proper order and with the parent relationship specified. For instance, to analyze an image composed of three layers A->B->C, where A is the base layer (i.e. `FROM debian:jessie`), three API calls must be made to this route, from A to C. Also, when analyzing B, A must be set as the parent, then when analyzing C, B must be defined as the parent.
|
||||
|
||||
This request blocks for the entire duration of the downloading and indexing of the layer and displays the provided Layer with an updated `IndexByVersion` property.
|
||||
The Name field must be unique globally. Consequently, using the Blob digest describing the Layer content is not sufficient as Clair won't be able to differentiate two empty filesystem diffs that belong to two different image trees.
|
||||
The Authorization field is an optional value whose contents will fill the Authorization HTTP Header when requesting the layer via HTTP.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
POST http://localhost:6060/v1/layers HTTP/1.1
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Layer": {
|
||||
"Name": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
|
||||
"Path": "https://mystorage.com/layers/523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6/layer.tar",
|
||||
"Headers": {
|
||||
"Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE"
|
||||
},
|
||||
"ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
|
||||
"Format": "Docker"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Layer": {
|
||||
"Name": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
|
||||
"Path": "https://mystorage.com/layers/523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6/layer.tar",
|
||||
"Headers": {
|
||||
"Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE"
|
||||
},
|
||||
"ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
|
||||
"Format": "Docker",
|
||||
"IndexedByVersion": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /layers/`:name`
|
||||
|
||||
#### Description
|
||||
|
||||
The GET route for the Layers resource displays a Layer and optionally all of its features and vulnerabilities. For an image composed of three layers A->B->C, calling this route on the third layer (C) will returns all the features and vulnerabilities for the entire image, including the analysis data gathered from the parent layers (A, B). For instance, a feature (and its potential vulnerabilities) detected in the first layer (A) will be shown when querying the third layer (C). On the other hand, a feature detected in the first layer (A) but then removed in either following layers (B, C) will not appear.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|-----------------|------|----------|-------------------------------------------------------------------------------|
|
||||
| features | bool | optional | Displays the list of features indexed in this layer and all of its parents. |
|
||||
| vulnerabilities | bool | optional | Displays the list of vulnerabilities along with the features described above. |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/layers/17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52?features&vulnerabilities HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Layer": {
|
||||
"Name": "17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52",
|
||||
"NamespaceName": "debian:8",
|
||||
"ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
|
||||
"IndexedByVersion": 1,
|
||||
"Features": [
|
||||
{
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "8.23-4",
|
||||
"Vulnerabilities": [
|
||||
{
|
||||
"Name": "CVE-2014-9471",
|
||||
"NamespaceName": "debian:8",
|
||||
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
|
||||
"Severity": "Low",
|
||||
"FixedBy": "9.23-5"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /layers/`:name`
|
||||
|
||||
#### Description
|
||||
|
||||
The DELETE route for the Layers resource removes a Layer and all of its children from the database.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
DELETE http://localhost:6060/v1/layers/17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52 HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Server: clair
|
||||
```
|
||||
|
||||
|
||||
## Namespaces
|
||||
|
||||
### GET /namespaces
|
||||
|
||||
#### Description
|
||||
|
||||
The GET route for the Namespaces resource displays a list of namespaces currently being managed.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/namespaces HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Namespaces": [
|
||||
{ "Name": "debian:8" },
|
||||
{ "Name": "debian:9" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Vulnerabilities
|
||||
|
||||
### GET /namespaces/`:nsName`/vulnerabilities
|
||||
|
||||
#### Description
|
||||
|
||||
The GET route for the Vulnerabilities resource displays the vulnerabilities data for a given namespace.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|---------|------|----------|------------------------------------------------------------|
|
||||
| limit | int | required | Limits the amount of the vunlerabilities data for a given namespace. |
|
||||
| page | int | required | Displays the specific page of the vunlerabilities data for a given namespace. |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities?limit=2 HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Vulnerabilities": [
|
||||
{
|
||||
"Name": "CVE-1999-1332",
|
||||
"NamespaceName": "debian:8",
|
||||
"Description": "gzexe in the gzip package on Red Hat Linux 5.0 and earlier allows local users to overwrite files of other users via a symlink attack on a temporary file.",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-1999-1332",
|
||||
"Severity": "Low"
|
||||
},
|
||||
{
|
||||
"Name": "CVE-1999-1572",
|
||||
"NamespaceName": "debian:8",
|
||||
"Description": "cpio on FreeBSD 2.1.0, Debian GNU/Linux 3.0, and possibly other operating systems, uses a 0 umask when creating files using the -O (archive) or -F options, which creates the files with mode 0666 and allows local users to read or overwrite those files.",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-1999-1572",
|
||||
"Severity": "Low",
|
||||
"Metadata": {
|
||||
"NVD": {
|
||||
"CVSSv2": {
|
||||
"Score": 2.1,
|
||||
"Vectors": "AV:L/AC:L/Au:N/C:P/I:N"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"NextPage":"gAAAAABW1ABiOlm6KMDKYFE022bEy_IFJdm4ExxTNuJZMN0Eycn0Sut2tOH9bDB4EWGy5s6xwATUHiG-6JXXaU5U32sBs6_DmA=="
|
||||
}
|
||||
```
|
||||
|
||||
### POST /namespaces/`:name`/vulnerabilities
|
||||
|
||||
#### Description
|
||||
|
||||
The POST route for the Vulnerabilities resource creates a new Vulnerability.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
POST http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities HTTP/1.1
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-2014-9471",
|
||||
"NamespaceName": "debian:8",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
|
||||
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
|
||||
"Severity": "Low",
|
||||
"Metadata": {
|
||||
"NVD": {
|
||||
"CVSSv2": {
|
||||
"Score": 7.5,
|
||||
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FixedIn": [
|
||||
{
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "8.23-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-2014-9471",
|
||||
"NamespaceName": "debian:8",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
|
||||
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
|
||||
"Severity": "Low",
|
||||
"Metadata": {
|
||||
"NVD": {
|
||||
"CVSSv2": {
|
||||
"Score": 7.5,
|
||||
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FixedIn": [
|
||||
{
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "8.23-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /namespaces/`:nsName`/vulnerabilities/`:vulnName`
|
||||
|
||||
#### Description
|
||||
|
||||
The GET route for the Vulnerabilities resource displays the current data for a given vulnerability and optionally the features that fix it.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|---------|------|----------|------------------------------------------------------------|
|
||||
| fixedIn | bool | optional | Displays the list of features that fix this vulnerability. |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471?fixedIn HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-2014-9471",
|
||||
"NamespaceName": "debian:8",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
|
||||
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
|
||||
"Severity": "Low",
|
||||
"Metadata": {
|
||||
"NVD": {
|
||||
"CVSSv2": {
|
||||
"Score": 7.5,
|
||||
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FixedIn": [
|
||||
{
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "8.23-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /namespaces/`:nsName`/vulnerabilities/`:vulnName`
|
||||
|
||||
#### Description
|
||||
|
||||
The PUT route for the Vulnerabilities resource updates a given Vulnerability.
|
||||
The "FixedIn" property of the Vulnerability must be empty or missing.
|
||||
Fixes should be managed by the Fixes resource.
|
||||
If this vulnerability was inserted by a Fetcher, changes may be lost when the Fetcher updates.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
PUT http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-2014-9471",
|
||||
"NamespaceName": "debian:8",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
|
||||
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
|
||||
"Severity": "Low",
|
||||
"Metadata": {
|
||||
"NVD": {
|
||||
"CVSSv2": {
|
||||
"Score": 7.5,
|
||||
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-2014-9471",
|
||||
"NamespaceName": "debian:8",
|
||||
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
|
||||
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
|
||||
"Severity": "Low",
|
||||
"Metadata": {
|
||||
"NVD": {
|
||||
"CVSSv2": {
|
||||
"Score": 7.5,
|
||||
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### DELETE /namespaces/`:nsName`/vulnerabilities/`:vulnName`
|
||||
|
||||
#### Description
|
||||
|
||||
The DELETE route for the Vulnerabilities resource deletes a given Vulnerability.
|
||||
If this vulnerability was inserted by a Fetcher, it may be re-inserted when the Fetcher updates.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471 HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Server: clair
|
||||
```
|
||||
|
||||
## Fixes
|
||||
|
||||
### GET /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes
|
||||
|
||||
#### Description
|
||||
|
||||
The GET route for the Fixes resource displays the list of Features that fix the given Vulnerability.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Features": [
|
||||
{
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "8.23-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes/`:featureName`
|
||||
|
||||
#### Description
|
||||
|
||||
The PUT route for the Fixes resource updates a Feature that is the fix for a given Vulnerability.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
PUT http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes/coreutils HTTP/1.1
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Feature": {
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "4.24-9"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Feature": {
|
||||
"Name": "coreutils",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "4.24-9"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes/`:featureName`
|
||||
|
||||
#### Description
|
||||
|
||||
The DELETE route for the Fixes resource removes a Feature as fix for the given Vulnerability.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
DELETE http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes/coreutils
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Server: clair
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
### GET /notifications/`:name`
|
||||
|
||||
#### Description
|
||||
|
||||
The GET route for the Notifications resource displays a notification that a Vulnerability has been updated.
|
||||
This route supports simultaneous pagination for both the `Old` and `New` Vulnerabilities' `OrderedLayersIntroducingVulnerability` which can be extremely long.
|
||||
The `LayersIntroducingVulnerability` property is deprecated and will eventually be removed from the API.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|-------|--------|----------|---------------------------------------------------------------------------------------------------------------|
|
||||
| page | string | optional | Displays the specific page of the "LayersIntroducingVulnerability" property on New and Old vulnerabilities. |
|
||||
| limit | int | optional | Limits the amount of results in the "LayersIntroducingVulnerability" property on New and Old vulnerabilities. |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
GET http://localhost:6060/v1/notifications/ec45ec87-bfc8-4129-a1c3-d2b82622175a?limit=2 HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=utf-8
|
||||
Server: clair
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Notification": {
|
||||
"Name": "ec45ec87-bfc8-4129-a1c3-d2b82622175a",
|
||||
"Created": "1456247389",
|
||||
"Notified": "1456246708",
|
||||
"Limit": 2,
|
||||
"Page": "gAAAAABWzJaC2JCH6Apr_R1f2EkjGdibnrKOobTcYXBWl6t0Cw6Q04ENGIymB6XlZ3Zi0bYt2c-2cXe43fvsJ7ECZhZz4P8C8F9efr_SR0HPiejzQTuG0qAzeO8klogFfFjSz2peBvgP",
|
||||
"NextPage": "gAAAAABWzJaCTyr6QXP2aYsCwEZfWIkU2GkNplSMlTOhLJfiR3LorBv8QYgEIgyOvZRmHQEzJKvkI6TP2PkRczBkcD17GE89btaaKMqEX14yHDgyfQvdasW1tj3-5bBRt0esKi9ym5En",
|
||||
"New": {
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-TEST",
|
||||
"NamespaceName": "debian:8",
|
||||
"Description": "New CVE",
|
||||
"Severity": "Low",
|
||||
"FixedIn": [
|
||||
{
|
||||
"Name": "grep",
|
||||
"NamespaceName": "debian:8",
|
||||
"Version": "2.25"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OrderedLayersIntroducingVulnerability": [
|
||||
{
|
||||
"Index": 1,
|
||||
"LayerName": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6"
|
||||
},
|
||||
{
|
||||
"Index": 2,
|
||||
"LayerName": "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d"
|
||||
}
|
||||
],
|
||||
"LayersIntroducingVulnerability": [
|
||||
"523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
|
||||
"3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d182371916b12d"
|
||||
]
|
||||
},
|
||||
"Old": {
|
||||
"Vulnerability": {
|
||||
"Name": "CVE-TEST",
|
||||
"NamespaceName": "debian:8",
|
||||
"Description": "New CVE",
|
||||
"Severity": "Low",
|
||||
"FixedIn": []
|
||||
},
|
||||
"OrderedLayersIntroducingVulnerability": [
|
||||
{
|
||||
"Index": 1,
|
||||
"LayerName": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6"
|
||||
},
|
||||
{
|
||||
"Index": 2,
|
||||
"LayerName": "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d"
|
||||
}
|
||||
],
|
||||
"LayersIntroducingVulnerability": [
|
||||
"3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d",
|
||||
"523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /notifications/`:name`
|
||||
|
||||
#### Description
|
||||
|
||||
The delete route for the Notifications resource marks a Notification as read.
|
||||
If a notification is not marked as read, Clair will continue to notify the provided endpoints.
|
||||
The time at which this Notification was marked as read can be seen in the `Notified` property of the response GET route for Notification.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```http
|
||||
DELETE http://localhost:6060/v1/notification/ec45ec87-bfc8-4129-a1c3-d2b82622175a HTTP/1.1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Server: clair
|
||||
```
|
@ -1,93 +0,0 @@
|
||||
# Understanding drivers, their data sources, and creating your own
|
||||
|
||||
Clair is organized into many different software components all of which are dynamically registered at compile time.
|
||||
All of these components can be found in the `ext/` directory.
|
||||
|
||||
## Driver Types
|
||||
|
||||
| Driver Type | Functionality | Example |
|
||||
|--------------|------------------------------------------------------------------------------------|---------------|
|
||||
| featurefmt | parses features of a particular format out of a layer | apk |
|
||||
| featurens | identifies whether a particular namespaces is applicable to a layer | alpine 3.5 |
|
||||
| imagefmt | determines the location of the root filesystem location for a layer | docker |
|
||||
| notification | implements the transport used to notify of vulnerability changes | webhook |
|
||||
| versionfmt | parses and compares version strings | rpm |
|
||||
| vulnmdsrc | fetches vulnerability metadata and appends them to vulnerabilities being processed | nvd |
|
||||
| vulnsrc | fetches vulnerabilities for a set of namespaces | alpine-sec-db |
|
||||
|
||||
## Data Sources for the built-in drivers
|
||||
|
||||
| Data Source | Data Collected | Format | License |
|
||||
|------------------------------------|--------------------------------------------------------------------------|--------|-----------------|
|
||||
| [Debian Security Bug Tracker] | Debian 6, 7, 8, unstable namespaces | [dpkg] | [Debian] |
|
||||
| [Ubuntu CVE Tracker] | Ubuntu 12.04, 12.10, 13.04, 14.04, 14.10, 15.04, 15.10, 16.04 namespaces | [dpkg] | [GPLv2] |
|
||||
| [Red Hat Security Data] | CentOS 5, 6, 7 namespaces | [rpm] | [CVRF] |
|
||||
| [Oracle Linux Security Data] | Oracle Linux 5, 6, 7 namespaces | [rpm] | [CVRF] |
|
||||
| [Amazon Linux Security Advisories] | Amazon Linux 2018.03, 2 namespaces | [rpm] | [MIT-0] |
|
||||
| [SUSE OVAL Descriptions] | openSUSE, SUSE Linux Enterprise namespaces | [rpm] | [CC-BY-NC-4.0] |
|
||||
| [Alpine SecDB] | Alpine 3.3, Alpine 3.4, Alpine 3.5 namespaces | [apk] | [MIT] |
|
||||
| [NIST NVD] | Generic Vulnerability Metadata | N/A | [Public Domain] |
|
||||
|
||||
[Debian Security Bug Tracker]: https://security-tracker.debian.org/tracker
|
||||
[Ubuntu CVE Tracker]: https://launchpad.net/ubuntu-cve-tracker
|
||||
[Red Hat Security Data]: https://www.redhat.com/security/data/metrics
|
||||
[Oracle Linux Security Data]: https://linux.oracle.com/security/
|
||||
[SUSE OVAL Descriptions]: https://www.suse.com/de-de/support/security/oval/
|
||||
[Amazon Linux Security Advisories]: https://alas.aws.amazon.com/
|
||||
[NIST NVD]: https://nvd.nist.gov
|
||||
[dpkg]: https://en.wikipedia.org/wiki/dpkg
|
||||
[rpm]: http://www.rpm.org
|
||||
[Debian]: https://www.debian.org/license
|
||||
[GPLv2]: https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
|
||||
[CVRF]: http://www.icasi.org/cvrf-licensing/
|
||||
[Public Domain]: https://nvd.nist.gov/faq
|
||||
[Alpine SecDB]: http://git.alpinelinux.org/cgit/alpine-secdb/
|
||||
[apk]: http://git.alpinelinux.org/cgit/apk-tools/
|
||||
[MIT]: https://gist.github.com/jzelinskie/6da1e2da728424d88518be2adbd76979
|
||||
[MIT-0]: https://spdx.org/licenses/MIT-0.html
|
||||
[CC-BY-NC-4.0]: https://creativecommons.org/licenses/by-nc/4.0/]
|
||||
|
||||
## Adding new drivers
|
||||
|
||||
In order to allow programmers to add additional behavior, Clair follows a pattern that Go programmers may recognize from the standard `database/sql` library.
|
||||
Each Driver Type defines an interface that must be implemented by drivers.
|
||||
|
||||
```go
|
||||
type DriverInterface interface {
|
||||
Action() error
|
||||
}
|
||||
|
||||
func RegisterDriver(name, DriverInterface) { ... }
|
||||
```
|
||||
|
||||
Create a new Go package containing an implementation of the driver interface.
|
||||
In the source file that implements this custom interface, create an `init()` function that registers the driver.
|
||||
|
||||
```go
|
||||
func init() {
|
||||
drivers.RegisterDriver("mydrivername", myDriverImplementation{})
|
||||
}
|
||||
|
||||
// This line causes the Go compiler to enforce that myDriverImplementation
|
||||
// implements the the DriverInterface at compile time.
|
||||
var _ drivers.DriverInterface = myDriverImplementation{}
|
||||
|
||||
type myDriverImplementation struct{}
|
||||
|
||||
func (d myDriverImplementation) Action() error {
|
||||
fmt.Println("Hello, Clair!")
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
The final step is to import the new driver in `main.go` as `_` in order ensure that the `init()` function executes, thus registering your driver.
|
||||
|
||||
```go
|
||||
import (
|
||||
...
|
||||
|
||||
_ "github.com/you/yourclairdriver"
|
||||
)
|
||||
```
|
||||
|
||||
If you believe what you've created can benefit others outside of your organization, please consider open sourcing it and creating a pull request to get it included by default.
|
@ -1,24 +0,0 @@
|
||||
# Presentations
|
||||
|
||||
This document tracks projects that integrate with Clair. [Join the community](https://github.com/coreos/clair/), and help us keep the list up-to-date.
|
||||
|
||||
## Clair v3
|
||||
|
||||
Coming soon...
|
||||
|
||||
## Clair v2
|
||||
|
||||
| Title | Event | Video/Slides |
|
||||
|---------------------------------------------------------------------------|-------------------------------|--------------------------------------------------------|
|
||||
| Clair: The Container Image Security Analyzer | [ContainerDays Boston 2016] | [YouTube][CDB2016YouTube] [SlideShare][CDB2016Slides] |
|
||||
| Identifying Common Vulnerabilities and Exposures in Containers with Clair | [CoreOS Fest 2016] | [YouTube](https://www.youtube.com/watch?v=YDCa51BK2q0) |
|
||||
| Clair: A Container Image Security Analyzer | [Microservices NYC] | [YouTube](https://www.youtube.com/watch?v=ynwKi2yhIX4) |
|
||||
| Clair: A Container Image Security Analyzer | [Container Orchestration NYC] | [YouTube](https://www.youtube.com/watch?v=wTfCOUDNV_M) |
|
||||
|
||||
[ContainerDays Boston 2016]: http://dynamicinfradays.org/events/2016-boston/
|
||||
[CDB2016YouTube]: https://www.youtube.com/watch?v=Kri67PtPv6s
|
||||
[CDB2016Slides]: https://www.slideshare.net/CoreOS_Slides/clair-a-container-image-security-analyzer-61197704
|
||||
|
||||
[CoreOS Fest 2016]: https://coreos.com/fest/#2016
|
||||
[Microservices NYC]: https://www.meetup.com/Microservices-NYC/events/230023492/
|
||||
[Container Orchestration NYC]: https://www.meetup.com/Kubernetes-Cloud-Native-New-York/events/229779466/
|
@ -1,117 +0,0 @@
|
||||
# Running Clair
|
||||
|
||||
The following document outlines possible ways to deploy Clair both on your local machine and to a cluster of machines.
|
||||
|
||||
## Official Container Repositories
|
||||
|
||||
Clair is officially packaged and released as a container.
|
||||
|
||||
* [quay.io/coreos/clair] - Stable releases
|
||||
* [quay.io/coreos/clair-jwt] - Stable releases with an embedded instance of [jwtproxy]
|
||||
* [quay.io/coreos/clair-git] - Development releases
|
||||
|
||||
[quay.io/coreos/clair]: https://quay.io/repository/coreos/clair
|
||||
[jwtproxy]: https://github.com/coreos/jwtproxy
|
||||
[quay.io/coreos/clair-jwt]: https://quay.io/repository/coreos/clair-jwt
|
||||
[quay.io/coreos/clair-git]: https://quay.io/repository/coreos/clair-git
|
||||
|
||||
## Common Architecture
|
||||
|
||||
### Registry Integration
|
||||
|
||||
Clair can be integrated directly into a container registry such that the registry is responsible for interacting with Clair on behalf of the user.
|
||||
This type of setup avoids the manual scanning of images and creates a sensible location to which Clair's vulnerability notifications can be propagated.
|
||||
The registry can also be used for authorization to avoid sharing vulnerability information about images to which one might not have access.
|
||||
|
||||
![Simple Clair Diagram](https://cloud.githubusercontent.com/assets/343539/21630809/c1adfbd2-d202-11e6-9dfe-9024139d0a28.png)
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Clair can be integrated into a CI/CD pipeline such that when a container image is produced, the step after pushing the image to a registry is to compose a request for Clair to scan that particular image.
|
||||
This type of integration is more flexible, but relies on additional components to be setup in order to secure.
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
**NOTE:** These instructions demonstrate running HEAD and not stable versions.
|
||||
|
||||
The following are community supported instructions to run Clair in a variety of ways.
|
||||
A [PostgreSQL 9.4+] database instance is required for all instructions.
|
||||
|
||||
[PostgreSQL 9.4+]: https://www.postgresql.org
|
||||
|
||||
### Cluster
|
||||
|
||||
#### Kubernetes (Helm)
|
||||
|
||||
If you don't have a local Kubernetes cluster already, check out [minikube].
|
||||
This assumes you've already ran `helm init`, you have access to a currently running instance of Tiller and that you are running the latest version of helm.
|
||||
|
||||
[minikube]: https://github.com/kubernetes/minikube
|
||||
|
||||
```
|
||||
git clone https://github.com/coreos/clair
|
||||
cd clair/contrib/helm
|
||||
cp clair/values.yaml ~/my_custom_values.yaml
|
||||
vi ~/my_custom_values.yaml
|
||||
helm dependency update clair
|
||||
helm install clair -f ~/my_custom_values.yaml
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```sh
|
||||
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/contrib/compose/docker-compose.yml -o $HOME/docker-compose.yml
|
||||
$ mkdir $HOME/clair_config
|
||||
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $HOME/clair_config/config.yaml
|
||||
$ $EDITOR $HOME/clair_config/config.yaml # Edit database source to be postgresql://postgres:password@postgres:5432?sslmode=disable
|
||||
$ docker-compose -f $HOME/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Docker Compose may start Clair before Postgres which will raise an error.
|
||||
If this error is raised, manually execute `docker-compose start clair`.
|
||||
|
||||
#### Docker
|
||||
|
||||
```sh
|
||||
$ mkdir $PWD/clair_config
|
||||
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $PWD/clair_config/config.yaml
|
||||
$ docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
|
||||
$ docker run --net=host -d -p 6060-6061:6060-6061 -v $PWD/clair_config:/config quay.io/coreos/clair-git:latest -config=/config/config.yaml
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
To build Clair, you need the latest stable version of [Go] and a working [Go environment].
|
||||
In addition, Clair requires some additional binaries be installed on the system [$PATH] as runtime dependencies:
|
||||
|
||||
* [git]
|
||||
* [rpm]
|
||||
* [xz]
|
||||
|
||||
[Go]: https://github.com/golang/go/releases
|
||||
[Go environment]: https://golang.org/doc/code.html
|
||||
[git]: https://git-scm.com
|
||||
[rpm]: http://www.rpm.org
|
||||
[xz]: http://tukaani.org/xz
|
||||
[$PATH]: https://en.wikipedia.org/wiki/PATH_(variable)
|
||||
|
||||
```sh
|
||||
$ go get github.com/coreos/clair
|
||||
$ go install github.com/coreos/clair/cmd/clair
|
||||
$ $EDITOR config.yaml # Add the URI for your postgres database
|
||||
$ ./$GOPATH/bin/clair -config=config.yaml
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I just started up Clair and nothing appears to be working, what's the deal?
|
||||
|
||||
During the first run, Clair will bootstrap its database with vulnerability data from the configured data sources.
|
||||
It can take several minutes before the database has been fully populated, but once this data is stored in the database, subsequent updates will take far less time.
|
||||
|
||||
### I'm seeing Linux kernel vulnerabilities in my image, that doesn't make any sense since containers share the host kernel!
|
||||
|
||||
Many container base images using Linux distributions as a foundation will install dummy kernel packages that do nothing but satisfy their package manager's dependency requirements.
|
||||
The Clair developers have taken the stance that Clair should not filter results, providing the most accurate data as possible to user interfaces that can then apply filters that make sense for their users.
|
@ -1,15 +0,0 @@
|
||||
# Terminology
|
||||
|
||||
## Container
|
||||
|
||||
- *Container* - the execution of an image
|
||||
- *Image* - a set of tarballs that contain the filesystem contents and run-time metadata of a container
|
||||
- *Layer* - one of the tarballs used in the composition of an image, often expressed as a filesystem delta from another layer
|
||||
|
||||
## Specific to Clair
|
||||
|
||||
- *Ancestry* - the Clair-internal representation of an Image
|
||||
- *Feature* - anything that when present in a filesystem could be an indication of a *vulnerability* (e.g. the presence of a file or an installed software package)
|
||||
- *Feature Namespace* (featurens) - a context around *features* and *vulnerabilities* (e.g. an operating system or a programming language)
|
||||
- *Vulnerability Source* (vulnsrc) - the component of Clair that tracks upstream vulnerability data and imports them into Clair's database
|
||||
- *Vulnerability Metadata Source* (vulnmdsrc) - the component of Clair that tracks upstream vulnerability metadata and associates them with vulnerabilities in Clair's database
|
@ -1,145 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/featurefmt"
|
||||
"github.com/coreos/clair/ext/featurens"
|
||||
"github.com/coreos/clair/ext/imagefmt"
|
||||
)
|
||||
|
||||
// AnalyzeError represents an failure when analyzing layer or constructing
|
||||
// ancestry.
|
||||
type AnalyzeError string
|
||||
|
||||
func (e AnalyzeError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
var (
|
||||
// StorageError represents an analyze error caused by the storage
|
||||
StorageError = AnalyzeError("failed to query the database.")
|
||||
// RetrieveBlobError represents an analyze error caused by failure of
|
||||
// downloading or extracting layer blobs.
|
||||
RetrieveBlobError = AnalyzeError("failed to download layer blob.")
|
||||
// ExtractBlobError represents an analyzer error caused by failure of
|
||||
// extracting a layer blob by imagefmt.
|
||||
ExtractBlobError = AnalyzeError("failed to extract files from layer blob.")
|
||||
// FeatureDetectorError is an error caused by failure of feature listing by
|
||||
// featurefmt.
|
||||
FeatureDetectorError = AnalyzeError("failed to scan feature from layer blob files.")
|
||||
// NamespaceDetectorError is an error caused by failure of namespace
|
||||
// detection by featurens.
|
||||
NamespaceDetectorError = AnalyzeError("failed to scan namespace from layer blob files.")
|
||||
)
|
||||
|
||||
// AnalyzeLayer retrieves the clair layer with all extracted features and namespaces.
|
||||
// If a layer is already scanned by all enabled detectors in the Clair instance, it returns directly.
|
||||
// Otherwise, it re-download the layer blob and scan the features and namespaced again.
|
||||
func AnalyzeLayer(ctx context.Context, store database.Datastore, blobSha256 string, blobFormat string, downloadURI string, downloadHeaders map[string]string) (*database.Layer, error) {
|
||||
layer, found, err := database.FindLayerAndRollback(store, blobSha256)
|
||||
logFields := log.Fields{"layer.Hash": blobSha256}
|
||||
if err != nil {
|
||||
log.WithError(err).WithFields(logFields).Error("failed to find layer in the storage")
|
||||
return nil, StorageError
|
||||
}
|
||||
|
||||
var scannedBy []database.Detector
|
||||
if found {
|
||||
scannedBy = layer.By
|
||||
}
|
||||
|
||||
// layer will be scanned by detectors not scanned the layer already.
|
||||
toScan := database.DiffDetectors(EnabledDetectors(), scannedBy)
|
||||
if len(toScan) != 0 {
|
||||
log.WithFields(logFields).Debug("scan layer blob not already scanned")
|
||||
newLayerScanResult := &database.Layer{Hash: blobSha256, By: toScan}
|
||||
blob, err := retrieveLayerBlob(ctx, downloadURI, downloadHeaders)
|
||||
if err != nil {
|
||||
log.WithError(err).WithFields(logFields).Error("failed to retrieve layer blob")
|
||||
return nil, RetrieveBlobError
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := blob.Close(); err != nil {
|
||||
log.WithFields(logFields).Error("failed to close layer blob reader")
|
||||
}
|
||||
}()
|
||||
|
||||
files := append(featurefmt.RequiredFilenames(toScan), featurens.RequiredFilenames(toScan)...)
|
||||
fileMap, err := imagefmt.Extract(blobFormat, blob, files)
|
||||
if err != nil {
|
||||
log.WithFields(logFields).WithError(err).Error("failed to extract layer blob")
|
||||
return nil, ExtractBlobError
|
||||
}
|
||||
|
||||
newLayerScanResult.Features, err = featurefmt.ListFeatures(fileMap, toScan)
|
||||
if err != nil {
|
||||
log.WithFields(logFields).WithError(err).Error("failed to detect features")
|
||||
return nil, FeatureDetectorError
|
||||
}
|
||||
|
||||
newLayerScanResult.Namespaces, err = featurens.Detect(fileMap, toScan)
|
||||
if err != nil {
|
||||
log.WithFields(logFields).WithError(err).Error("failed to detect namespaces")
|
||||
return nil, NamespaceDetectorError
|
||||
}
|
||||
|
||||
if err = saveLayerChange(store, newLayerScanResult); err != nil {
|
||||
log.WithFields(logFields).WithError(err).Error("failed to store layer change")
|
||||
return nil, StorageError
|
||||
}
|
||||
|
||||
layer = database.MergeLayers(layer, newLayerScanResult)
|
||||
} else {
|
||||
log.WithFields(logFields).Debug("found scanned layer blob")
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
// EnabledDetectors retrieves a list of all detectors installed in the Clair
|
||||
// instance.
|
||||
func EnabledDetectors() []database.Detector {
|
||||
return append(featurefmt.ListListers(), featurens.ListDetectors()...)
|
||||
}
|
||||
|
||||
// RegisterConfiguredDetectors populates the database with registered detectors.
|
||||
func RegisterConfiguredDetectors(store database.Datastore) {
|
||||
if err := database.PersistDetectorsAndCommit(store, EnabledDetectors()); err != nil {
|
||||
panic("failed to initialize Clair analyzer")
|
||||
}
|
||||
}
|
||||
|
||||
func saveLayerChange(store database.Datastore, layer *database.Layer) error {
|
||||
if err := database.PersistFeaturesAndCommit(store, layer.GetFeatures()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := database.PersistNamespacesAndCommit(store, layer.GetNamespaces()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := database.PersistPartialLayerAndCommit(store, layer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,355 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
)
|
||||
|
||||
type layerIndexedFeature struct {
|
||||
Feature *database.LayerFeature
|
||||
Namespace *layerIndexedNamespace
|
||||
IntroducedIn int
|
||||
}
|
||||
|
||||
type layerIndexedNamespace struct {
|
||||
Namespace database.LayerNamespace `json:"namespace"`
|
||||
IntroducedIn int `json:"introducedIn"`
|
||||
}
|
||||
|
||||
// AncestryBuilder builds an Ancestry, which contains an ordered list of layers
|
||||
// and their features.
|
||||
type AncestryBuilder struct {
|
||||
layerIndex int
|
||||
layerNames []string
|
||||
detectors []database.Detector
|
||||
namespaces []layerIndexedNamespace // unique namespaces
|
||||
features map[database.Detector][]layerIndexedFeature
|
||||
}
|
||||
|
||||
// NewAncestryBuilder creates a new ancestry builder.
|
||||
//
|
||||
// ancestry builder takes in the extracted layer information and produce a set of
|
||||
// namespaces, features, and the relation between features for the whole image.
|
||||
func NewAncestryBuilder(detectors []database.Detector) *AncestryBuilder {
|
||||
return &AncestryBuilder{
|
||||
layerIndex: 0,
|
||||
detectors: detectors,
|
||||
namespaces: make([]layerIndexedNamespace, 0),
|
||||
features: make(map[database.Detector][]layerIndexedFeature),
|
||||
}
|
||||
}
|
||||
|
||||
// AddLeafLayer adds a leaf layer to the ancestry builder, and computes the
|
||||
// namespaced features.
|
||||
func (b *AncestryBuilder) AddLeafLayer(layer *database.Layer) {
|
||||
b.layerNames = append(b.layerNames, layer.Hash)
|
||||
for i := range layer.Namespaces {
|
||||
b.updateNamespace(&layer.Namespaces[i])
|
||||
}
|
||||
|
||||
allFeatureMap := map[database.Detector][]database.LayerFeature{}
|
||||
for i := range layer.Features {
|
||||
layerFeature := layer.Features[i]
|
||||
allFeatureMap[layerFeature.By] = append(allFeatureMap[layerFeature.By], layerFeature)
|
||||
}
|
||||
|
||||
// we only care about the ones specified by builder's detectors
|
||||
featureMap := map[database.Detector][]database.LayerFeature{}
|
||||
for i := range b.detectors {
|
||||
detector := b.detectors[i]
|
||||
featureMap[detector] = allFeatureMap[detector]
|
||||
}
|
||||
|
||||
for detector := range featureMap {
|
||||
b.addLayerFeatures(detector, featureMap[detector])
|
||||
}
|
||||
|
||||
b.layerIndex++
|
||||
}
|
||||
|
||||
// Every detector inspects a set of files for the features
|
||||
// therefore, if that set of files gives a different set of features, it
|
||||
// should replace the existing features.
|
||||
func (b *AncestryBuilder) addLayerFeatures(detector database.Detector, features []database.LayerFeature) {
|
||||
if len(features) == 0 {
|
||||
// TODO(sidac): we need to differentiate if the detector finds that all
|
||||
// features are removed ( a file change ), or the package installer is
|
||||
// removed ( a file deletion ), or there's no change in the file ( file
|
||||
// does not exist in the blob ) Right now, we're just assuming that no
|
||||
// change in the file because that's the most common case.
|
||||
return
|
||||
}
|
||||
|
||||
existingFeatures := b.features[detector]
|
||||
currentFeatures := make([]layerIndexedFeature, 0, len(features))
|
||||
// Features that are not in the current layer should be removed.
|
||||
for i := range existingFeatures {
|
||||
feature := existingFeatures[i]
|
||||
for j := range features {
|
||||
if features[j] == *feature.Feature {
|
||||
currentFeatures = append(currentFeatures, feature)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Features that newly introduced in the current layer should be added.
|
||||
for i := range features {
|
||||
found := false
|
||||
for j := range existingFeatures {
|
||||
if *existingFeatures[j].Feature == features[i] {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
namespace, found := b.lookupNamespace(&features[i])
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
currentFeatures = append(currentFeatures, b.createLayerIndexedFeature(namespace, &features[i]))
|
||||
}
|
||||
}
|
||||
|
||||
b.features[detector] = currentFeatures
|
||||
}
|
||||
|
||||
// updateNamespace update the namespaces for the ancestry. It does the following things:
|
||||
// 1. when a detector detects a new namespace, it's added to the ancestry.
|
||||
// 2. when a detector detects a difference in the detected namespace, it
|
||||
// replaces the namespace, and also move all features under that namespace to
|
||||
// the new namespace.
|
||||
func (b *AncestryBuilder) updateNamespace(layerNamespace *database.LayerNamespace) {
|
||||
var (
|
||||
previous *layerIndexedNamespace
|
||||
foundUpgrade bool
|
||||
)
|
||||
|
||||
newNSNames := strings.Split(layerNamespace.Name, ":")
|
||||
if len(newNSNames) != 2 {
|
||||
log.Error("invalid namespace name")
|
||||
}
|
||||
|
||||
newNSName := newNSNames[0]
|
||||
newNSVersion := newNSNames[1]
|
||||
for i, ns := range b.namespaces {
|
||||
nsNames := strings.Split(ns.Namespace.Name, ":")
|
||||
if len(nsNames) != 2 {
|
||||
log.Error("invalid namespace name")
|
||||
continue
|
||||
}
|
||||
|
||||
nsName := nsNames[0]
|
||||
nsVersion := nsNames[1]
|
||||
if ns.Namespace.VersionFormat == layerNamespace.VersionFormat && nsName == newNSName {
|
||||
if nsVersion != newNSVersion {
|
||||
previous = &b.namespaces[i]
|
||||
foundUpgrade = true
|
||||
break
|
||||
} else {
|
||||
// not changed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we didn't found the namespace is a upgrade from another namespace, so we
|
||||
// simply add it.
|
||||
if !foundUpgrade {
|
||||
b.namespaces = append(b.namespaces, layerIndexedNamespace{
|
||||
Namespace: *layerNamespace,
|
||||
IntroducedIn: b.layerIndex,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// All features referencing to this namespace are now pointing to the new namespace.
|
||||
// Also those features are now treated as introduced in the same layer as
|
||||
// when this new namespace is introduced.
|
||||
previous.Namespace = *layerNamespace
|
||||
previous.IntroducedIn = b.layerIndex
|
||||
|
||||
for _, features := range b.features {
|
||||
for i, feature := range features {
|
||||
if feature.Namespace == previous {
|
||||
features[i].IntroducedIn = previous.IntroducedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *AncestryBuilder) createLayerIndexedFeature(namespace *layerIndexedNamespace, feature *database.LayerFeature) layerIndexedFeature {
|
||||
return layerIndexedFeature{
|
||||
Feature: feature,
|
||||
Namespace: namespace,
|
||||
IntroducedIn: b.layerIndex,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *AncestryBuilder) lookupNamespace(feature *database.LayerFeature) (*layerIndexedNamespace, bool) {
|
||||
matchedNamespaces := []*layerIndexedNamespace{}
|
||||
if feature.PotentialNamespace.Name != "" {
|
||||
a := &layerIndexedNamespace{
|
||||
Namespace: database.LayerNamespace{
|
||||
Namespace: feature.PotentialNamespace,
|
||||
},
|
||||
IntroducedIn: b.layerIndex,
|
||||
}
|
||||
matchedNamespaces = append(matchedNamespaces, a)
|
||||
} else {
|
||||
|
||||
for i, namespace := range b.namespaces {
|
||||
if namespace.Namespace.VersionFormat == feature.VersionFormat {
|
||||
matchedNamespaces = append(matchedNamespaces, &b.namespaces[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchedNamespaces) == 1 {
|
||||
return matchedNamespaces[0], true
|
||||
}
|
||||
|
||||
serialized, _ := json.Marshal(matchedNamespaces)
|
||||
fields := log.Fields{
|
||||
"feature.Name": feature.Name,
|
||||
"feature.VersionFormat": feature.VersionFormat,
|
||||
"ancestryBuilder.namespaces": string(serialized),
|
||||
}
|
||||
|
||||
if len(matchedNamespaces) > 1 {
|
||||
log.WithFields(fields).Warn("skip features with ambiguous namespaces")
|
||||
} else {
|
||||
log.WithFields(fields).Warn("skip features with no matching namespace")
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (b *AncestryBuilder) ancestryFeatures(index int) []database.AncestryFeature {
|
||||
ancestryFeatures := []database.AncestryFeature{}
|
||||
for detector, features := range b.features {
|
||||
for _, feature := range features {
|
||||
if feature.IntroducedIn == index {
|
||||
ancestryFeatures = append(ancestryFeatures, database.AncestryFeature{
|
||||
NamespacedFeature: database.NamespacedFeature{
|
||||
Feature: feature.Feature.Feature,
|
||||
Namespace: feature.Namespace.Namespace.Namespace,
|
||||
},
|
||||
FeatureBy: detector,
|
||||
NamespaceBy: feature.Namespace.Namespace.By,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ancestryFeatures
|
||||
}
|
||||
|
||||
func (b *AncestryBuilder) ancestryLayers() []database.AncestryLayer {
|
||||
layers := make([]database.AncestryLayer, 0, b.layerIndex)
|
||||
for i := 0; i < b.layerIndex; i++ {
|
||||
layers = append(layers, database.AncestryLayer{
|
||||
Hash: b.layerNames[i],
|
||||
Features: b.ancestryFeatures(i),
|
||||
})
|
||||
}
|
||||
|
||||
return layers
|
||||
}
|
||||
|
||||
// Ancestry produces an Ancestry from the builder.
|
||||
func (b *AncestryBuilder) Ancestry(name string) *database.Ancestry {
|
||||
if name == "" {
|
||||
// TODO(sidac): we'll use the computed ancestry name in the future.
|
||||
// During the transition, it still requires the user to use the correct
|
||||
// ancestry name.
|
||||
name = ancestryName(b.layerNames)
|
||||
log.WithField("ancestry.Name", name).Warn("generated ancestry name since it's not specified")
|
||||
}
|
||||
|
||||
return &database.Ancestry{
|
||||
Name: name,
|
||||
By: b.detectors,
|
||||
Layers: b.ancestryLayers(),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveAncestry saves an ancestry to the datastore.
|
||||
func SaveAncestry(store database.Datastore, ancestry *database.Ancestry) error {
|
||||
log.WithField("ancestry.Name", ancestry.Name).Debug("saving ancestry")
|
||||
features := []database.NamespacedFeature{}
|
||||
for _, layer := range ancestry.Layers {
|
||||
features = append(features, layer.GetFeatures()...)
|
||||
}
|
||||
|
||||
if err := database.PersistNamespacedFeaturesAndCommit(store, features); err != nil {
|
||||
return StorageError
|
||||
}
|
||||
|
||||
if err := database.UpsertAncestryAndCommit(store, ancestry); err != nil {
|
||||
return StorageError
|
||||
}
|
||||
|
||||
if err := database.CacheRelatedVulnerabilityAndCommit(store, features); err != nil {
|
||||
return StorageError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAncestryCached checks if the ancestry is already cached in the database with the current set of detectors.
|
||||
func IsAncestryCached(store database.Datastore, name string, layerHashes []string) (bool, error) {
|
||||
if name == "" {
|
||||
// TODO(sidac): we'll use the computed ancestry name in the future.
|
||||
// During the transition, it still requires the user to use the correct
|
||||
// ancestry name.
|
||||
name = ancestryName(layerHashes)
|
||||
log.WithField("ancestry.Name", name).Warn("generated ancestry name since it's not specified")
|
||||
}
|
||||
|
||||
ancestry, found, err := database.FindAncestryAndRollback(store, name)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("ancestry.Name", name).Error("failed to query ancestry in database")
|
||||
return false, StorageError
|
||||
}
|
||||
|
||||
if found {
|
||||
if len(database.DiffDetectors(EnabledDetectors(), ancestry.By)) == 0 {
|
||||
log.WithField("ancestry.Name", name).Debug("found cached ancestry")
|
||||
} else {
|
||||
log.WithField("ancestry.Name", name).Debug("found outdated ancestry cache")
|
||||
}
|
||||
} else {
|
||||
log.WithField("ancestry.Name", name).Debug("ancestry not cached")
|
||||
}
|
||||
|
||||
return found && len(database.DiffDetectors(EnabledDetectors(), ancestry.By)) == 0, nil
|
||||
}
|
||||
|
||||
func ancestryName(layerHashes []string) string {
|
||||
tag := sha256.Sum256([]byte(strings.Join(layerHashes, ",")))
|
||||
return hex.EncodeToString(tag[:])
|
||||
}
|
@ -1,297 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
)
|
||||
|
||||
var (
|
||||
dpkg = database.NewFeatureDetector("dpkg", "1.0")
|
||||
rpm = database.NewFeatureDetector("rpm", "1.0")
|
||||
pip = database.NewFeatureDetector("pip", "1.0")
|
||||
python = database.NewNamespaceDetector("python", "1.0")
|
||||
osrelease = database.NewNamespaceDetector("os-release", "1.0")
|
||||
aptsources = database.NewNamespaceDetector("apt-sources", "1.0")
|
||||
ubuntu = *database.NewNamespace("ubuntu:14.04", "dpkg")
|
||||
ubuntu16 = *database.NewNamespace("ubuntu:16.04", "dpkg")
|
||||
rhel7 = *database.NewNamespace("cpe:/o:redhat:enterprise_linux:7::computenode", "rpm")
|
||||
debian = *database.NewNamespace("debian:7", "dpkg")
|
||||
python2 = *database.NewNamespace("python:2", "pip")
|
||||
sed = *database.NewSourcePackage("sed", "4.4-2", "dpkg")
|
||||
sedByRPM = *database.NewBinaryPackage("sed", "4.4-2", "rpm")
|
||||
sedBin = *database.NewBinaryPackage("sed", "4.4-2", "dpkg")
|
||||
tar = *database.NewBinaryPackage("tar", "1.29b-2", "dpkg")
|
||||
scipy = *database.NewSourcePackage("scipy", "3.0.0", "pip")
|
||||
|
||||
emptyNamespace = database.Namespace{}
|
||||
|
||||
detectors = []database.Detector{dpkg, osrelease, rpm}
|
||||
multinamespaceDetectors = []database.Detector{dpkg, osrelease, pip}
|
||||
)
|
||||
|
||||
type ancestryBuilder struct {
|
||||
ancestry *database.Ancestry
|
||||
}
|
||||
|
||||
func newAncestryBuilder(name string) *ancestryBuilder {
|
||||
return &ancestryBuilder{&database.Ancestry{Name: name}}
|
||||
}
|
||||
|
||||
func (b *ancestryBuilder) addDetectors(d ...database.Detector) *ancestryBuilder {
|
||||
b.ancestry.By = append(b.ancestry.By, d...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *ancestryBuilder) addLayer(hash string, f ...database.AncestryFeature) *ancestryBuilder {
|
||||
l := database.AncestryLayer{Hash: hash}
|
||||
l.Features = append(l.Features, f...)
|
||||
b.ancestry.Layers = append(b.ancestry.Layers, l)
|
||||
return b
|
||||
}
|
||||
|
||||
func ancestryFeature(namespace database.Namespace, feature database.Feature, nsBy database.Detector, fBy database.Detector) database.AncestryFeature {
|
||||
return database.AncestryFeature{
|
||||
NamespacedFeature: database.NamespacedFeature{feature, namespace},
|
||||
FeatureBy: fBy,
|
||||
NamespaceBy: nsBy,
|
||||
}
|
||||
}
|
||||
|
||||
// layerBuilder is for helping constructing the layer test artifacts.
|
||||
type layerBuilder struct {
|
||||
layer *database.Layer
|
||||
}
|
||||
|
||||
func newLayerBuilder(hash string) *layerBuilder {
|
||||
return &layerBuilder{&database.Layer{Hash: hash, By: detectors}}
|
||||
}
|
||||
|
||||
func newLayerBuilderWithoutDetector(hash string) *layerBuilder {
|
||||
return &layerBuilder{&database.Layer{Hash: hash}}
|
||||
}
|
||||
|
||||
func (b *layerBuilder) addDetectors(d ...database.Detector) *layerBuilder {
|
||||
b.layer.By = append(b.layer.By, d...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *layerBuilder) addNamespace(detector database.Detector, ns database.Namespace) *layerBuilder {
|
||||
b.layer.Namespaces = append(b.layer.Namespaces, database.LayerNamespace{
|
||||
Namespace: ns,
|
||||
By: detector,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *layerBuilder) addFeature(detector database.Detector, f database.Feature, ns database.Namespace) *layerBuilder {
|
||||
b.layer.Features = append(b.layer.Features, database.LayerFeature{
|
||||
Feature: f,
|
||||
By: detector,
|
||||
PotentialNamespace: ns,
|
||||
})
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
var testImage = []*database.Layer{
|
||||
// empty layer
|
||||
newLayerBuilder("0").layer,
|
||||
// ubuntu namespace
|
||||
newLayerBuilder("1").addNamespace(osrelease, ubuntu).layer,
|
||||
// install sed
|
||||
newLayerBuilder("2").addFeature(dpkg, sed, emptyNamespace).layer,
|
||||
// install tar
|
||||
newLayerBuilder("3").addFeature(dpkg, sed, emptyNamespace).addFeature(dpkg, tar, emptyNamespace).layer,
|
||||
// remove tar
|
||||
newLayerBuilder("4").addFeature(dpkg, sed, emptyNamespace).layer,
|
||||
// upgrade ubuntu
|
||||
newLayerBuilder("5").addNamespace(osrelease, ubuntu16).layer,
|
||||
// no change to the detectable files
|
||||
newLayerBuilder("6").layer,
|
||||
// change to the package installer database but no features are affected.
|
||||
newLayerBuilder("7").addFeature(dpkg, sed, emptyNamespace).layer,
|
||||
}
|
||||
|
||||
var invalidNamespace = []*database.Layer{
|
||||
// add package without namespace, this indicates that the namespace detector
|
||||
// could not detect the namespace.
|
||||
newLayerBuilder("0").addFeature(dpkg, sed, emptyNamespace).layer,
|
||||
}
|
||||
|
||||
var noMatchingNamespace = []*database.Layer{
|
||||
newLayerBuilder("0").addFeature(rpm, sedByRPM, emptyNamespace).addFeature(dpkg, sed, emptyNamespace).addNamespace(osrelease, ubuntu).layer,
|
||||
}
|
||||
|
||||
var multiplePackagesOnFirstLayer = []*database.Layer{
|
||||
newLayerBuilder("0").addFeature(dpkg, sed, emptyNamespace).addFeature(dpkg, tar, emptyNamespace).addFeature(dpkg, sedBin, emptyNamespace).addNamespace(osrelease, ubuntu16).layer,
|
||||
}
|
||||
|
||||
var twoNamespaceDetectorsWithSameResult = []*database.Layer{
|
||||
newLayerBuilderWithoutDetector("0").addDetectors(dpkg, aptsources, osrelease).addFeature(dpkg, sed, emptyNamespace).addNamespace(aptsources, ubuntu).addNamespace(osrelease, ubuntu).layer,
|
||||
}
|
||||
|
||||
var sameVersionFormatDiffName = []*database.Layer{
|
||||
newLayerBuilder("0").addFeature(dpkg, sed, emptyNamespace).addNamespace(aptsources, ubuntu).addNamespace(osrelease, debian).layer,
|
||||
}
|
||||
|
||||
var potentialFeatureNamespace = []*database.Layer{
|
||||
newLayerBuilder("0").addFeature(rpm, sed, rhel7).layer,
|
||||
}
|
||||
|
||||
func TestAddLayer(t *testing.T) {
|
||||
cases := []struct {
|
||||
title string
|
||||
image []*database.Layer
|
||||
nonDefaultDetectors []database.Detector
|
||||
expectedAncestry database.Ancestry
|
||||
}{
|
||||
{
|
||||
title: "empty image",
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{})).addDetectors(detectors...).ancestry,
|
||||
},
|
||||
{
|
||||
title: "empty layer",
|
||||
image: testImage[:1],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
|
||||
addLayer("0").ancestry,
|
||||
},
|
||||
{
|
||||
title: "ubuntu",
|
||||
image: testImage[:2],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").ancestry,
|
||||
},
|
||||
{
|
||||
title: "ubuntu install sed",
|
||||
image: testImage[:3],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").
|
||||
addLayer("2", ancestryFeature(ubuntu, sed, osrelease, dpkg)).ancestry,
|
||||
},
|
||||
{
|
||||
title: "ubuntu install tar",
|
||||
image: testImage[:4],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").
|
||||
addLayer("2", ancestryFeature(ubuntu, sed, osrelease, dpkg)).
|
||||
addLayer("3", ancestryFeature(ubuntu, tar, osrelease, dpkg)).ancestry,
|
||||
}, {
|
||||
title: "ubuntu uninstall tar",
|
||||
image: testImage[:5],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").
|
||||
addLayer("2", ancestryFeature(ubuntu, sed, osrelease, dpkg)).
|
||||
addLayer("3").
|
||||
addLayer("4").ancestry,
|
||||
}, {
|
||||
title: "ubuntu upgrade",
|
||||
image: testImage[:6],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4", "5"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").
|
||||
addLayer("2").
|
||||
addLayer("3").
|
||||
addLayer("4").
|
||||
addLayer("5", ancestryFeature(ubuntu16, sed, osrelease, dpkg)).ancestry,
|
||||
}, {
|
||||
title: "no change to the detectable files",
|
||||
image: testImage[:7],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4", "5", "6"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").
|
||||
addLayer("2").
|
||||
addLayer("3").
|
||||
addLayer("4").
|
||||
addLayer("5", ancestryFeature(ubuntu16, sed, osrelease, dpkg)).
|
||||
addLayer("6").ancestry,
|
||||
}, {
|
||||
title: "change to the package installer database but no features are affected.",
|
||||
image: testImage[:8],
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4", "5", "6", "7"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
addLayer("1").
|
||||
addLayer("2").
|
||||
addLayer("3").
|
||||
addLayer("4").
|
||||
addLayer("5", ancestryFeature(ubuntu16, sed, osrelease, dpkg)).
|
||||
addLayer("6").
|
||||
addLayer("7").ancestry,
|
||||
}, {
|
||||
title: "layers with features and namespace.",
|
||||
image: multiplePackagesOnFirstLayer,
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
|
||||
addLayer("0",
|
||||
ancestryFeature(ubuntu16, sed, osrelease, dpkg),
|
||||
ancestryFeature(ubuntu16, sedBin, osrelease, dpkg),
|
||||
ancestryFeature(ubuntu16, tar, osrelease, dpkg)).
|
||||
ancestry,
|
||||
}, {
|
||||
title: "two namespace detectors giving same namespace.",
|
||||
image: twoNamespaceDetectorsWithSameResult,
|
||||
nonDefaultDetectors: []database.Detector{osrelease, aptsources, dpkg},
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(osrelease, aptsources, dpkg).
|
||||
addLayer("0", ancestryFeature(ubuntu, sed, aptsources, dpkg)).
|
||||
ancestry,
|
||||
}, {
|
||||
title: "feature without namespace",
|
||||
image: invalidNamespace,
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
ancestry,
|
||||
}, {
|
||||
title: "two namespaces with the same version format but different names",
|
||||
image: sameVersionFormatDiffName,
|
||||
// failure of matching a namespace will result in the package not being added.
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
|
||||
addLayer("0").
|
||||
ancestry,
|
||||
}, {
|
||||
title: "noMatchingNamespace",
|
||||
image: noMatchingNamespace,
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).addLayer("0", ancestryFeature(ubuntu, sed, osrelease, dpkg)).ancestry,
|
||||
}, {
|
||||
title: "featureWithPotentialNamespace",
|
||||
image: potentialFeatureNamespace,
|
||||
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).addLayer("0", ancestryFeature(rhel7, sed, database.Detector{}, rpm)).ancestry,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
var builder *AncestryBuilder
|
||||
if len(test.nonDefaultDetectors) != 0 {
|
||||
builder = NewAncestryBuilder(test.nonDefaultDetectors)
|
||||
} else {
|
||||
builder = NewAncestryBuilder(detectors)
|
||||
}
|
||||
|
||||
for _, layer := range test.image {
|
||||
builder.AddLeafLayer(layer)
|
||||
}
|
||||
|
||||
ancestry := builder.Ancestry("")
|
||||
require.True(t, database.AssertAncestryEqual(t, &test.expectedAncestry, ancestry))
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,343 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fernet/fernet-go"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"Message,omitempty"`
|
||||
}
|
||||
|
||||
type Layer struct {
|
||||
Name string `json:"Name,omitempty"`
|
||||
NamespaceName string `json:"NamespaceName,omitempty"`
|
||||
Path string `json:"Path,omitempty"`
|
||||
Headers map[string]string `json:"Headers,omitempty"`
|
||||
ParentName string `json:"ParentName,omitempty"`
|
||||
Format string `json:"Format,omitempty"`
|
||||
IndexedByVersion int `json:"IndexedByVersion,omitempty"`
|
||||
Features []Feature `json:"Features,omitempty"`
|
||||
}
|
||||
|
||||
func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabilities bool) Layer {
|
||||
layer := Layer{
|
||||
Name: dbLayer.Name,
|
||||
IndexedByVersion: dbLayer.EngineVersion,
|
||||
}
|
||||
|
||||
if dbLayer.Parent != nil {
|
||||
layer.ParentName = dbLayer.Parent.Name
|
||||
}
|
||||
|
||||
if dbLayer.Namespace != nil {
|
||||
layer.NamespaceName = dbLayer.Namespace.Name
|
||||
}
|
||||
|
||||
if withFeatures || withVulnerabilities && dbLayer.Features != nil {
|
||||
for _, dbFeatureVersion := range dbLayer.Features {
|
||||
feature := Feature{
|
||||
Name: dbFeatureVersion.Feature.Name,
|
||||
NamespaceName: dbFeatureVersion.Feature.Namespace.Name,
|
||||
VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat,
|
||||
Version: dbFeatureVersion.Version,
|
||||
AddedBy: dbFeatureVersion.AddedBy.Name,
|
||||
}
|
||||
|
||||
for _, dbVuln := range dbFeatureVersion.AffectedBy {
|
||||
vuln := Vulnerability{
|
||||
Name: dbVuln.Name,
|
||||
NamespaceName: dbVuln.Namespace.Name,
|
||||
Description: dbVuln.Description,
|
||||
Link: dbVuln.Link,
|
||||
Severity: string(dbVuln.Severity),
|
||||
Metadata: dbVuln.Metadata,
|
||||
}
|
||||
|
||||
if dbVuln.FixedBy != versionfmt.MaxVersion {
|
||||
vuln.FixedBy = dbVuln.FixedBy
|
||||
}
|
||||
feature.Vulnerabilities = append(feature.Vulnerabilities, vuln)
|
||||
}
|
||||
layer.Features = append(layer.Features, feature)
|
||||
}
|
||||
}
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
type Namespace struct {
|
||||
Name string `json:"Name,omitempty"`
|
||||
VersionFormat string `json:"VersionFormat,omitempty"`
|
||||
}
|
||||
|
||||
type Vulnerability struct {
|
||||
Name string `json:"Name,omitempty"`
|
||||
NamespaceName string `json:"NamespaceName,omitempty"`
|
||||
Description string `json:"Description,omitempty"`
|
||||
Link string `json:"Link,omitempty"`
|
||||
Severity string `json:"Severity,omitempty"`
|
||||
Metadata map[string]interface{} `json:"Metadata,omitempty"`
|
||||
FixedBy string `json:"FixedBy,omitempty"`
|
||||
FixedIn []Feature `json:"FixedIn,omitempty"`
|
||||
}
|
||||
|
||||
func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) {
|
||||
severity, err := database.NewSeverity(v.Severity)
|
||||
if err != nil {
|
||||
return database.Vulnerability{}, err
|
||||
}
|
||||
|
||||
var dbFeatures []database.FeatureVersion
|
||||
for _, feature := range v.FixedIn {
|
||||
dbFeature, err := feature.DatabaseModel()
|
||||
if err != nil {
|
||||
return database.Vulnerability{}, err
|
||||
}
|
||||
|
||||
dbFeatures = append(dbFeatures, dbFeature)
|
||||
}
|
||||
|
||||
return database.Vulnerability{
|
||||
Name: v.Name,
|
||||
Namespace: database.Namespace{Name: v.NamespaceName},
|
||||
Description: v.Description,
|
||||
Link: v.Link,
|
||||
Severity: severity,
|
||||
Metadata: v.Metadata,
|
||||
FixedIn: dbFeatures,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn bool) Vulnerability {
|
||||
vuln := Vulnerability{
|
||||
Name: dbVuln.Name,
|
||||
NamespaceName: dbVuln.Namespace.Name,
|
||||
Description: dbVuln.Description,
|
||||
Link: dbVuln.Link,
|
||||
Severity: string(dbVuln.Severity),
|
||||
Metadata: dbVuln.Metadata,
|
||||
}
|
||||
|
||||
if withFixedIn {
|
||||
for _, dbFeatureVersion := range dbVuln.FixedIn {
|
||||
vuln.FixedIn = append(vuln.FixedIn, FeatureFromDatabaseModel(dbFeatureVersion))
|
||||
}
|
||||
}
|
||||
|
||||
return vuln
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
Name string `json:"Name,omitempty"`
|
||||
NamespaceName string `json:"NamespaceName,omitempty"`
|
||||
VersionFormat string `json:"VersionFormat,omitempty"`
|
||||
Version string `json:"Version,omitempty"`
|
||||
Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"`
|
||||
AddedBy string `json:"AddedBy,omitempty"`
|
||||
}
|
||||
|
||||
func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature {
|
||||
version := dbFeatureVersion.Version
|
||||
if version == versionfmt.MaxVersion {
|
||||
version = "None"
|
||||
}
|
||||
|
||||
return Feature{
|
||||
Name: dbFeatureVersion.Feature.Name,
|
||||
NamespaceName: dbFeatureVersion.Feature.Namespace.Name,
|
||||
VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat,
|
||||
Version: version,
|
||||
AddedBy: dbFeatureVersion.AddedBy.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func (f Feature) DatabaseModel() (fv database.FeatureVersion, err error) {
|
||||
var version string
|
||||
if f.Version == "None" {
|
||||
version = versionfmt.MaxVersion
|
||||
} else {
|
||||
err = versionfmt.Valid(f.VersionFormat, f.Version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
version = f.Version
|
||||
}
|
||||
|
||||
fv = database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Name: f.Name,
|
||||
Namespace: database.Namespace{
|
||||
Name: f.NamespaceName,
|
||||
VersionFormat: f.VersionFormat,
|
||||
},
|
||||
},
|
||||
Version: version,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Name string `json:"Name,omitempty"`
|
||||
Created string `json:"Created,omitempty"`
|
||||
Notified string `json:"Notified,omitempty"`
|
||||
Deleted string `json:"Deleted,omitempty"`
|
||||
Limit int `json:"Limit,omitempty"`
|
||||
Page string `json:"Page,omitempty"`
|
||||
NextPage string `json:"NextPage,omitempty"`
|
||||
Old *VulnerabilityWithLayers `json:"Old,omitempty"`
|
||||
New *VulnerabilityWithLayers `json:"New,omitempty"`
|
||||
}
|
||||
|
||||
func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotification, limit int, pageToken string, nextPage database.VulnerabilityNotificationPageNumber, key string) Notification {
|
||||
var oldVuln *VulnerabilityWithLayers
|
||||
if dbNotification.OldVulnerability != nil {
|
||||
v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.OldVulnerability)
|
||||
oldVuln = &v
|
||||
}
|
||||
|
||||
var newVuln *VulnerabilityWithLayers
|
||||
if dbNotification.NewVulnerability != nil {
|
||||
v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.NewVulnerability)
|
||||
newVuln = &v
|
||||
}
|
||||
|
||||
var nextPageStr string
|
||||
if nextPage != database.NoVulnerabilityNotificationPage {
|
||||
nextPageBytes, _ := tokenMarshal(nextPage, key)
|
||||
nextPageStr = string(nextPageBytes)
|
||||
}
|
||||
|
||||
var created, notified, deleted string
|
||||
if !dbNotification.Created.IsZero() {
|
||||
created = fmt.Sprintf("%d", dbNotification.Created.Unix())
|
||||
}
|
||||
if !dbNotification.Notified.IsZero() {
|
||||
notified = fmt.Sprintf("%d", dbNotification.Notified.Unix())
|
||||
}
|
||||
if !dbNotification.Deleted.IsZero() {
|
||||
deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix())
|
||||
}
|
||||
|
||||
// TODO(jzelinskie): implement "changed" key
|
||||
fmt.Println(dbNotification.Deleted.IsZero())
|
||||
return Notification{
|
||||
Name: dbNotification.Name,
|
||||
Created: created,
|
||||
Notified: notified,
|
||||
Deleted: deleted,
|
||||
Limit: limit,
|
||||
Page: pageToken,
|
||||
NextPage: nextPageStr,
|
||||
Old: oldVuln,
|
||||
New: newVuln,
|
||||
}
|
||||
}
|
||||
|
||||
type VulnerabilityWithLayers struct {
|
||||
Vulnerability *Vulnerability `json:"Vulnerability,omitempty"`
|
||||
|
||||
// This field is guaranteed to be in order only for pagination.
|
||||
// Indices from different notifications may not be comparable.
|
||||
OrderedLayersIntroducingVulnerability []OrderedLayerName `json:"OrderedLayersIntroducingVulnerability,omitempty"`
|
||||
|
||||
// This field is deprecated.
|
||||
LayersIntroducingVulnerability []string `json:"LayersIntroducingVulnerability,omitempty"`
|
||||
}
|
||||
|
||||
type OrderedLayerName struct {
|
||||
Index int `json:"Index"`
|
||||
LayerName string `json:"LayerName"`
|
||||
}
|
||||
|
||||
func VulnerabilityWithLayersFromDatabaseModel(dbVuln database.Vulnerability) VulnerabilityWithLayers {
|
||||
vuln := VulnerabilityFromDatabaseModel(dbVuln, true)
|
||||
|
||||
var layers []string
|
||||
var orderedLayers []OrderedLayerName
|
||||
for _, layer := range dbVuln.LayersIntroducingVulnerability {
|
||||
layers = append(layers, layer.Name)
|
||||
orderedLayers = append(orderedLayers, OrderedLayerName{
|
||||
Index: layer.ID,
|
||||
LayerName: layer.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return VulnerabilityWithLayers{
|
||||
Vulnerability: &vuln,
|
||||
OrderedLayersIntroducingVulnerability: orderedLayers,
|
||||
LayersIntroducingVulnerability: layers,
|
||||
}
|
||||
}
|
||||
|
||||
type LayerEnvelope struct {
|
||||
Layer *Layer `json:"Layer,omitempty"`
|
||||
Error *Error `json:"Error,omitempty"`
|
||||
}
|
||||
|
||||
type NamespaceEnvelope struct {
|
||||
Namespaces *[]Namespace `json:"Namespaces,omitempty"`
|
||||
Error *Error `json:"Error,omitempty"`
|
||||
}
|
||||
|
||||
type VulnerabilityEnvelope struct {
|
||||
Vulnerability *Vulnerability `json:"Vulnerability,omitempty"`
|
||||
Vulnerabilities *[]Vulnerability `json:"Vulnerabilities,omitempty"`
|
||||
NextPage string `json:"NextPage,omitempty"`
|
||||
Error *Error `json:"Error,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationEnvelope struct {
|
||||
Notification *Notification `json:"Notification,omitempty"`
|
||||
Error *Error `json:"Error,omitempty"`
|
||||
}
|
||||
|
||||
type FeatureEnvelope struct {
|
||||
Feature *Feature `json:"Feature,omitempty"`
|
||||
Features *[]Feature `json:"Features,omitempty"`
|
||||
Error *Error `json:"Error,omitempty"`
|
||||
}
|
||||
|
||||
func tokenUnmarshal(token string, key string, v interface{}) error {
|
||||
k, _ := fernet.DecodeKey(key)
|
||||
msg := fernet.VerifyAndDecrypt([]byte(token), time.Hour, []*fernet.Key{k})
|
||||
if msg == nil {
|
||||
return errors.New("invalid or expired pagination token")
|
||||
}
|
||||
|
||||
return json.NewDecoder(bytes.NewBuffer(msg)).Decode(&v)
|
||||
}
|
||||
|
||||
func tokenMarshal(v interface{}, key string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := json.NewEncoder(&buf).Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k, _ := fernet.DecodeKey(key)
|
||||
return fernet.EncryptAndSign(buf.Bytes(), k)
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
// Copyright 2015 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package v1 implements the first version of the Clair API.
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
)
|
||||
|
||||
var (
|
||||
promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "clair_api_response_duration_milliseconds",
|
||||
Help: "The duration of time it takes to receieve and write a response to an API request",
|
||||
Buckets: prometheus.ExponentialBuckets(9.375, 2, 10),
|
||||
}, []string{"route", "code"})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(promResponseDurationMilliseconds)
|
||||
}
|
||||
|
||||
type handler func(http.ResponseWriter, *http.Request, httprouter.Params, *context) (route string, status int)
|
||||
|
||||
func httpHandler(h handler, ctx *context) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
start := time.Now()
|
||||
route, status := h(w, r, p, ctx)
|
||||
statusStr := strconv.Itoa(status)
|
||||
if status == 0 {
|
||||
statusStr = "???"
|
||||
}
|
||||
|
||||
promResponseDurationMilliseconds.
|
||||
WithLabelValues(route, statusStr).
|
||||
Observe(float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond))
|
||||
|
||||
log.WithFields(log.Fields{"remote addr": r.RemoteAddr, "method": r.Method, "request uri": r.RequestURI, "status": statusStr, "elapsed time": time.Since(start)}).Info("Handled HTTP request")
|
||||
}
|
||||
}
|
||||
|
||||
type context struct {
|
||||
Store database.Datastore
|
||||
PaginationKey string
|
||||
}
|
||||
|
||||
// NewRouter creates an HTTP router for version 1 of the Clair API.
|
||||
func NewRouter(store database.Datastore, paginationKey string) *httprouter.Router {
|
||||
router := httprouter.New()
|
||||
ctx := &context{store, paginationKey}
|
||||
|
||||
// Layers
|
||||
router.POST("/layers", httpHandler(postLayer, ctx))
|
||||
router.GET("/layers/:layerName", httpHandler(getLayer, ctx))
|
||||
router.DELETE("/layers/:layerName", httpHandler(deleteLayer, ctx))
|
||||
|
||||
// Namespaces
|
||||
router.GET("/namespaces", httpHandler(getNamespaces, ctx))
|
||||
|
||||
// Vulnerabilities
|
||||
router.GET("/namespaces/:namespaceName/vulnerabilities", httpHandler(getVulnerabilities, ctx))
|
||||
router.POST("/namespaces/:namespaceName/vulnerabilities", httpHandler(postVulnerability, ctx))
|
||||
router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(getVulnerability, ctx))
|
||||
router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(putVulnerability, ctx))
|
||||
router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(deleteVulnerability, ctx))
|
||||
|
||||
// Fixes
|
||||
router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes", httpHandler(getFixes, ctx))
|
||||
router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", httpHandler(putFix, ctx))
|
||||
router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", httpHandler(deleteFix, ctx))
|
||||
|
||||
// Notifications
|
||||
router.GET("/notifications/:notificationName", httpHandler(getNotification, ctx))
|
||||
router.DELETE("/notifications/:notificationName", httpHandler(deleteNotification, ctx))
|
||||
|
||||
// Metrics
|
||||
router.GET("/metrics", httpHandler(getMetrics, ctx))
|
||||
|
||||
return router
|
||||
}
|
@ -0,0 +1,502 @@
|
||||
// Copyright 2015 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair"
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
"github.com/coreos/clair/pkg/tarutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// These are the route identifiers for prometheus.
|
||||
postLayerRoute = "v1/postLayer"
|
||||
getLayerRoute = "v1/getLayer"
|
||||
deleteLayerRoute = "v1/deleteLayer"
|
||||
getNamespacesRoute = "v1/getNamespaces"
|
||||
getVulnerabilitiesRoute = "v1/getVulnerabilities"
|
||||
postVulnerabilityRoute = "v1/postVulnerability"
|
||||
getVulnerabilityRoute = "v1/getVulnerability"
|
||||
putVulnerabilityRoute = "v1/putVulnerability"
|
||||
deleteVulnerabilityRoute = "v1/deleteVulnerability"
|
||||
getFixesRoute = "v1/getFixes"
|
||||
putFixRoute = "v1/putFix"
|
||||
deleteFixRoute = "v1/deleteFix"
|
||||
getNotificationRoute = "v1/getNotification"
|
||||
deleteNotificationRoute = "v1/deleteNotification"
|
||||
getMetricsRoute = "v1/getMetrics"
|
||||
|
||||
// maxBodySize restricts client request bodies to 1MiB.
|
||||
maxBodySize int64 = 1048576
|
||||
|
||||
// statusUnprocessableEntity represents the 422 (Unprocessable Entity) status code, which means
|
||||
// the server understands the content type of the request entity
|
||||
// (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the
|
||||
// request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was
|
||||
// unable to process the contained instructions.
|
||||
statusUnprocessableEntity = 422
|
||||
)
|
||||
|
||||
func decodeJSON(r *http.Request, v interface{}) error {
|
||||
defer r.Body.Close()
|
||||
return json.NewDecoder(io.LimitReader(r.Body, maxBodySize)).Decode(v)
|
||||
}
|
||||
|
||||
func writeResponse(w http.ResponseWriter, r *http.Request, status int, resp interface{}) {
|
||||
// Headers must be written before the response.
|
||||
header := w.Header()
|
||||
header.Set("Content-Type", "application/json;charset=utf-8")
|
||||
header.Set("Server", "clair")
|
||||
|
||||
// Gzip the response if the client supports it.
|
||||
var writer io.Writer = w
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
gzipWriter := gzip.NewWriter(w)
|
||||
defer gzipWriter.Close()
|
||||
writer = gzipWriter
|
||||
|
||||
header.Set("Content-Encoding", "gzip")
|
||||
}
|
||||
|
||||
// Write the response.
|
||||
w.WriteHeader(status)
|
||||
err := json.NewEncoder(writer).Encode(resp)
|
||||
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *json.MarshalerError, *json.UnsupportedTypeError, *json.UnsupportedValueError:
|
||||
panic("v1: failed to marshal response: " + err.Error())
|
||||
default:
|
||||
log.WithError(err).Warning("failed to write response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
request := LayerEnvelope{}
|
||||
err := decodeJSON(r, &request)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return postLayerRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
if request.Layer == nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{"failed to provide layer"}})
|
||||
return postLayerRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
err = clair.ProcessLayer(ctx.Store, request.Layer.Format, request.Layer.Name, request.Layer.ParentName, request.Layer.Path, request.Layer.Headers)
|
||||
if err != nil {
|
||||
if err == tarutil.ErrCouldNotExtract ||
|
||||
err == tarutil.ErrExtractedFileTooBig ||
|
||||
err == clair.ErrUnsupported {
|
||||
writeResponse(w, r, statusUnprocessableEntity, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return postLayerRoute, statusUnprocessableEntity
|
||||
}
|
||||
|
||||
if _, badreq := err.(*commonerr.ErrBadRequest); badreq {
|
||||
writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return postLayerRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return postLayerRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusCreated, LayerEnvelope{Layer: &Layer{
|
||||
Name: request.Layer.Name,
|
||||
ParentName: request.Layer.ParentName,
|
||||
Path: request.Layer.Path,
|
||||
Headers: request.Layer.Headers,
|
||||
Format: request.Layer.Format,
|
||||
IndexedByVersion: clair.Version,
|
||||
}})
|
||||
return postLayerRoute, http.StatusCreated
|
||||
}
|
||||
|
||||
func getLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
_, withFeatures := r.URL.Query()["features"]
|
||||
_, withVulnerabilities := r.URL.Query()["vulnerabilities"]
|
||||
|
||||
dbLayer, err := ctx.Store.FindLayer(p.ByName("layerName"), withFeatures, withVulnerabilities)
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return getLayerRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return getLayerRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
layer := LayerFromDatabaseModel(dbLayer, withFeatures, withVulnerabilities)
|
||||
|
||||
writeResponse(w, r, http.StatusOK, LayerEnvelope{Layer: &layer})
|
||||
return getLayerRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func deleteLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
err := ctx.Store.DeleteLayer(p.ByName("layerName"))
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteLayerRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteLayerRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return deleteLayerRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
dbNamespaces, err := ctx.Store.ListNamespaces()
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, NamespaceEnvelope{Error: &Error{err.Error()}})
|
||||
return getNamespacesRoute, http.StatusInternalServerError
|
||||
}
|
||||
var namespaces []Namespace
|
||||
for _, dbNamespace := range dbNamespaces {
|
||||
namespaces = append(namespaces, Namespace{
|
||||
Name: dbNamespace.Name,
|
||||
VersionFormat: dbNamespace.VersionFormat,
|
||||
})
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusOK, NamespaceEnvelope{Namespaces: &namespaces})
|
||||
return getNamespacesRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
query := r.URL.Query()
|
||||
|
||||
limitStrs, limitExists := query["limit"]
|
||||
if !limitExists {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"must provide limit query parameter"}})
|
||||
return getVulnerabilitiesRoute, http.StatusBadRequest
|
||||
}
|
||||
limit, err := strconv.Atoi(limitStrs[0])
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid limit format: " + err.Error()}})
|
||||
return getVulnerabilitiesRoute, http.StatusBadRequest
|
||||
} else if limit < 0 {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"limit value should not be less than zero"}})
|
||||
return getVulnerabilitiesRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
page := 0
|
||||
pageStrs, pageExists := query["page"]
|
||||
if pageExists {
|
||||
err = tokenUnmarshal(pageStrs[0], ctx.PaginationKey, &page)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
}
|
||||
|
||||
namespace := p.ByName("namespaceName")
|
||||
if namespace == "" {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"namespace should not be empty"}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(namespace, limit, page)
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return getVulnerabilityRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return getVulnerabilitiesRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
var vulns []Vulnerability
|
||||
for _, dbVuln := range dbVulns {
|
||||
vuln := VulnerabilityFromDatabaseModel(dbVuln, false)
|
||||
vulns = append(vulns, vuln)
|
||||
}
|
||||
|
||||
var nextPageStr string
|
||||
if nextPage != -1 {
|
||||
nextPageBytes, err := tokenMarshal(nextPage, ctx.PaginationKey)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
nextPageStr = string(nextPageBytes)
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns, NextPage: nextPageStr})
|
||||
return getVulnerabilitiesRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
request := VulnerabilityEnvelope{}
|
||||
err := decodeJSON(r, &request)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return postVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
if request.Vulnerability == nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to provide vulnerability"}})
|
||||
return postVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
vuln, err := request.Vulnerability.DatabaseModel()
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return postVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *commonerr.ErrBadRequest:
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return postVulnerabilityRoute, http.StatusBadRequest
|
||||
default:
|
||||
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return postVulnerabilityRoute, http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusCreated, VulnerabilityEnvelope{Vulnerability: request.Vulnerability})
|
||||
return postVulnerabilityRoute, http.StatusCreated
|
||||
}
|
||||
|
||||
func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
_, withFixedIn := r.URL.Query()["fixedIn"]
|
||||
|
||||
dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName"))
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return getVulnerabilityRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return getVulnerabilityRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
vuln := VulnerabilityFromDatabaseModel(dbVuln, withFixedIn)
|
||||
|
||||
writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerability: &vuln})
|
||||
return getVulnerabilityRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
request := VulnerabilityEnvelope{}
|
||||
err := decodeJSON(r, &request)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return putVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
if request.Vulnerability == nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to provide vulnerability"}})
|
||||
return putVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
if len(request.Vulnerability.FixedIn) != 0 {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"Vulnerability.FixedIn must be empty"}})
|
||||
return putVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
vuln, err := request.Vulnerability.DatabaseModel()
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return putVulnerabilityRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
vuln.Namespace.Name = p.ByName("namespaceName")
|
||||
vuln.Name = p.ByName("vulnerabilityName")
|
||||
|
||||
err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *commonerr.ErrBadRequest:
|
||||
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return putVulnerabilityRoute, http.StatusBadRequest
|
||||
default:
|
||||
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return putVulnerabilityRoute, http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerability: request.Vulnerability})
|
||||
return putVulnerabilityRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
err := ctx.Store.DeleteVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName"))
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteVulnerabilityRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteVulnerabilityRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return deleteVulnerabilityRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName"))
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return getFixesRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return getFixesRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
vuln := VulnerabilityFromDatabaseModel(dbVuln, true)
|
||||
writeResponse(w, r, http.StatusOK, FeatureEnvelope{Features: &vuln.FixedIn})
|
||||
return getFixesRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
request := FeatureEnvelope{}
|
||||
err := decodeJSON(r, &request)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return putFixRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
if request.Feature == nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"failed to provide feature"}})
|
||||
return putFixRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
if request.Feature.Name != p.ByName("fixName") {
|
||||
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"feature name in URL and JSON do not match"}})
|
||||
return putFixRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
dbFix, err := request.Feature.DatabaseModel()
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return putFixRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
err = ctx.Store.InsertVulnerabilityFixes(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), []database.FeatureVersion{dbFix})
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *commonerr.ErrBadRequest:
|
||||
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return putFixRoute, http.StatusBadRequest
|
||||
default:
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return putFixRoute, http.StatusNotFound
|
||||
}
|
||||
writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return putFixRoute, http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
writeResponse(w, r, http.StatusOK, FeatureEnvelope{Feature: request.Feature})
|
||||
return putFixRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func deleteFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
err := ctx.Store.DeleteVulnerabilityFix(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), p.ByName("fixName"))
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteFixRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteFixRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return deleteFixRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
query := r.URL.Query()
|
||||
|
||||
limitStrs, limitExists := query["limit"]
|
||||
if !limitExists {
|
||||
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"must provide limit query parameter"}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
limit, err := strconv.Atoi(limitStrs[0])
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid limit format: " + err.Error()}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
|
||||
var pageToken string
|
||||
page := database.VulnerabilityNotificationFirstPage
|
||||
pageStrs, pageExists := query["page"]
|
||||
if pageExists {
|
||||
err := tokenUnmarshal(pageStrs[0], ctx.PaginationKey, &page)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid page format: " + err.Error()}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
pageToken = pageStrs[0]
|
||||
} else {
|
||||
pageTokenBytes, err := tokenMarshal(page, ctx.PaginationKey)
|
||||
if err != nil {
|
||||
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}})
|
||||
return getNotificationRoute, http.StatusBadRequest
|
||||
}
|
||||
pageToken = string(pageTokenBytes)
|
||||
}
|
||||
|
||||
dbNotification, nextPage, err := ctx.Store.GetNotification(p.ByName("notificationName"), limit, page)
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, NotificationEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteNotificationRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, NotificationEnvelope{Error: &Error{err.Error()}})
|
||||
return getNotificationRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
notification := NotificationFromDatabaseModel(dbNotification, limit, pageToken, nextPage, ctx.PaginationKey)
|
||||
|
||||
writeResponse(w, r, http.StatusOK, NotificationEnvelope{Notification: ¬ification})
|
||||
return getNotificationRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func deleteNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
err := ctx.Store.DeleteNotification(p.ByName("notificationName"))
|
||||
if err == commonerr.ErrNotFound {
|
||||
writeResponse(w, r, http.StatusNotFound, NotificationEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteNotificationRoute, http.StatusNotFound
|
||||
} else if err != nil {
|
||||
writeResponse(w, r, http.StatusInternalServerError, NotificationEnvelope{Error: &Error{err.Error()}})
|
||||
return deleteNotificationRoute, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return deleteNotificationRoute, http.StatusOK
|
||||
}
|
||||
|
||||
func getMetrics(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
|
||||
prometheus.Handler().ServeHTTP(w, r)
|
||||
return getMetricsRoute, 0
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
FROM golang:alpine
|
||||
|
||||
RUN apk add --update --no-cache git bash protobuf-dev
|
||||
|
||||
RUN go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||
RUN go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||
RUN go get -u github.com/golang/protobuf/protoc-gen-go
|
File diff suppressed because it is too large
Load Diff
@ -1,442 +0,0 @@
|
||||
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
|
||||
// source: api/v3/clairpb/clair.proto
|
||||
|
||||
/*
|
||||
Package clairpb is a reverse proxy.
|
||||
|
||||
It translates gRPC into RESTful JSON APIs.
|
||||
*/
|
||||
package clairpb
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/utilities"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ codes.Code
|
||||
var _ io.Reader
|
||||
var _ status.Status
|
||||
var _ = runtime.String
|
||||
var _ = utilities.NewDoubleArray
|
||||
|
||||
func request_AncestryService_GetAncestry_0(ctx context.Context, marshaler runtime.Marshaler, client AncestryServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetAncestryRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["ancestry_name"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ancestry_name")
|
||||
}
|
||||
|
||||
protoReq.AncestryName, err = runtime.String(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ancestry_name", err)
|
||||
}
|
||||
|
||||
msg, err := client.GetAncestry(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_AncestryService_PostAncestry_0(ctx context.Context, marshaler runtime.Marshaler, client AncestryServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq PostAncestryRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if req.ContentLength > 0 {
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := client.PostAncestry(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_NotificationService_GetNotification_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
|
||||
)
|
||||
|
||||
func request_NotificationService_GetNotification_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetNotificationRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["name"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
||||
}
|
||||
|
||||
protoReq.Name, err = runtime.String(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
||||
}
|
||||
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_NotificationService_GetNotification_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.GetNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_NotificationService_MarkNotificationAsRead_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq MarkNotificationAsReadRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["name"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
||||
}
|
||||
|
||||
protoReq.Name, err = runtime.String(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
||||
}
|
||||
|
||||
msg, err := client.MarkNotificationAsRead(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_StatusService_GetStatus_0(ctx context.Context, marshaler runtime.Marshaler, client StatusServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetStatusRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := client.GetStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterAncestryServiceHandlerFromEndpoint is same as RegisterAncestryServiceHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterAncestryServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.Dial(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterAncestryServiceHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
// RegisterAncestryServiceHandler registers the http handlers for service AncestryService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over "conn".
|
||||
func RegisterAncestryServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
|
||||
return RegisterAncestryServiceHandlerClient(ctx, mux, NewAncestryServiceClient(conn))
|
||||
}
|
||||
|
||||
// RegisterAncestryServiceHandler registers the http handlers for service AncestryService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over the given implementation of "AncestryServiceClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AncestryServiceClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "AncestryServiceClient" to call the correct interceptors.
|
||||
func RegisterAncestryServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AncestryServiceClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_AncestryService_GetAncestry_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_AncestryService_GetAncestry_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_AncestryService_GetAncestry_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_AncestryService_PostAncestry_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_AncestryService_PostAncestry_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_AncestryService_PostAncestry_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_AncestryService_GetAncestry_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"ancestry", "ancestry_name"}, ""))
|
||||
|
||||
pattern_AncestryService_PostAncestry_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"ancestry"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_AncestryService_GetAncestry_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_AncestryService_PostAncestry_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
// RegisterNotificationServiceHandlerFromEndpoint is same as RegisterNotificationServiceHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterNotificationServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.Dial(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterNotificationServiceHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
// RegisterNotificationServiceHandler registers the http handlers for service NotificationService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over "conn".
|
||||
func RegisterNotificationServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
|
||||
return RegisterNotificationServiceHandlerClient(ctx, mux, NewNotificationServiceClient(conn))
|
||||
}
|
||||
|
||||
// RegisterNotificationServiceHandler registers the http handlers for service NotificationService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over the given implementation of "NotificationServiceClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "NotificationServiceClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "NotificationServiceClient" to call the correct interceptors.
|
||||
func RegisterNotificationServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client NotificationServiceClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_NotificationService_GetNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_NotificationService_GetNotification_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_NotificationService_GetNotification_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("DELETE", pattern_NotificationService_MarkNotificationAsRead_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_NotificationService_MarkNotificationAsRead_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_NotificationService_MarkNotificationAsRead_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_NotificationService_GetNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, ""))
|
||||
|
||||
pattern_NotificationService_MarkNotificationAsRead_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_NotificationService_GetNotification_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_NotificationService_MarkNotificationAsRead_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
// RegisterStatusServiceHandlerFromEndpoint is same as RegisterStatusServiceHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterStatusServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.Dial(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterStatusServiceHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
// RegisterStatusServiceHandler registers the http handlers for service StatusService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over "conn".
|
||||
func RegisterStatusServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
|
||||
return RegisterStatusServiceHandlerClient(ctx, mux, NewStatusServiceClient(conn))
|
||||
}
|
||||
|
||||
// RegisterStatusServiceHandler registers the http handlers for service StatusService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over the given implementation of "StatusServiceClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "StatusServiceClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "StatusServiceClient" to call the correct interceptors.
|
||||
func RegisterStatusServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client StatusServiceClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_StatusService_GetStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_StatusService_GetStatus_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_StatusService_GetStatus_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_StatusService_GetStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"status"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_StatusService_GetStatus_0 = runtime.ForwardResponseMessage
|
||||
)
|
@ -1,248 +0,0 @@
|
||||
// Copyright 2018 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
package coreos.clair;
|
||||
|
||||
option go_package = "clairpb";
|
||||
option java_package = "com.coreos.clair.pb";
|
||||
|
||||
service AncestryService {
|
||||
// The RPC used to read the results of scanning for a particular ancestry.
|
||||
rpc GetAncestry(GetAncestryRequest) returns (GetAncestryResponse) {
|
||||
option (google.api.http) = { get: "/ancestry/{ancestry_name}" };
|
||||
}
|
||||
// The RPC used to create a new scan of an ancestry.
|
||||
rpc PostAncestry(PostAncestryRequest) returns (PostAncestryResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/ancestry"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
service StatusService {
|
||||
// The RPC used to show the internal state of current Clair instance.
|
||||
rpc GetStatus(GetStatusRequest) returns (GetStatusResponse) {
|
||||
option (google.api.http) = { get: "/status" };
|
||||
}
|
||||
}
|
||||
|
||||
service NotificationService {
|
||||
// The RPC used to get a particularly Notification.
|
||||
rpc GetNotification(GetNotificationRequest) returns (GetNotificationResponse) {
|
||||
option (google.api.http) = { get: "/notifications/{name}" };
|
||||
}
|
||||
// The RPC used to mark a Notification as read after it has been processed.
|
||||
rpc MarkNotificationAsRead(MarkNotificationAsReadRequest) returns (MarkNotificationAsReadResponse) {
|
||||
option (google.api.http) = { delete: "/notifications/{name}" };
|
||||
}
|
||||
}
|
||||
|
||||
message Vulnerability {
|
||||
// The name of the vulnerability.
|
||||
string name = 1;
|
||||
// The name of the namespace in which the vulnerability was detected.
|
||||
string namespace_name = 2;
|
||||
// A description of the vulnerability according to the source for the namespace.
|
||||
string description = 3;
|
||||
// A link to the vulnerability according to the source for the namespace.
|
||||
string link = 4;
|
||||
// How dangerous the vulnerability is.
|
||||
string severity = 5;
|
||||
// Namespace agnostic metadata about the vulnerability.
|
||||
string metadata = 6;
|
||||
// The feature that fixes this vulnerability.
|
||||
// This field only exists when a vulnerability is a part of a Feature.
|
||||
string fixed_by = 7;
|
||||
// The Features that are affected by the vulnerability.
|
||||
// This field only exists when a vulnerability is a part of a Notification.
|
||||
repeated Feature affected_versions = 8;
|
||||
}
|
||||
|
||||
message Detector {
|
||||
enum DType {
|
||||
DETECTOR_D_TYPE_INVALID = 0;
|
||||
DETECTOR_D_TYPE_NAMESPACE = 1;
|
||||
DETECTOR_D_TYPE_FEATURE = 2;
|
||||
}
|
||||
// The name of the detector.
|
||||
string name = 1;
|
||||
// The version of the detector.
|
||||
string version = 2;
|
||||
// The type of the detector.
|
||||
DType dtype = 3;
|
||||
}
|
||||
|
||||
message Namespace {
|
||||
// The name of the namespace.
|
||||
string name = 1;
|
||||
// The detector used to detect the namespace. This only exists when present in
|
||||
// an Ancestry Feature.
|
||||
Detector detector = 2;
|
||||
}
|
||||
|
||||
message Feature {
|
||||
// The name of the feature.
|
||||
string name = 1;
|
||||
// The namespace in which the feature is detected.
|
||||
Namespace namespace = 2;
|
||||
// The specific version of this feature.
|
||||
string version = 3;
|
||||
// The format used to parse version numbers for the feature.
|
||||
string version_format = 4;
|
||||
// The detector used to detect this feature. This only exists when present in
|
||||
// an Ancestry.
|
||||
Detector detector = 5;
|
||||
// The list of vulnerabilities that affect the feature.
|
||||
repeated Vulnerability vulnerabilities = 6;
|
||||
// The feature type indicates if the feature represents a source package or
|
||||
// binary package.
|
||||
string feature_type = 7;
|
||||
}
|
||||
|
||||
message Layer {
|
||||
// The sha256 tarsum for the layer.
|
||||
string hash = 1;
|
||||
}
|
||||
|
||||
message ClairStatus {
|
||||
// The implemented detectors in this Clair instance
|
||||
repeated Detector detectors = 1;
|
||||
// The time at which the updater last ran.
|
||||
google.protobuf.Timestamp last_update_time = 2;
|
||||
}
|
||||
|
||||
message GetAncestryRequest {
|
||||
// The name of the desired ancestry.
|
||||
string ancestry_name = 1;
|
||||
}
|
||||
|
||||
message GetAncestryResponse {
|
||||
message AncestryLayer {
|
||||
// The layer's information.
|
||||
Layer layer = 1;
|
||||
// The features detected in this layer.
|
||||
repeated Feature detected_features = 2;
|
||||
}
|
||||
message Ancestry {
|
||||
// The name of the desired ancestry.
|
||||
string name = 1;
|
||||
// The detectors used to scan this Ancestry. It may not be the current set
|
||||
// of detectors in clair status.
|
||||
repeated Detector detectors = 2;
|
||||
// The list of layers along with detected features in each.
|
||||
repeated AncestryLayer layers = 3;
|
||||
}
|
||||
// The ancestry requested.
|
||||
Ancestry ancestry = 1;
|
||||
// The status of Clair at the time of the request.
|
||||
ClairStatus status = 2;
|
||||
}
|
||||
|
||||
message PostAncestryRequest {
|
||||
message PostLayer {
|
||||
// The hash of the layer.
|
||||
string hash = 1;
|
||||
// The location of the layer (URL or file path).
|
||||
string path = 2;
|
||||
// Any HTTP Headers that need to be used if requesting a layer over HTTP(S).
|
||||
map<string, string> headers = 3;
|
||||
}
|
||||
// The name of the ancestry being scanned.
|
||||
// If scanning OCI images, this should be the hash of the manifest.
|
||||
string ancestry_name = 1;
|
||||
// The format of the image being uploaded.
|
||||
string format = 2;
|
||||
// The layers to be scanned for this Ancestry, ordered in the way that i th
|
||||
// layer is the parent of i + 1 th layer.
|
||||
repeated PostLayer layers = 3;
|
||||
}
|
||||
|
||||
message PostAncestryResponse {
|
||||
// The status of Clair at the time of the request.
|
||||
ClairStatus status = 1;
|
||||
}
|
||||
|
||||
message GetNotificationRequest {
|
||||
// The current page of previous vulnerabilities for the ancestry.
|
||||
// This will be empty when it is the first page.
|
||||
string old_vulnerability_page = 1;
|
||||
// The current page of vulnerabilities for the ancestry.
|
||||
// This will be empty when it is the first page.
|
||||
string new_vulnerability_page = 2;
|
||||
// The requested maximum number of results per page.
|
||||
int32 limit = 3;
|
||||
// The name of the notification being requested.
|
||||
string name = 4;
|
||||
}
|
||||
|
||||
message GetNotificationResponse {
|
||||
message Notification {
|
||||
// The name of the requested notification.
|
||||
string name = 1;
|
||||
// The time at which the notification was created.
|
||||
string created = 2;
|
||||
// The time at which the notification was last sent out.
|
||||
string notified = 3;
|
||||
// The time at which a notification has been deleted.
|
||||
string deleted = 4;
|
||||
// The previous vulnerability and a paginated view of the ancestries it affects.
|
||||
PagedVulnerableAncestries old = 5;
|
||||
// The newly updated vulnerability and a paginated view of the ancestries it affects.
|
||||
PagedVulnerableAncestries new = 6;
|
||||
}
|
||||
// The notification as requested.
|
||||
Notification notification = 1;
|
||||
}
|
||||
|
||||
message PagedVulnerableAncestries {
|
||||
message IndexedAncestryName {
|
||||
// The index is an ever increasing number associated with the particular ancestry.
|
||||
// This is useful if you're processing notifications, and need to keep track of the progress of paginating the results.
|
||||
int32 index = 1;
|
||||
// The name of the ancestry.
|
||||
string name = 2;
|
||||
}
|
||||
// The identifier for the current page.
|
||||
string current_page = 1;
|
||||
// The token used to request the next page.
|
||||
// This will be empty when there are no more pages.
|
||||
string next_page = 2;
|
||||
// The requested maximum number of results per page.
|
||||
int32 limit = 3;
|
||||
// The vulnerability that affects a given set of ancestries.
|
||||
Vulnerability vulnerability = 4;
|
||||
// The ancestries affected by a vulnerability.
|
||||
repeated IndexedAncestryName ancestries = 5;
|
||||
}
|
||||
|
||||
message MarkNotificationAsReadRequest {
|
||||
// The name of the Notification that has been processed.
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message MarkNotificationAsReadResponse {}
|
||||
|
||||
message GetStatusRequest {}
|
||||
|
||||
message GetStatusResponse {
|
||||
// The status of the current Clair instance.
|
||||
ClairStatus status = 1;
|
||||
}
|
@ -1,495 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "api/v3/clairpb/clair.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {
|
||||
"/ancestry": {
|
||||
"post": {
|
||||
"summary": "The RPC used to create a new scan of an ancestry.",
|
||||
"operationId": "PostAncestry",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clairPostAncestryResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clairPostAncestryRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"AncestryService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ancestry/{ancestry_name}": {
|
||||
"get": {
|
||||
"summary": "The RPC used to read the results of scanning for a particular ancestry.",
|
||||
"operationId": "GetAncestry",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clairGetAncestryResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ancestry_name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"AncestryService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/notifications/{name}": {
|
||||
"get": {
|
||||
"summary": "The RPC used to get a particularly Notification.",
|
||||
"operationId": "GetNotification",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clairGetNotificationResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "old_vulnerability_page",
|
||||
"description": "The current page of previous vulnerabilities for the ancestry.\nThis will be empty when it is the first page.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "new_vulnerability_page",
|
||||
"description": "The current page of vulnerabilities for the ancestry.\nThis will be empty when it is the first page.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The requested maximum number of results per page.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"NotificationService"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"summary": "The RPC used to mark a Notification as read after it has been processed.",
|
||||
"operationId": "MarkNotificationAsRead",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clairMarkNotificationAsReadResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"NotificationService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/status": {
|
||||
"get": {
|
||||
"summary": "The RPC used to show the internal state of current Clair instance.",
|
||||
"operationId": "GetStatus",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clairGetStatusResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"StatusService"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DetectorDType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DETECTOR_D_TYPE_INVALID",
|
||||
"DETECTOR_D_TYPE_NAMESPACE",
|
||||
"DETECTOR_D_TYPE_FEATURE"
|
||||
],
|
||||
"default": "DETECTOR_D_TYPE_INVALID"
|
||||
},
|
||||
"GetAncestryResponseAncestry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the desired ancestry."
|
||||
},
|
||||
"detectors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/clairDetector"
|
||||
},
|
||||
"description": "The detectors used to scan this Ancestry. It may not be the current set\nof detectors in clair status."
|
||||
},
|
||||
"layers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/GetAncestryResponseAncestryLayer"
|
||||
},
|
||||
"description": "The list of layers along with detected features in each."
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetAncestryResponseAncestryLayer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"layer": {
|
||||
"$ref": "#/definitions/clairLayer",
|
||||
"description": "The layer's information."
|
||||
},
|
||||
"detected_features": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/clairFeature"
|
||||
},
|
||||
"description": "The features detected in this layer."
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetNotificationResponseNotification": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the requested notification."
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"description": "The time at which the notification was created."
|
||||
},
|
||||
"notified": {
|
||||
"type": "string",
|
||||
"description": "The time at which the notification was last sent out."
|
||||
},
|
||||
"deleted": {
|
||||
"type": "string",
|
||||
"description": "The time at which a notification has been deleted."
|
||||
},
|
||||
"old": {
|
||||
"$ref": "#/definitions/clairPagedVulnerableAncestries",
|
||||
"description": "The previous vulnerability and a paginated view of the ancestries it affects."
|
||||
},
|
||||
"new": {
|
||||
"$ref": "#/definitions/clairPagedVulnerableAncestries",
|
||||
"description": "The newly updated vulnerability and a paginated view of the ancestries it affects."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PagedVulnerableAncestriesIndexedAncestryName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The index is an ever increasing number associated with the particular ancestry.\nThis is useful if you're processing notifications, and need to keep track of the progress of paginating the results."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the ancestry."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PostAncestryRequestPostLayer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"description": "The hash of the layer."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The location of the layer (URL or filepath)."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Any HTTP Headers that need to be used if requesting a layer over HTTP(S)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairClairStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detectors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/clairDetector"
|
||||
},
|
||||
"title": "The implemented detectors in this Clair instance"
|
||||
},
|
||||
"last_update_time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The time at which the updater last ran."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairDetector": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the detector."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The version of the detector."
|
||||
},
|
||||
"dtype": {
|
||||
"$ref": "#/definitions/DetectorDType",
|
||||
"description": "The type of the detector."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairFeature": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the feature."
|
||||
},
|
||||
"namespace": {
|
||||
"$ref": "#/definitions/clairNamespace",
|
||||
"description": "The namespace in which the feature is detected."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The specific version of this feature."
|
||||
},
|
||||
"version_format": {
|
||||
"type": "string",
|
||||
"description": "The format used to parse version numbers for the feature."
|
||||
},
|
||||
"detector": {
|
||||
"$ref": "#/definitions/clairDetector",
|
||||
"description": "The detector used to detect this feature. This only exists when present in\nan Ancestry."
|
||||
},
|
||||
"vulnerabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/clairVulnerability"
|
||||
},
|
||||
"description": "The list of vulnerabilities that affect the feature."
|
||||
},
|
||||
"feature_type": {
|
||||
"type": "string",
|
||||
"description": "The feature type indicates if the feature represents a source package or\nbinary package."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairGetAncestryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ancestry": {
|
||||
"$ref": "#/definitions/GetAncestryResponseAncestry",
|
||||
"description": "The ancestry requested."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/clairClairStatus",
|
||||
"description": "The status of Clair at the time of the request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairGetNotificationResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"notification": {
|
||||
"$ref": "#/definitions/GetNotificationResponseNotification",
|
||||
"description": "The notification as requested."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairGetStatusResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/definitions/clairClairStatus",
|
||||
"description": "The status of the current Clair instance."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairLayer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"description": "The sha256 tarsum for the layer."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairMarkNotificationAsReadResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"clairNamespace": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the namespace."
|
||||
},
|
||||
"detector": {
|
||||
"$ref": "#/definitions/clairDetector",
|
||||
"description": "The detector used to detect the namespace. This only exists when present in\nan Ancestry Feature."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairPagedVulnerableAncestries": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current_page": {
|
||||
"type": "string",
|
||||
"description": "The identifier for the current page."
|
||||
},
|
||||
"next_page": {
|
||||
"type": "string",
|
||||
"description": "The token used to request the next page.\nThis will be empty when there are no more pages."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The requested maximum number of results per page."
|
||||
},
|
||||
"vulnerability": {
|
||||
"$ref": "#/definitions/clairVulnerability",
|
||||
"description": "The vulnerability that affects a given set of ancestries."
|
||||
},
|
||||
"ancestries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PagedVulnerableAncestriesIndexedAncestryName"
|
||||
},
|
||||
"description": "The ancestries affected by a vulnerability."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairPostAncestryRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ancestry_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the ancestry being scanned.\nIf scanning OCI images, this should be the hash of the manifest."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "The format of the image being uploaded."
|
||||
},
|
||||
"layers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PostAncestryRequestPostLayer"
|
||||
},
|
||||
"description": "The layers to be scanned for this Ancestry, ordered in the way that i th\nlayer is the parent of i + 1 th layer."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairPostAncestryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/definitions/clairClairStatus",
|
||||
"description": "The status of Clair at the time of the request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clairVulnerability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the vulnerability."
|
||||
},
|
||||
"namespace_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the namespace in which the vulnerability was detected."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of the vulnerability according to the source for the namespace."
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"description": "A link to the vulnerability according to the source for the namespace."
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"description": "How dangerous the vulnerability is."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"description": "Namespace agnostic metadata about the vulnerability."
|
||||
},
|
||||
"fixed_by": {
|
||||
"type": "string",
|
||||
"description": "The feature that fixes this vulnerability.\nThis field only exists when a vulnerability is a part of a Feature."
|
||||
},
|
||||
"affected_versions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/clairFeature"
|
||||
},
|
||||
"description": "The Features that are affected by the vulnerability.\nThis field only exists when a vulnerability is a part of a Notification."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clairpb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt"
|
||||
)
|
||||
|
||||
// DatabaseDetectorTypeMapping maps the database detector type to the integer
|
||||
// enum proto.
|
||||
var DatabaseDetectorTypeMapping = map[database.DetectorType]Detector_DType{
|
||||
database.NamespaceDetectorType: Detector_DType(1),
|
||||
database.FeatureDetectorType: Detector_DType(2),
|
||||
}
|
||||
|
||||
// PagedVulnerableAncestriesFromDatabaseModel converts database
|
||||
// PagedVulnerableAncestries to api PagedVulnerableAncestries and assigns
|
||||
// indexes to ancestries.
|
||||
func PagedVulnerableAncestriesFromDatabaseModel(dbVuln *database.PagedVulnerableAncestries) (*PagedVulnerableAncestries, error) {
|
||||
if dbVuln == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
vuln, err := VulnerabilityFromDatabaseModel(dbVuln.Vulnerability)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
next := ""
|
||||
if !dbVuln.End {
|
||||
next = string(dbVuln.Next)
|
||||
}
|
||||
|
||||
vulnAncestry := PagedVulnerableAncestries{
|
||||
Vulnerability: vuln,
|
||||
CurrentPage: string(dbVuln.Current),
|
||||
NextPage: next,
|
||||
Limit: int32(dbVuln.Limit),
|
||||
}
|
||||
|
||||
for index, ancestryName := range dbVuln.Affected {
|
||||
indexedAncestry := PagedVulnerableAncestries_IndexedAncestryName{
|
||||
Name: ancestryName,
|
||||
Index: int32(index),
|
||||
}
|
||||
vulnAncestry.Ancestries = append(vulnAncestry.Ancestries, &indexedAncestry)
|
||||
}
|
||||
|
||||
return &vulnAncestry, nil
|
||||
}
|
||||
|
||||
// NotificationFromDatabaseModel converts database notification, old and new
|
||||
// vulnerabilities' paged vulnerable ancestries to be api notification.
|
||||
func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotificationWithVulnerable) (*GetNotificationResponse_Notification, error) {
|
||||
var (
|
||||
noti GetNotificationResponse_Notification
|
||||
err error
|
||||
)
|
||||
|
||||
noti.Name = dbNotification.Name
|
||||
if !dbNotification.Created.IsZero() {
|
||||
noti.Created = fmt.Sprintf("%d", dbNotification.Created.Unix())
|
||||
}
|
||||
|
||||
if !dbNotification.Notified.IsZero() {
|
||||
noti.Notified = fmt.Sprintf("%d", dbNotification.Notified.Unix())
|
||||
}
|
||||
|
||||
if !dbNotification.Deleted.IsZero() {
|
||||
noti.Deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix())
|
||||
}
|
||||
|
||||
noti.Old, err = PagedVulnerableAncestriesFromDatabaseModel(dbNotification.Old)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
noti.New, err = PagedVulnerableAncestriesFromDatabaseModel(dbNotification.New)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ¬i, nil
|
||||
}
|
||||
|
||||
// VulnerabilityFromDatabaseModel converts database Vulnerability to api Vulnerability.
|
||||
func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability) (*Vulnerability, error) {
|
||||
metaString := ""
|
||||
if dbVuln.Metadata != nil {
|
||||
metadataByte, err := json.Marshal(dbVuln.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metaString = string(metadataByte)
|
||||
}
|
||||
|
||||
return &Vulnerability{
|
||||
Name: dbVuln.Name,
|
||||
NamespaceName: dbVuln.Namespace.Name,
|
||||
Description: dbVuln.Description,
|
||||
Link: dbVuln.Link,
|
||||
Severity: string(dbVuln.Severity),
|
||||
Metadata: metaString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VulnerabilityWithFixedInFromDatabaseModel converts database VulnerabilityWithFixedIn to api Vulnerability.
|
||||
func VulnerabilityWithFixedInFromDatabaseModel(dbVuln database.VulnerabilityWithFixedIn) (*Vulnerability, error) {
|
||||
vuln, err := VulnerabilityFromDatabaseModel(dbVuln.Vulnerability)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vuln.FixedBy = dbVuln.FixedInVersion
|
||||
return vuln, nil
|
||||
}
|
||||
|
||||
// NamespacedFeatureFromDatabaseModel converts database namespacedFeature to api Feature.
|
||||
func NamespacedFeatureFromDatabaseModel(feature database.AncestryFeature) *Feature {
|
||||
version := feature.Feature.Version
|
||||
if version == versionfmt.MaxVersion {
|
||||
version = "None"
|
||||
}
|
||||
|
||||
return &Feature{
|
||||
Name: feature.Feature.Name,
|
||||
Namespace: &Namespace{
|
||||
Name: feature.Namespace.Name,
|
||||
Detector: DetectorFromDatabaseModel(feature.NamespaceBy),
|
||||
},
|
||||
VersionFormat: feature.Namespace.VersionFormat,
|
||||
Version: version,
|
||||
Detector: DetectorFromDatabaseModel(feature.FeatureBy),
|
||||
FeatureType: string(feature.Type),
|
||||
}
|
||||
}
|
||||
|
||||
// DetectorFromDatabaseModel converts database detector to api detector.
|
||||
func DetectorFromDatabaseModel(detector database.Detector) *Detector {
|
||||
if !detector.Valid() {
|
||||
return nil
|
||||
}
|
||||
return &Detector{
|
||||
Name: detector.Name,
|
||||
Version: detector.Version,
|
||||
Dtype: DatabaseDetectorTypeMapping[detector.DType],
|
||||
}
|
||||
}
|
||||
|
||||
// DetectorsFromDatabaseModel converts database detectors to api detectors.
|
||||
func DetectorsFromDatabaseModel(dbDetectors []database.Detector) []*Detector {
|
||||
detectors := make([]*Detector, 0, len(dbDetectors))
|
||||
for _, d := range dbDetectors {
|
||||
detectors = append(detectors, DetectorFromDatabaseModel(d))
|
||||
}
|
||||
|
||||
return detectors
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2018 clair authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
DOCKER_REPO_ROOT="$GOPATH/src/github.com/coreos/clair"
|
||||
IMAGE=${IMAGE:-"quay.io/coreos/clair-gen-proto"}
|
||||
|
||||
docker run --rm -it \
|
||||
-v "$DOCKER_REPO_ROOT":"$DOCKER_REPO_ROOT" \
|
||||
-w "$DOCKER_REPO_ROOT" \
|
||||
"$IMAGE" \
|
||||
"./api/v3/clairpb/run_in_docker.sh"
|
@ -1,3 +0,0 @@
|
||||
protoc_version: 3.5.1
|
||||
protoc_includes:
|
||||
- ../../../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis
|
@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2018 clair authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
protoc -I/usr/include -I. \
|
||||
-I"${GOPATH}/src" \
|
||||
-I"${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis" \
|
||||
--go_out=plugins=grpc:. \
|
||||
--grpc-gateway_out=logtostderr=true:. \
|
||||
--swagger_out=logtostderr=true:. \
|
||||
./api/v3/clairpb/clair.proto
|
||||
|
||||
go generate .
|
@ -1,232 +0,0 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/coreos/clair"
|
||||
pb "github.com/coreos/clair/api/v3/clairpb"
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/imagefmt"
|
||||
"github.com/coreos/clair/pkg/pagination"
|
||||
)
|
||||
|
||||
func newRPCErrorWithClairError(code codes.Code, err error) error {
|
||||
return status.Errorf(code, "clair error reason: '%s'", err.Error())
|
||||
}
|
||||
|
||||
// NotificationServer implements NotificationService interface for serving RPC.
|
||||
type NotificationServer struct {
|
||||
Store database.Datastore
|
||||
}
|
||||
|
||||
// AncestryServer implements AncestryService interface for serving RPC.
|
||||
type AncestryServer struct {
|
||||
Store database.Datastore
|
||||
}
|
||||
|
||||
// StatusServer implements StatusService interface for serving RPC.
|
||||
type StatusServer struct {
|
||||
Store database.Datastore
|
||||
}
|
||||
|
||||
// GetStatus implements getting the current status of Clair via the Clair service.
|
||||
func (s *StatusServer) GetStatus(ctx context.Context, req *pb.GetStatusRequest) (*pb.GetStatusResponse, error) {
|
||||
clairStatus, err := GetClairStatus(s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &pb.GetStatusResponse{Status: clairStatus}, nil
|
||||
}
|
||||
|
||||
// PostAncestry implements posting an ancestry via the Clair gRPC service.
|
||||
func (s *AncestryServer) PostAncestry(ctx context.Context, req *pb.PostAncestryRequest) (*pb.PostAncestryResponse, error) {
|
||||
blobFormat := req.GetFormat()
|
||||
if !imagefmt.IsSupported(blobFormat) {
|
||||
return nil, status.Error(codes.InvalidArgument, "image blob format is not supported")
|
||||
}
|
||||
|
||||
clairStatus, err := GetClairStatus(s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
// check if the ancestry is already processed; if not we build the ancestry again.
|
||||
layerHashes := make([]string, len(req.Layers))
|
||||
for i, layer := range req.Layers {
|
||||
layerHashes[i] = layer.GetHash()
|
||||
}
|
||||
|
||||
found, err := clair.IsAncestryCached(s.Store, req.AncestryName, layerHashes)
|
||||
if err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
if found {
|
||||
return &pb.PostAncestryResponse{Status: clairStatus}, nil
|
||||
}
|
||||
|
||||
builder := clair.NewAncestryBuilder(clair.EnabledDetectors())
|
||||
layerMap := map[string]*database.Layer{}
|
||||
layerMapLock := sync.RWMutex{}
|
||||
g, analyzerCtx := errgroup.WithContext(ctx)
|
||||
for i := range req.Layers {
|
||||
layer := req.Layers[i]
|
||||
if _, ok := layerMap[layer.Hash]; !ok {
|
||||
layerMap[layer.Hash] = nil
|
||||
if layer == nil {
|
||||
err := status.Error(codes.InvalidArgument, "ancestry layer is invalid")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if layer.GetHash() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "ancestry layer hash should not be empty")
|
||||
}
|
||||
|
||||
if layer.GetPath() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "ancestry layer path should not be empty")
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
clairLayer, err := clair.AnalyzeLayer(analyzerCtx, s.Store, layer.Hash, req.Format, layer.Path, layer.Headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
layerMapLock.Lock()
|
||||
layerMap[layer.Hash] = clairLayer
|
||||
layerMapLock.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err = g.Wait(); err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
for _, layer := range req.Layers {
|
||||
builder.AddLeafLayer(layerMap[layer.Hash])
|
||||
}
|
||||
|
||||
if err := clair.SaveAncestry(s.Store, builder.Ancestry(req.AncestryName)); err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
return &pb.PostAncestryResponse{Status: clairStatus}, nil
|
||||
}
|
||||
|
||||
// GetAncestry implements retrieving an ancestry via the Clair gRPC service.
|
||||
func (s *AncestryServer) GetAncestry(ctx context.Context, req *pb.GetAncestryRequest) (*pb.GetAncestryResponse, error) {
|
||||
name := req.GetAncestryName()
|
||||
if name == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "ancestry name should not be empty")
|
||||
}
|
||||
|
||||
ancestry, ok, err := database.FindAncestryAndRollback(s.Store, name)
|
||||
if err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "requested ancestry '%s' is not found", req.GetAncestryName())
|
||||
}
|
||||
|
||||
pbAncestry := &pb.GetAncestryResponse_Ancestry{
|
||||
Name: ancestry.Name,
|
||||
Detectors: pb.DetectorsFromDatabaseModel(ancestry.By),
|
||||
}
|
||||
|
||||
for _, layer := range ancestry.Layers {
|
||||
pbLayer, err := s.GetPbAncestryLayer(layer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pbAncestry.Layers = append(pbAncestry.Layers, pbLayer)
|
||||
}
|
||||
|
||||
pbClairStatus, err := GetClairStatus(s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &pb.GetAncestryResponse{
|
||||
Status: pbClairStatus,
|
||||
Ancestry: pbAncestry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetNotification implements retrieving a notification via the Clair gRPC
|
||||
// service.
|
||||
func (s *NotificationServer) GetNotification(ctx context.Context, req *pb.GetNotificationRequest) (*pb.GetNotificationResponse, error) {
|
||||
if req.GetName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "notification name should not be empty")
|
||||
}
|
||||
|
||||
if req.GetLimit() <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "notification page limit should not be empty or less than 1")
|
||||
}
|
||||
|
||||
dbNotification, ok, err := database.FindVulnerabilityNotificationAndRollback(
|
||||
s.Store,
|
||||
req.GetName(),
|
||||
int(req.GetLimit()),
|
||||
pagination.Token(req.GetOldVulnerabilityPage()),
|
||||
pagination.Token(req.GetNewVulnerabilityPage()),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "requested notification '%s' is not found", req.GetName())
|
||||
}
|
||||
|
||||
notification, err := pb.NotificationFromDatabaseModel(dbNotification)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &pb.GetNotificationResponse{Notification: notification}, nil
|
||||
}
|
||||
|
||||
// MarkNotificationAsRead implements deleting a notification via the Clair gRPC
|
||||
// service.
|
||||
func (s *NotificationServer) MarkNotificationAsRead(ctx context.Context, req *pb.MarkNotificationAsReadRequest) (*pb.MarkNotificationAsReadResponse, error) {
|
||||
if req.GetName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "notification name should not be empty")
|
||||
}
|
||||
|
||||
found, err := database.MarkNotificationAsReadAndCommit(s.Store, req.GetName())
|
||||
if err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, status.Errorf(codes.NotFound, "requested notification '%s' is not found", req.GetName())
|
||||
}
|
||||
|
||||
return &pb.MarkNotificationAsReadResponse{}, nil
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
// Copyright 2018 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
pb "github.com/coreos/clair/api/v3/clairpb"
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/pkg/grpcutil"
|
||||
)
|
||||
|
||||
var (
|
||||
promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "clair_v3_api_response_duration_milliseconds",
|
||||
Help: "The duration of time it takes to receive and write a response to an V2 API request",
|
||||
Buckets: prometheus.ExponentialBuckets(9.375, 2, 10),
|
||||
}, []string{"route", "code"})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(promResponseDurationMilliseconds)
|
||||
}
|
||||
|
||||
func prometheusHandler(h http.Handler) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", h)
|
||||
mux.Handle("/metrics", prometheus.Handler())
|
||||
return mux
|
||||
}
|
||||
|
||||
type httpStatusWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (w *httpStatusWriter) WriteHeader(code int) {
|
||||
w.StatusCode = code
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func loggingHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
lrw := &httpStatusWriter{ResponseWriter: w, StatusCode: http.StatusOK}
|
||||
|
||||
h.ServeHTTP(lrw, r)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"remote addr": r.RemoteAddr,
|
||||
"method": r.Method,
|
||||
"request uri": r.RequestURI,
|
||||
"status": strconv.Itoa(lrw.StatusCode),
|
||||
"elapsed time (ms)": float64(time.Since(start).Nanoseconds()) * 1e-6,
|
||||
}).Info("handled HTTP request")
|
||||
})
|
||||
}
|
||||
|
||||
// ListenAndServe serves the Clair v3 API over gRPC and the gRPC Gateway.
|
||||
func ListenAndServe(addr, certFile, keyFile, caPath string, store database.Datastore) error {
|
||||
srv := grpcutil.MuxedGRPCServer{
|
||||
Addr: addr,
|
||||
ServicesFunc: func(gsrv *grpc.Server) {
|
||||
pb.RegisterAncestryServiceServer(gsrv, &AncestryServer{Store: store})
|
||||
pb.RegisterNotificationServiceServer(gsrv, &NotificationServer{Store: store})
|
||||
pb.RegisterStatusServiceServer(gsrv, &StatusServer{Store: store})
|
||||
},
|
||||
ServiceHandlerFuncs: []grpcutil.RegisterServiceHandlerFunc{
|
||||
pb.RegisterAncestryServiceHandler,
|
||||
pb.RegisterNotificationServiceHandler,
|
||||
pb.RegisterStatusServiceHandler,
|
||||
},
|
||||
}
|
||||
|
||||
middleware := func(h http.Handler) http.Handler {
|
||||
return prometheusHandler(loggingHandler(h))
|
||||
}
|
||||
|
||||
var err error
|
||||
if caPath == "" {
|
||||
err = srv.ListenAndServe(middleware)
|
||||
} else {
|
||||
err = srv.ListenAndServeTLS(certFile, keyFile, caPath, middleware)
|
||||
}
|
||||
return err
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3
|
||||
|
||||
import (
|
||||
"github.com/coreos/clair"
|
||||
pb "github.com/coreos/clair/api/v3/clairpb"
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// GetClairStatus retrieves the current status of Clair and wrap it inside
|
||||
// protobuf struct.
|
||||
func GetClairStatus(store database.Datastore) (*pb.ClairStatus, error) {
|
||||
status := &pb.ClairStatus{
|
||||
Detectors: pb.DetectorsFromDatabaseModel(clair.EnabledDetectors()),
|
||||
}
|
||||
|
||||
t, firstUpdate, err := clair.GetLastUpdateTime(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if firstUpdate {
|
||||
return status, nil
|
||||
}
|
||||
|
||||
status.LastUpdateTime, err = ptypes.TimestampProto(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// GetPbAncestryLayer retrieves an ancestry layer with vulnerabilities and
|
||||
// features in an ancestry based on the provided database layer.
|
||||
func (s *AncestryServer) GetPbAncestryLayer(layer database.AncestryLayer) (*pb.GetAncestryResponse_AncestryLayer, error) {
|
||||
pbLayer := &pb.GetAncestryResponse_AncestryLayer{
|
||||
Layer: &pb.Layer{
|
||||
Hash: layer.Hash,
|
||||
},
|
||||
}
|
||||
|
||||
features := layer.GetFeatures()
|
||||
affectedFeatures, err := database.FindAffectedNamespacedFeaturesAndRollback(s.Store, features)
|
||||
if err != nil {
|
||||
return nil, newRPCErrorWithClairError(codes.Internal, err)
|
||||
}
|
||||
|
||||
for _, feature := range affectedFeatures {
|
||||
if !feature.Valid {
|
||||
panic("feature is missing in the database, it indicates the database is corrupted.")
|
||||
}
|
||||
|
||||
for _, detectedFeature := range layer.Features {
|
||||
if detectedFeature.NamespacedFeature != feature.NamespacedFeature {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
pbFeature = pb.NamespacedFeatureFromDatabaseModel(detectedFeature)
|
||||
pbVuln *pb.Vulnerability
|
||||
err error
|
||||
)
|
||||
|
||||
for _, vuln := range feature.AffectedBy {
|
||||
if pbVuln, err = pb.VulnerabilityWithFixedInFromDatabaseModel(vuln); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
pbFeature.Vulnerabilities = append(pbFeature.Vulnerabilities, pbVuln)
|
||||
}
|
||||
|
||||
pbLayer.DetectedFeatures = append(pbLayer.DetectedFeatures, pbFeature)
|
||||
}
|
||||
}
|
||||
|
||||
return pbLayer, nil
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/clair/pkg/httputil"
|
||||
)
|
||||
|
||||
func retrieveLayerBlob(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
httpHeaders := make(http.Header)
|
||||
for key, value := range headers {
|
||||
httpHeaders[key] = []string{value}
|
||||
}
|
||||
|
||||
reader, err := httputil.GetWithContext(ctx, path, httpHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
return os.Open(path)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
@ -1,11 +0,0 @@
|
||||
name: clair
|
||||
home: https://coreos.com/clair
|
||||
version: 0.1.1
|
||||
appVersion: 3.0.0-pre
|
||||
description: Clair is an open source project for the static analysis of vulnerabilities in application containers.
|
||||
icon: https://cloud.githubusercontent.com/assets/343539/21630811/c5081e5c-d202-11e6-92eb-919d5999c77a.png
|
||||
sources:
|
||||
- https://github.com/coreos/clair
|
||||
maintainers:
|
||||
- name: Jimmy Zelinskie
|
||||
email: jimmy.zelinskie@coreos.com
|
@ -1,5 +0,0 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: "1.0.0"
|
||||
condition: postgresql.enabled
|
||||
repository: "alias:stable"
|
@ -1,83 +0,0 @@
|
||||
clair:
|
||||
database:
|
||||
# Database driver.
|
||||
type: pgsql
|
||||
options:
|
||||
# PostgreSQL Connection string.
|
||||
# https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
{{- if .Values.config.postgresURI }}
|
||||
source: "{{ .Values.config.postgresURI }}"
|
||||
{{ else }}
|
||||
source: "host={{ template "postgresql.fullname" . }} port=5432 user={{ .Values.postgresql.postgresUser }} password={{ .Values.postgresql.postgresPassword }} dbname={{ .Values.postgresql.postgresDatabase }} sslmode=disable statement_timeout=60000"
|
||||
{{ end }}
|
||||
|
||||
# Number of elements kept in the cache.
|
||||
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
|
||||
cachesize: 16384
|
||||
|
||||
# 32-bit URL-safe base64 key used to encrypt pagination tokens.
|
||||
# If one is not provided, it will be generated.
|
||||
# Multiple clair instances in the same cluster need the same value.
|
||||
paginationkey: "{{ .Values.config.paginationKey }}"
|
||||
api:
|
||||
# v3 grpc/RESTful API server address.
|
||||
addr: "0.0.0.0:{{ .Values.service.internalApiPort }}"
|
||||
|
||||
# Health server address.
|
||||
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
|
||||
healthaddr: "0.0.0.0:{{ .Values.service.internalHealthPort }}"
|
||||
|
||||
# Deadline before an API request will respond with a 503.
|
||||
timeout: 900s
|
||||
|
||||
# Optional PKI configuration.
|
||||
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||
# https://github.com/coreos/etcd-ca
|
||||
# https://github.com/cloudflare/cfssl
|
||||
servername:
|
||||
cafile:
|
||||
keyfile:
|
||||
certfile:
|
||||
|
||||
worker:
|
||||
namespace_detectors:
|
||||
{{- range $key, $value := .Values.config.enabledNamespaceDetectors }}
|
||||
- {{ $value }}
|
||||
{{- end }}
|
||||
|
||||
feature_listers:
|
||||
{{- range $key, $value := .Values.config.enabledFeatureListers }}
|
||||
- {{ $value }}
|
||||
{{- end }}
|
||||
|
||||
updater:
|
||||
# Frequency the database will be updated with vulnerabilities from the default data sources.
|
||||
# The value 0 disables the updater entirely.
|
||||
interval: "{{ .Values.config.updateInterval }}"
|
||||
enabledupdaters:
|
||||
{{- range $key, $value := .Values.config.enabledUpdaters }}
|
||||
- {{ $value }}
|
||||
{{- end }}
|
||||
|
||||
notifier:
|
||||
# Number of attempts before the notification is marked as failed to be sent.
|
||||
attempts: 3
|
||||
|
||||
# Duration before a failed notification is retried.
|
||||
renotifyinterval: 2h
|
||||
|
||||
http:
|
||||
# Optional endpoint that will receive notifications via POST requests.
|
||||
endpoint: "{{ .Values.config.notificationWebhookEndpoint }}"
|
||||
|
||||
# Optional PKI configuration.
|
||||
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||
# https://github.com/cloudflare/cfssl
|
||||
# https://github.com/coreos/etcd-ca
|
||||
servername:
|
||||
cafile:
|
||||
keyfile:
|
||||
certfile:
|
||||
|
||||
# Optional HTTP Proxy: must be a valid URL (including the scheme).
|
||||
proxy:
|
@ -1,33 +0,0 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "clair.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "clair.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified postgresql name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "postgresql.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
@ -1,59 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "clair.fullname" . }}
|
||||
labels:
|
||||
heritage: {{ .Release.Service | quote }}
|
||||
release: {{ .Release.Name | quote }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
|
||||
app: {{ template "clair.fullname" . }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ template "clair.fullname" . }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "clair.fullname" . }}
|
||||
spec:
|
||||
volumes:
|
||||
- name: "{{ .Chart.Name }}-config"
|
||||
secret:
|
||||
secretName: {{ template "clair.fullname" . }}
|
||||
{{- if .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml .Values.nodeSelector | indent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml .Values.tolerations | indent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- "-log-level={{ .Values.logLevel }}"
|
||||
{{ if .Values.insecureTls }}- "--insecure-tls" {{end}}
|
||||
ports:
|
||||
- name: "{{ .Chart.Name }}-api"
|
||||
containerPort: {{ .Values.service.internalApiPort }}
|
||||
protocol: TCP
|
||||
- name: "{{ .Chart.Name }}-health"
|
||||
containerPort: {{ .Values.service.internalHealthPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: {{ .Values.service.internalHealthPort }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: {{ .Values.service.internalHealthPort }}
|
||||
volumeMounts:
|
||||
- name: "{{ .Chart.Name }}-config"
|
||||
mountPath: /etc/clair
|
||||
readOnly: true
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 10 }}
|
@ -1,33 +0,0 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $serviceName := include "clair.fullname" . -}}
|
||||
{{- $servicePort := .Values.service.externalApiPort -}}
|
||||
{{- $path := .Values.ingress.path | default "/" -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ template "clair.fullname" . }}
|
||||
labels:
|
||||
heritage: {{ .Release.Service | quote }}
|
||||
release: {{ .Release.Name | quote }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
|
||||
app: {{ template "clair.fullname" . }}
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.ingress.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
rules:
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
- host: {{ $host }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $path }}
|
||||
backend:
|
||||
serviceName: {{ $serviceName }}
|
||||
servicePort: {{ $servicePort }}
|
||||
{{- end -}}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{ toYaml .Values.ingress.tls | indent 4 }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "clair.fullname" . }}
|
||||
labels:
|
||||
heritage: {{ .Release.Service | quote }}
|
||||
release: {{ .Release.Name | quote }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
|
||||
app: {{ template "clair.fullname" . }}
|
||||
type: Opaque
|
||||
data:
|
||||
config.yaml: |-
|
||||
{{ include (print .Template.BasePath "/_config.yaml.tpl") . | b64enc | indent 4 }}
|
@ -1,28 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ template "clair.fullname" . }}
|
||||
labels:
|
||||
heritage: {{ .Release.Service | quote }}
|
||||
release: {{ .Release.Name | quote }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
|
||||
app: {{ template "clair.fullname" . }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: "{{ .Chart.Name }}-api"
|
||||
port: {{ .Values.service.externalApiPort }}
|
||||
targetPort: {{ .Values.service.internalApiPort }}
|
||||
protocol: TCP
|
||||
{{- if and (.Values.service.apiNodePort) (eq .Values.service.type "NodePort") }}
|
||||
nodePort: {{ .Values.service.apiNodePort }}
|
||||
{{- end }}
|
||||
- name: "{{ .Chart.Name }}-health"
|
||||
port: {{ .Values.service.externalHealthPort }}
|
||||
targetPort: {{ .Values.service.internalHealthPort }}
|
||||
protocol: TCP
|
||||
{{- if and (.Values.service.healthNodePort) (eq .Values.service.type "NodePort") }}
|
||||
nodePort: {{ .Values.service.healthNodePort }}
|
||||
{{- end }}
|
||||
selector:
|
||||
app: {{ template "clair.fullname" . }}
|
@ -1,79 +0,0 @@
|
||||
# Default values for clair.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
replicaCount: 1
|
||||
logLevel: info
|
||||
insecureTls: false
|
||||
image:
|
||||
repository: quay.io/coreos/clair-git
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
service:
|
||||
type: ClusterIP
|
||||
internalApiPort: 6060
|
||||
externalApiPort: 6060
|
||||
internalHealthPort: 6061
|
||||
externalHealthPort: 6061
|
||||
ingress:
|
||||
enabled: false
|
||||
# Used to create Ingress record (should used with service.type: ClusterIP).
|
||||
hosts:
|
||||
- chart-example.local
|
||||
annotations:
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
tls:
|
||||
# Secrets must be manually created in the namespace.
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 1500Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 500Mi
|
||||
config:
|
||||
# postgresURI: "postgres://user:password@host:5432/postgres?sslmode=disable"
|
||||
paginationKey: "XxoPtCUzrUv4JV5dS+yQ+MdW7yLEJnRMwigVY/bpgtQ="
|
||||
updateInterval: 2h
|
||||
notificationWebhookEndpoint: https://example.com/notify/me
|
||||
enabledUpdaters:
|
||||
- debian
|
||||
- ubuntu
|
||||
- rhel
|
||||
- oracle
|
||||
- alpine
|
||||
enabledNamespaceDetectors:
|
||||
- os-release
|
||||
- lsb-release
|
||||
- apt-sources
|
||||
- alpine-release
|
||||
- redhat-release
|
||||
enabledFeatureListers:
|
||||
- apk
|
||||
- dpkg
|
||||
- rpm
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
||||
# Configuration values for the postgresql dependency.
|
||||
# ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md
|
||||
postgresql:
|
||||
# The dependant Postgres chart can be disabled, to connect to
|
||||
# an existing database by defining config.postgresURI
|
||||
enabled: true
|
||||
|
||||
imageTag: 9.6-alpine
|
||||
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
# These values are hardcoded until Helm supports secrets.
|
||||
# For more info see: https://github.com/kubernetes/helm/issues/2196
|
||||
postgresUser: clair
|
||||
postgresPassword: clair
|
||||
postgresDatabase: clair
|
||||
|
||||
persistence:
|
||||
size: 10Gi
|
@ -0,0 +1,84 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: clairsvc
|
||||
labels:
|
||||
app: clair
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- port: 6060
|
||||
protocol: TCP
|
||||
nodePort: 30060
|
||||
name: clair-port0
|
||||
- port: 6061
|
||||
protocol: TCP
|
||||
nodePort: 30061
|
||||
name: clair-port1
|
||||
selector:
|
||||
app: clair
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: clair
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: clair
|
||||
spec:
|
||||
volumes:
|
||||
- name: secret-volume
|
||||
secret:
|
||||
secretName: clairsecret
|
||||
containers:
|
||||
- name: clair
|
||||
image: quay.io/coreos/clair
|
||||
args:
|
||||
- "-config"
|
||||
- "/config/config.yaml"
|
||||
ports:
|
||||
- containerPort: 6060
|
||||
- containerPort: 6061
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: secret-volume
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
name: clair-postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- image: postgres:latest
|
||||
name: postgres
|
||||
env:
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: password
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: postgres-port
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
name: postgres
|
||||
spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
selector:
|
||||
app: postgres
|
@ -0,0 +1,80 @@
|
||||
# Copyright 2015 clair authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined.
|
||||
clair:
|
||||
database:
|
||||
# Database driver
|
||||
type: pgsql
|
||||
options:
|
||||
# PostgreSQL Connection string
|
||||
# https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
source: postgres://postgres:password@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# Number of elements kept in the cache
|
||||
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
|
||||
cachesize: 16384
|
||||
|
||||
api:
|
||||
# API server port
|
||||
port: 6060
|
||||
|
||||
# Health server port
|
||||
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
|
||||
healthport: 6061
|
||||
|
||||
# Deadline before an API request will respond with a 503
|
||||
timeout: 900s
|
||||
|
||||
# 32-bit URL-safe base64 key used to encrypt pagination tokens
|
||||
# If one is not provided, it will be generated.
|
||||
# Multiple clair instances in the same cluster need the same value.
|
||||
paginationkey:
|
||||
|
||||
# Optional PKI configuration
|
||||
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||
# https://github.com/coreos/etcd-ca
|
||||
# https://github.com/cloudflare/cfssl
|
||||
servername:
|
||||
cafile:
|
||||
keyfile:
|
||||
certfile:
|
||||
|
||||
updater:
|
||||
# Frequency the database will be updated with vulnerabilities from the default data sources
|
||||
# The value 0 disables the updater entirely.
|
||||
interval: 2h
|
||||
|
||||
notifier:
|
||||
# Number of attempts before the notification is marked as failed to be sent
|
||||
attempts: 3
|
||||
|
||||
# Duration before a failed notification is retried
|
||||
renotifyinterval: 2h
|
||||
|
||||
http:
|
||||
# Optional endpoint that will receive notifications via POST requests
|
||||
endpoint:
|
||||
|
||||
# Optional PKI configuration
|
||||
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||
# https://github.com/cloudflare/cfssl
|
||||
# https://github.com/coreos/etcd-ca
|
||||
servername:
|
||||
cafile:
|
||||
keyfile:
|
||||
certfile:
|
||||
|
||||
# Optional HTTP Proxy: must be a valid URL (including the scheme).
|
||||
proxy:
|
@ -1,96 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
// Ancestry is a manifest that keeps all layers in an image in order.
|
||||
type Ancestry struct {
|
||||
// Name is a globally unique value for a set of layers. This is often the
|
||||
// sha256 digest of an OCI/Docker manifest.
|
||||
Name string `json:"name"`
|
||||
// By contains the processors that are used when computing the
|
||||
// content of this ancestry.
|
||||
By []Detector `json:"by"`
|
||||
// Layers should be ordered and i_th layer is the parent of i+1_th layer in
|
||||
// the slice.
|
||||
Layers []AncestryLayer `json:"layers"`
|
||||
}
|
||||
|
||||
// Valid checks if the ancestry is compliant to spec.
|
||||
func (a *Ancestry) Valid() bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, d := range a.By {
|
||||
if !d.Valid() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range a.Layers {
|
||||
if !l.Valid() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AncestryLayer is a layer with all detected namespaced features.
|
||||
type AncestryLayer struct {
|
||||
// Hash is the sha-256 tarsum on the layer's blob content.
|
||||
Hash string `json:"hash"`
|
||||
// Features are the features introduced by this layer when it was
|
||||
// processed.
|
||||
Features []AncestryFeature `json:"features"`
|
||||
}
|
||||
|
||||
// Valid checks if the Ancestry Layer is compliant to the spec.
|
||||
func (l *AncestryLayer) Valid() bool {
|
||||
if l == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if l.Hash == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetFeatures returns the Ancestry's features.
|
||||
func (l *AncestryLayer) GetFeatures() []NamespacedFeature {
|
||||
nsf := make([]NamespacedFeature, 0, len(l.Features))
|
||||
for _, f := range l.Features {
|
||||
nsf = append(nsf, f.NamespacedFeature)
|
||||
}
|
||||
|
||||
return nsf
|
||||
}
|
||||
|
||||
// AncestryFeature is a namespaced feature with the detectors used to
|
||||
// find this feature.
|
||||
type AncestryFeature struct {
|
||||
NamespacedFeature `json:"namespacedFeature"`
|
||||
|
||||
// FeatureBy is the detector that detected the feature.
|
||||
FeatureBy Detector `json:"featureBy"`
|
||||
// NamespaceBy is the detector that detected the namespace.
|
||||
NamespaceBy Detector `json:"namespaceBy"`
|
||||
}
|
@ -1,539 +0,0 @@
|
||||
// Copyright 2018 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
"github.com/coreos/clair/pkg/pagination"
|
||||
"github.com/deckarep/golang-set"
|
||||
)
|
||||
|
||||
// DeduplicateNamespaces deduplicates a list of namespaces.
|
||||
func DeduplicateNamespaces(namespaces ...Namespace) []Namespace {
|
||||
nsSet := mapset.NewSet()
|
||||
for _, ns := range namespaces {
|
||||
nsSet.Add(ns)
|
||||
}
|
||||
|
||||
uniqueNamespaces := make([]Namespace, 0, nsSet.Cardinality())
|
||||
for ns := range nsSet.Iter() {
|
||||
uniqueNamespaces = append(uniqueNamespaces, ns.(Namespace))
|
||||
}
|
||||
|
||||
return uniqueNamespaces
|
||||
}
|
||||
|
||||
// DeduplicateFeatures deduplicates a list of list of features.
|
||||
func DeduplicateFeatures(features ...Feature) []Feature {
|
||||
fSet := mapset.NewSet()
|
||||
for _, f := range features {
|
||||
fSet.Add(f)
|
||||
}
|
||||
|
||||
return ConvertFeatureSetToFeatures(fSet)
|
||||
}
|
||||
|
||||
// ConvertFeatureSetToFeatures converts a feature set to an array of features
|
||||
func ConvertFeatureSetToFeatures(features mapset.Set) []Feature {
|
||||
uniqueFeatures := make([]Feature, 0, features.Cardinality())
|
||||
for f := range features.Iter() {
|
||||
uniqueFeatures = append(uniqueFeatures, f.(Feature))
|
||||
}
|
||||
|
||||
return uniqueFeatures
|
||||
}
|
||||
|
||||
func ConvertFeatureSetToLayerFeatures(features mapset.Set) []LayerFeature {
|
||||
uniqueLayerFeatures := make([]LayerFeature, 0, features.Cardinality())
|
||||
for f := range features.Iter() {
|
||||
feature := f.(Feature)
|
||||
layerFeature := LayerFeature{
|
||||
Feature: feature,
|
||||
}
|
||||
uniqueLayerFeatures = append(uniqueLayerFeatures, layerFeature)
|
||||
}
|
||||
|
||||
return uniqueLayerFeatures
|
||||
}
|
||||
|
||||
// FindKeyValueAndRollback wraps session FindKeyValue function with begin and
|
||||
// roll back.
|
||||
func FindKeyValueAndRollback(datastore Datastore, key string) (value string, ok bool, err error) {
|
||||
var tx Session
|
||||
tx, err = datastore.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
value, ok, err = tx.FindKeyValue(key)
|
||||
return
|
||||
}
|
||||
|
||||
// PersistPartialLayerAndCommit wraps session PersistLayer function with begin and
|
||||
// commit.
|
||||
func PersistPartialLayerAndCommit(datastore Datastore, layer *Layer) error {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.PersistLayer(layer.Hash, layer.Features, layer.Namespaces, layer.By); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// PersistFeaturesAndCommit wraps session PersistFeaturesAndCommit function with begin and commit.
|
||||
func PersistFeaturesAndCommit(datastore Datastore, features []Feature) error {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.PersistFeatures(features); err != nil {
|
||||
serialized, _ := json.Marshal(features)
|
||||
log.WithError(err).WithField("feature", string(serialized)).Error("failed to store features")
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// PersistNamespacesAndCommit wraps session PersistNamespaces function with
|
||||
// begin and commit.
|
||||
func PersistNamespacesAndCommit(datastore Datastore, namespaces []Namespace) error {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.PersistNamespaces(namespaces); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// FindAncestryAndRollback wraps session FindAncestry function with begin and
|
||||
// rollback.
|
||||
func FindAncestryAndRollback(datastore Datastore, name string) (Ancestry, bool, error) {
|
||||
tx, err := datastore.Begin()
|
||||
defer tx.Rollback()
|
||||
|
||||
if err != nil {
|
||||
return Ancestry{}, false, err
|
||||
}
|
||||
|
||||
return tx.FindAncestry(name)
|
||||
}
|
||||
|
||||
// FindLayerAndRollback wraps session FindLayer function with begin and rollback.
|
||||
func FindLayerAndRollback(datastore Datastore, hash string) (layer *Layer, ok bool, err error) {
|
||||
var tx Session
|
||||
if tx, err = datastore.Begin(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
// TODO(sidac): In order to make the session interface more idiomatic, we'll
|
||||
// return the pointer value in the future.
|
||||
var dereferencedLayer Layer
|
||||
dereferencedLayer, ok, err = tx.FindLayer(hash)
|
||||
layer = &dereferencedLayer
|
||||
return
|
||||
}
|
||||
|
||||
// DeduplicateNamespacedFeatures returns a copy of all unique features in the
|
||||
// input.
|
||||
func DeduplicateNamespacedFeatures(features []NamespacedFeature) []NamespacedFeature {
|
||||
nsSet := mapset.NewSet()
|
||||
for _, ns := range features {
|
||||
nsSet.Add(ns)
|
||||
}
|
||||
|
||||
uniqueFeatures := make([]NamespacedFeature, 0, nsSet.Cardinality())
|
||||
for ns := range nsSet.Iter() {
|
||||
uniqueFeatures = append(uniqueFeatures, ns.(NamespacedFeature))
|
||||
}
|
||||
|
||||
return uniqueFeatures
|
||||
}
|
||||
|
||||
// GetAncestryFeatures returns a list of unique namespaced features in the
|
||||
// ancestry.
|
||||
func GetAncestryFeatures(ancestry Ancestry) []NamespacedFeature {
|
||||
features := []NamespacedFeature{}
|
||||
for _, layer := range ancestry.Layers {
|
||||
features = append(features, layer.GetFeatures()...)
|
||||
}
|
||||
|
||||
return DeduplicateNamespacedFeatures(features)
|
||||
}
|
||||
|
||||
// UpsertAncestryAndCommit wraps session UpsertAncestry function with begin and commit.
|
||||
func UpsertAncestryAndCommit(datastore Datastore, ancestry *Ancestry) error {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.UpsertAncestry(*ancestry); err != nil {
|
||||
log.WithError(err).Error("failed to upsert the ancestry")
|
||||
serialized, _ := json.Marshal(ancestry)
|
||||
log.Debug(string(serialized))
|
||||
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PersistNamespacedFeaturesAndCommit wraps session PersistNamespacedFeatures function
|
||||
// with begin and commit.
|
||||
func PersistNamespacedFeaturesAndCommit(datastore Datastore, features []NamespacedFeature) error {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.PersistNamespacedFeatures(features); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheRelatedVulnerabilityAndCommit wraps session CacheAffectedNamespacedFeatures
|
||||
// function with begin and commit.
|
||||
func CacheRelatedVulnerabilityAndCommit(datastore Datastore, features []NamespacedFeature) error {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.CacheAffectedNamespacedFeatures(features); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// IntersectDetectors returns the detectors in both d1 and d2.
|
||||
func IntersectDetectors(d1 []Detector, d2 []Detector) []Detector {
|
||||
d1Set := mapset.NewSet()
|
||||
for _, d := range d1 {
|
||||
d1Set.Add(d)
|
||||
}
|
||||
|
||||
d2Set := mapset.NewSet()
|
||||
for _, d := range d2 {
|
||||
d2Set.Add(d)
|
||||
}
|
||||
|
||||
inter := d1Set.Intersect(d2Set)
|
||||
detectors := make([]Detector, 0, inter.Cardinality())
|
||||
for d := range inter.Iter() {
|
||||
detectors = append(detectors, d.(Detector))
|
||||
}
|
||||
|
||||
return detectors
|
||||
}
|
||||
|
||||
// DiffDetectors returns the detectors belongs to d1 but not d2
|
||||
func DiffDetectors(d1 []Detector, d2 []Detector) []Detector {
|
||||
d1Set := mapset.NewSet()
|
||||
for _, d := range d1 {
|
||||
d1Set.Add(d)
|
||||
}
|
||||
|
||||
d2Set := mapset.NewSet()
|
||||
for _, d := range d2 {
|
||||
d2Set.Add(d)
|
||||
}
|
||||
|
||||
diff := d1Set.Difference(d2Set)
|
||||
detectors := make([]Detector, 0, diff.Cardinality())
|
||||
for d := range diff.Iter() {
|
||||
detectors = append(detectors, d.(Detector))
|
||||
}
|
||||
|
||||
return detectors
|
||||
}
|
||||
|
||||
// MergeLayers merges all content in new layer to l, where the content is
|
||||
// updated.
|
||||
func MergeLayers(l *Layer, new *Layer) *Layer {
|
||||
featureSet := mapset.NewSet()
|
||||
namespaceSet := mapset.NewSet()
|
||||
bySet := mapset.NewSet()
|
||||
|
||||
for _, f := range l.Features {
|
||||
featureSet.Add(f)
|
||||
}
|
||||
|
||||
for _, ns := range l.Namespaces {
|
||||
namespaceSet.Add(ns)
|
||||
}
|
||||
|
||||
for _, d := range l.By {
|
||||
bySet.Add(d)
|
||||
}
|
||||
|
||||
for _, feature := range new.Features {
|
||||
if !featureSet.Contains(feature) {
|
||||
l.Features = append(l.Features, feature)
|
||||
featureSet.Add(feature)
|
||||
}
|
||||
}
|
||||
|
||||
for _, namespace := range new.Namespaces {
|
||||
if !namespaceSet.Contains(namespace) {
|
||||
l.Namespaces = append(l.Namespaces, namespace)
|
||||
namespaceSet.Add(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
for _, detector := range new.By {
|
||||
if !bySet.Contains(detector) {
|
||||
l.By = append(l.By, detector)
|
||||
bySet.Add(detector)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// AcquireLock acquires a named global lock for a duration.
|
||||
func AcquireLock(datastore Datastore, name, owner string, duration time.Duration) (acquired bool, expiration time.Time) {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return false, time.Time{}
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
locked, t, err := tx.AcquireLock(name, owner, duration)
|
||||
if err != nil {
|
||||
return false, time.Time{}
|
||||
}
|
||||
|
||||
if locked {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
return locked, t
|
||||
}
|
||||
|
||||
// ExtendLock extends the duration of an existing global lock for the given
|
||||
// duration.
|
||||
func ExtendLock(ds Datastore, name, whoami string, desiredLockDuration time.Duration) (bool, time.Time) {
|
||||
tx, err := ds.Begin()
|
||||
if err != nil {
|
||||
return false, time.Time{}
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
locked, expiration, err := tx.ExtendLock(name, whoami, desiredLockDuration)
|
||||
if err != nil {
|
||||
return false, time.Time{}
|
||||
}
|
||||
|
||||
if locked {
|
||||
if err := tx.Commit(); err == nil {
|
||||
return locked, expiration
|
||||
}
|
||||
}
|
||||
|
||||
return false, time.Time{}
|
||||
}
|
||||
|
||||
// ReleaseLock releases a named global lock.
|
||||
func ReleaseLock(datastore Datastore, name, owner string) {
|
||||
tx, err := datastore.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.ReleaseLock(name, owner); err != nil {
|
||||
return
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// PersistDetectorsAndCommit stores the detectors in the data store.
|
||||
func PersistDetectorsAndCommit(store Datastore, detectors []Detector) error {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
if err := tx.PersistDetectors(detectors); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkNotificationAsReadAndCommit marks a notification as read.
|
||||
func MarkNotificationAsReadAndCommit(store Datastore, name string) (bool, error) {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
err = tx.DeleteNotification(name)
|
||||
if err == commonerr.ErrNotFound {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindAffectedNamespacedFeaturesAndRollback finds the vulnerabilities on each
|
||||
// feature.
|
||||
func FindAffectedNamespacedFeaturesAndRollback(store Datastore, features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error) {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
nullableFeatures, err := tx.FindAffectedNamespacedFeatures(features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nullableFeatures, nil
|
||||
}
|
||||
|
||||
// FindVulnerabilityNotificationAndRollback finds the vulnerability notification
|
||||
// and rollback.
|
||||
func FindVulnerabilityNotificationAndRollback(store Datastore, name string, limit int, oldVulnerabilityPage pagination.Token, newVulnerabilityPage pagination.Token) (VulnerabilityNotificationWithVulnerable, bool, error) {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return VulnerabilityNotificationWithVulnerable{}, false, err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
return tx.FindVulnerabilityNotification(name, limit, oldVulnerabilityPage, newVulnerabilityPage)
|
||||
}
|
||||
|
||||
// FindNewNotification finds notifications either never notified or notified
|
||||
// before the given time.
|
||||
func FindNewNotification(store Datastore, notifiedBefore time.Time) (NotificationHook, bool, error) {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return NotificationHook{}, false, err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
return tx.FindNewNotification(notifiedBefore)
|
||||
}
|
||||
|
||||
// UpdateKeyValueAndCommit stores the key value to storage.
|
||||
func UpdateKeyValueAndCommit(store Datastore, key, value string) error {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
if err = tx.UpdateKeyValue(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// InsertVulnerabilityNotificationsAndCommit inserts the notifications into db
|
||||
// and commit.
|
||||
func InsertVulnerabilityNotificationsAndCommit(store Datastore, notifications []VulnerabilityNotification) error {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.InsertVulnerabilityNotifications(notifications); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// FindVulnerabilitiesAndRollback finds the vulnerabilities based on given ids.
|
||||
func FindVulnerabilitiesAndRollback(store Datastore, ids []VulnerabilityID) ([]NullableVulnerability, error) {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
return tx.FindVulnerabilities(ids)
|
||||
}
|
||||
|
||||
func UpdateVulnerabilitiesAndCommit(store Datastore, toRemove []VulnerabilityID, toAdd []VulnerabilityWithAffected) error {
|
||||
tx, err := store.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteVulnerabilities(toRemove); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.InsertVulnerabilities(toAdd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
// Copyright 2018 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamespaceDetectorType is a type of detector that extracts the namespaces.
|
||||
NamespaceDetectorType DetectorType = "namespace"
|
||||
// FeatureDetectorType is a type of detector that extracts the features.
|
||||
FeatureDetectorType DetectorType = "feature"
|
||||
)
|
||||
|
||||
// DetectorTypes contains all detector types.
|
||||
var (
|
||||
DetectorTypes = []DetectorType{
|
||||
NamespaceDetectorType,
|
||||
FeatureDetectorType,
|
||||
}
|
||||
// ErrFailedToParseDetectorType is the error returned when a detector type could
|
||||
// not be parsed from a string.
|
||||
ErrFailedToParseDetectorType = errors.New("failed to parse DetectorType from input")
|
||||
// ErrInvalidDetector is the error returned when a detector from database has
|
||||
// invalid name or version or type.
|
||||
ErrInvalidDetector = errors.New("the detector has invalid metadata")
|
||||
)
|
||||
|
||||
// DetectorType is the type of a detector.
|
||||
type DetectorType string
|
||||
|
||||
// Value implements the database/sql/driver.Valuer interface.
|
||||
func (s DetectorType) Value() (driver.Value, error) {
|
||||
return string(s), nil
|
||||
}
|
||||
|
||||
// Scan implements the database/sql.Scanner interface.
|
||||
func (s *DetectorType) Scan(value interface{}) error {
|
||||
val, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("could not scan a Severity from a non-string input")
|
||||
}
|
||||
|
||||
var err error
|
||||
*s, err = NewDetectorType(string(val))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDetectorType attempts to parse a string into a standard DetectorType
|
||||
// value.
|
||||
func NewDetectorType(s string) (DetectorType, error) {
|
||||
for _, ss := range DetectorTypes {
|
||||
if strings.EqualFold(s, string(ss)) {
|
||||
return ss, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", ErrFailedToParseDetectorType
|
||||
}
|
||||
|
||||
// Valid checks if a detector type is defined.
|
||||
func (s DetectorType) Valid() bool {
|
||||
for _, t := range DetectorTypes {
|
||||
if s == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Detector is an versioned Clair extension.
|
||||
type Detector struct {
|
||||
// Name of an extension should be non-empty and uniquely identifies the
|
||||
// extension.
|
||||
Name string `json:"name"`
|
||||
// Version of an extension should be non-empty.
|
||||
Version string `json:"version"`
|
||||
// DType is the type of the extension and should be one of the types in
|
||||
// DetectorTypes.
|
||||
DType DetectorType `json:"dtype"`
|
||||
}
|
||||
|
||||
// Valid checks if all fields in the detector satisfies the spec.
|
||||
func (d Detector) Valid() bool {
|
||||
if d.Name == "" || d.Version == "" || !d.DType.Valid() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns a unique string representation of the detector.
|
||||
func (d Detector) String() string {
|
||||
return fmt.Sprintf("%s:%s", d.Name, d.Version)
|
||||
}
|
||||
|
||||
// NewNamespaceDetector returns a new namespace detector.
|
||||
func NewNamespaceDetector(name, version string) Detector {
|
||||
return Detector{
|
||||
Name: name,
|
||||
Version: version,
|
||||
DType: NamespaceDetectorType,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFeatureDetector returns a new feature detector.
|
||||
func NewFeatureDetector(name, version string) Detector {
|
||||
return Detector{
|
||||
Name: name,
|
||||
Version: version,
|
||||
DType: FeatureDetectorType,
|
||||
}
|
||||
}
|
||||
|
||||
// SerializeDetectors returns the string representation of given detectors.
|
||||
func SerializeDetectors(detectors []Detector) []string {
|
||||
strDetectors := []string{}
|
||||
for _, d := range detectors {
|
||||
strDetectors = append(strDetectors, d.String())
|
||||
}
|
||||
|
||||
return strDetectors
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
// StorageError is database error
|
||||
type StorageError struct {
|
||||
reason string
|
||||
original error
|
||||
}
|
||||
|
||||
func (e *StorageError) Error() string {
|
||||
return e.reason
|
||||
}
|
||||
|
||||
// NewStorageErrorWithInternalError creates a new database error
|
||||
func NewStorageErrorWithInternalError(reason string, originalError error) *StorageError {
|
||||
return &StorageError{reason, originalError}
|
||||
}
|
||||
|
||||
// NewStorageError creates a new database error
|
||||
func NewStorageError(reason string) *StorageError {
|
||||
return &StorageError{reason, nil}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
// Feature represents a package detected in a layer but the namespace is not
|
||||
// determined.
|
||||
//
|
||||
// e.g. Name: Libssl1.0, Version: 1.0, VersionFormat: dpkg, Type: binary
|
||||
// dpkg is the version format of the installer package manager, which in this
|
||||
// case could be dpkg or apk.
|
||||
type Feature struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
VersionFormat string `json:"versionFormat"`
|
||||
Type FeatureType `json:"type"`
|
||||
}
|
||||
|
||||
// NamespacedFeature is a feature with determined namespace and can be affected
|
||||
// by vulnerabilities.
|
||||
//
|
||||
// e.g. OpenSSL 1.0 dpkg Debian:7.
|
||||
type NamespacedFeature struct {
|
||||
Feature `json:"feature"`
|
||||
|
||||
Namespace Namespace `json:"namespace"`
|
||||
}
|
||||
|
||||
// AffectedNamespacedFeature is a namespaced feature affected by the
|
||||
// vulnerabilities with fixed-in versions for this feature.
|
||||
type AffectedNamespacedFeature struct {
|
||||
NamespacedFeature
|
||||
|
||||
AffectedBy []VulnerabilityWithFixedIn
|
||||
}
|
||||
|
||||
// VulnerabilityWithFixedIn is used for AffectedNamespacedFeature to retrieve
|
||||
// the affecting vulnerabilities and the fixed-in versions for the feature.
|
||||
type VulnerabilityWithFixedIn struct {
|
||||
Vulnerability
|
||||
|
||||
FixedInVersion string
|
||||
}
|
||||
|
||||
// AffectedFeature is used to determine whether a namespaced feature is affected
|
||||
// by a Vulnerability. Namespace and Feature Name is unique. Affected Feature is
|
||||
// bound to vulnerability.
|
||||
type AffectedFeature struct {
|
||||
// FeatureType determines which type of package it affects.
|
||||
FeatureType FeatureType
|
||||
Namespace Namespace
|
||||
FeatureName string
|
||||
// FixedInVersion is known next feature version that's not affected by the
|
||||
// vulnerability. Empty FixedInVersion means the unaffected version is
|
||||
// unknown.
|
||||
FixedInVersion string
|
||||
// AffectedVersion contains the version range to determine whether or not a
|
||||
// feature is affected.
|
||||
AffectedVersion string
|
||||
}
|
||||
|
||||
// NullableAffectedNamespacedFeature is an affectednamespacedfeature with
|
||||
// whether it's found in datastore.
|
||||
type NullableAffectedNamespacedFeature struct {
|
||||
AffectedNamespacedFeature
|
||||
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func NewFeature(name string, version string, versionFormat string, featureType FeatureType) *Feature {
|
||||
return &Feature{name, version, versionFormat, featureType}
|
||||
}
|
||||
|
||||
func NewBinaryPackage(name string, version string, versionFormat string) *Feature {
|
||||
return &Feature{name, version, versionFormat, BinaryPackage}
|
||||
}
|
||||
|
||||
func NewSourcePackage(name string, version string, versionFormat string) *Feature {
|
||||
return &Feature{name, version, versionFormat, SourcePackage}
|
||||
}
|
||||
|
||||
func NewNamespacedFeature(namespace *Namespace, feature *Feature) *NamespacedFeature {
|
||||
// TODO: namespaced feature should use pointer values
|
||||
return &NamespacedFeature{*feature, *namespace}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FeatureType indicates the type of feature that a vulnerability
|
||||
// affects.
|
||||
type FeatureType string
|
||||
|
||||
const (
|
||||
SourcePackage FeatureType = "source"
|
||||
BinaryPackage FeatureType = "binary"
|
||||
)
|
||||
|
||||
var featureTypes = []FeatureType{
|
||||
SourcePackage,
|
||||
BinaryPackage,
|
||||
}
|
||||
|
||||
// Scan implements the database/sql.Scanner interface.
|
||||
func (t *FeatureType) Scan(value interface{}) error {
|
||||
val := value.(string)
|
||||
for _, ft := range featureTypes {
|
||||
if string(ft) == val {
|
||||
*t = ft
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("invalid feature type received from database: '%s'", val))
|
||||
}
|
||||
|
||||
// Value implements the database/sql/driver.Valuer interface.
|
||||
func (t *FeatureType) Value() (driver.Value, error) {
|
||||
return string(*t), nil
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
// Layer is a layer with all the detected features and namespaces.
|
||||
type Layer struct {
|
||||
// Hash is the sha-256 tarsum on the layer's blob content.
|
||||
Hash string `json:"hash"`
|
||||
// By contains a list of detectors scanned this Layer.
|
||||
By []Detector `json:"by"`
|
||||
Namespaces []LayerNamespace `json:"namespaces"`
|
||||
Features []LayerFeature `json:"features"`
|
||||
}
|
||||
|
||||
func (l *Layer) GetFeatures() []Feature {
|
||||
features := make([]Feature, 0, len(l.Features))
|
||||
for _, f := range l.Features {
|
||||
features = append(features, f.Feature)
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
func (l *Layer) GetNamespaces() []Namespace {
|
||||
namespaces := make([]Namespace, 0, len(l.Namespaces)+len(l.Features))
|
||||
for _, ns := range l.Namespaces {
|
||||
namespaces = append(namespaces, ns.Namespace)
|
||||
}
|
||||
for _, f := range l.Features {
|
||||
if f.PotentialNamespace.Valid() {
|
||||
namespaces = append(namespaces, f.PotentialNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
return namespaces
|
||||
}
|
||||
|
||||
// LayerNamespace is a namespace with detection information.
|
||||
type LayerNamespace struct {
|
||||
Namespace `json:"namespace"`
|
||||
|
||||
// By is the detector found the namespace.
|
||||
By Detector `json:"by"`
|
||||
}
|
||||
|
||||
// LayerFeature is a feature with detection information.
|
||||
type LayerFeature struct {
|
||||
Feature `json:"feature"`
|
||||
|
||||
// By is the detector found the feature.
|
||||
By Detector `json:"by"`
|
||||
PotentialNamespace Namespace `json:"potentialNamespace"`
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// MetadataMap is for storing the metadata returned by vulnerability database.
|
||||
type MetadataMap map[string]interface{}
|
||||
|
||||
func (mm *MetadataMap) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// github.com/lib/pq decodes TEXT/VARCHAR fields into strings.
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
panic("got type other than []byte from database")
|
||||
}
|
||||
return json.Unmarshal([]byte(val), mm)
|
||||
}
|
||||
|
||||
func (mm *MetadataMap) Value() (driver.Value, error) {
|
||||
json, err := json.Marshal(*mm)
|
||||
return string(json), err
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ID is only meant to be used by database implementations and should never be used for anything else.
|
||||
type Model struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
type Layer struct {
|
||||
Model
|
||||
|
||||
Name string
|
||||
EngineVersion int
|
||||
Parent *Layer
|
||||
Namespace *Namespace
|
||||
Features []FeatureVersion
|
||||
}
|
||||
|
||||
type Namespace struct {
|
||||
Model
|
||||
|
||||
Name string
|
||||
VersionFormat string
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
Model
|
||||
|
||||
Name string
|
||||
Namespace Namespace
|
||||
}
|
||||
|
||||
type FeatureVersion struct {
|
||||
Model
|
||||
|
||||
Feature Feature
|
||||
Version string
|
||||
AffectedBy []Vulnerability
|
||||
|
||||
// For output purposes. Only make sense when the feature version is in the context of an image.
|
||||
AddedBy Layer
|
||||
}
|
||||
|
||||
type Vulnerability struct {
|
||||
Model
|
||||
|
||||
Name string
|
||||
Namespace Namespace
|
||||
|
||||
Description string
|
||||
Link string
|
||||
Severity Severity
|
||||
|
||||
Metadata MetadataMap
|
||||
|
||||
FixedIn []FeatureVersion
|
||||
LayersIntroducingVulnerability []Layer
|
||||
|
||||
// For output purposes. Only make sense when the vulnerability
|
||||
// is already about a specific Feature/FeatureVersion.
|
||||
FixedBy string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type MetadataMap map[string]interface{}
|
||||
|
||||
func (mm *MetadataMap) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// github.com/lib/pq decodes TEXT/VARCHAR fields into strings.
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
panic("got type other than []byte from database")
|
||||
}
|
||||
return json.Unmarshal([]byte(val), mm)
|
||||
}
|
||||
|
||||
func (mm *MetadataMap) Value() (driver.Value, error) {
|
||||
json, err := json.Marshal(*mm)
|
||||
return string(json), err
|
||||
}
|
||||
|
||||
type VulnerabilityNotification struct {
|
||||
Model
|
||||
|
||||
Name string
|
||||
|
||||
Created time.Time
|
||||
Notified time.Time
|
||||
Deleted time.Time
|
||||
|
||||
OldVulnerability *Vulnerability
|
||||
NewVulnerability *Vulnerability
|
||||
}
|
||||
|
||||
type VulnerabilityNotificationPageNumber struct {
|
||||
// -1 means that we reached the end already.
|
||||
OldVulnerability int
|
||||
NewVulnerability int
|
||||
}
|
||||
|
||||
var VulnerabilityNotificationFirstPage = VulnerabilityNotificationPageNumber{0, 0}
|
||||
var NoVulnerabilityNotificationPage = VulnerabilityNotificationPageNumber{-1, -1}
|
@ -1,34 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
// Namespace is the contextual information around features.
|
||||
//
|
||||
// e.g. Debian:7, NodeJS.
|
||||
type Namespace struct {
|
||||
Name string `json:"name"`
|
||||
VersionFormat string `json:"versionFormat"`
|
||||
}
|
||||
|
||||
func NewNamespace(name string, versionFormat string) *Namespace {
|
||||
return &Namespace{name, versionFormat}
|
||||
}
|
||||
|
||||
func (ns *Namespace) Valid() bool {
|
||||
if ns.Name == "" || ns.VersionFormat == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/clair/pkg/pagination"
|
||||
)
|
||||
|
||||
// NotificationHook is a message sent to another service to inform of a change
|
||||
// to a Vulnerability or the Ancestries affected by a Vulnerability. It contains
|
||||
// the name of a notification that should be read and marked as read via the
|
||||
// API.
|
||||
type NotificationHook struct {
|
||||
Name string
|
||||
|
||||
Created time.Time
|
||||
Notified time.Time
|
||||
Deleted time.Time
|
||||
}
|
||||
|
||||
// VulnerabilityNotification is a notification for vulnerability changes.
|
||||
type VulnerabilityNotification struct {
|
||||
NotificationHook
|
||||
|
||||
Old *Vulnerability
|
||||
New *Vulnerability
|
||||
}
|
||||
|
||||
// VulnerabilityNotificationWithVulnerable is a notification for vulnerability
|
||||
// changes with vulnerable ancestries.
|
||||
type VulnerabilityNotificationWithVulnerable struct {
|
||||
NotificationHook
|
||||
|
||||
Old *PagedVulnerableAncestries
|
||||
New *PagedVulnerableAncestries
|
||||
}
|
||||
|
||||
// PagedVulnerableAncestries is a vulnerability with a page of affected
|
||||
// ancestries each with a special index attached for streaming purpose. The
|
||||
// current page number and next page number are for navigate.
|
||||
type PagedVulnerableAncestries struct {
|
||||
Vulnerability
|
||||
|
||||
// Affected is a map of special indexes to Ancestries, which the pair
|
||||
// should be unique in a stream. Every indexes in the map should be larger
|
||||
// than previous page.
|
||||
Affected map[int]string
|
||||
|
||||
Limit int
|
||||
Current pagination.Token
|
||||
Next pagination.Token
|
||||
|
||||
// End signals the end of the pages.
|
||||
End bool
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ancestry
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/layer"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
const (
|
||||
insertAncestry = `
|
||||
INSERT INTO ancestry (name) VALUES ($1) RETURNING id`
|
||||
|
||||
findAncestryID = `SELECT id FROM ancestry WHERE name = $1`
|
||||
removeAncestry = `DELETE FROM ancestry WHERE name = $1`
|
||||
|
||||
insertAncestryFeatures = `
|
||||
INSERT INTO ancestry_feature
|
||||
(ancestry_layer_id, namespaced_feature_id, feature_detector_id, namespace_detector_id) VALUES
|
||||
($1, $2, $3, $4)`
|
||||
)
|
||||
|
||||
func FindAncestry(tx *sql.Tx, name string) (database.Ancestry, bool, error) {
|
||||
var (
|
||||
ancestry = database.Ancestry{Name: name}
|
||||
err error
|
||||
)
|
||||
|
||||
id, ok, err := FindAncestryID(tx, name)
|
||||
if !ok || err != nil {
|
||||
return ancestry, ok, err
|
||||
}
|
||||
|
||||
if ancestry.By, err = FindAncestryDetectors(tx, id); err != nil {
|
||||
return ancestry, false, err
|
||||
}
|
||||
|
||||
if ancestry.Layers, err = FindAncestryLayers(tx, id); err != nil {
|
||||
return ancestry, false, err
|
||||
}
|
||||
|
||||
return ancestry, true, nil
|
||||
}
|
||||
|
||||
func UpsertAncestry(tx *sql.Tx, ancestry database.Ancestry) error {
|
||||
if !ancestry.Valid() {
|
||||
return database.ErrInvalidParameters
|
||||
}
|
||||
|
||||
if err := RemoveAncestry(tx, ancestry.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := InsertAncestry(tx, ancestry.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
detectorIDs, err := detector.FindDetectorIDs(tx, ancestry.By)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert ancestry metadata
|
||||
if err := InsertAncestryDetectors(tx, id, detectorIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
layers := make([]string, 0, len(ancestry.Layers))
|
||||
for _, l := range ancestry.Layers {
|
||||
layers = append(layers, l.Hash)
|
||||
}
|
||||
|
||||
layerIDs, ok, err := layer.FindLayerIDs(tx, layers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Error("layer cannot be found, this indicates that the internal logic of calling UpsertAncestry is wrong or the database is corrupted.")
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
|
||||
ancestryLayerIDs, err := InsertAncestryLayers(tx, id, layerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, id := range ancestryLayerIDs {
|
||||
if err := InsertAncestryFeatures(tx, id, ancestry.Layers[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertAncestry(tx *sql.Tx, name string) (int64, error) {
|
||||
var id int64
|
||||
err := tx.QueryRow(insertAncestry, name).Scan(&id)
|
||||
if err != nil {
|
||||
if util.IsErrUniqueViolation(err) {
|
||||
return 0, util.HandleError("insertAncestry", errors.New("other Go-routine is processing this ancestry (skip)"))
|
||||
}
|
||||
|
||||
return 0, util.HandleError("insertAncestry", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func FindAncestryID(tx *sql.Tx, name string) (int64, bool, error) {
|
||||
var id sql.NullInt64
|
||||
if err := tx.QueryRow(findAncestryID, name).Scan(&id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
return 0, false, util.HandleError("findAncestryID", err)
|
||||
}
|
||||
|
||||
return id.Int64, true, nil
|
||||
}
|
||||
|
||||
func RemoveAncestry(tx *sql.Tx, name string) error {
|
||||
result, err := tx.Exec(removeAncestry, name)
|
||||
if err != nil {
|
||||
return util.HandleError("removeAncestry", err)
|
||||
}
|
||||
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return util.HandleError("removeAncestry", err)
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
log.WithField("ancestry", name).Debug("removed ancestry")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ancestry
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
var selectAncestryDetectors = `
|
||||
SELECT d.name, d.version, d.dtype
|
||||
FROM ancestry_detector, detector AS d
|
||||
WHERE ancestry_detector.detector_id = d.id AND ancestry_detector.ancestry_id = $1;`
|
||||
|
||||
var insertAncestryDetectors = `
|
||||
INSERT INTO ancestry_detector (ancestry_id, detector_id)
|
||||
SELECT $1, $2
|
||||
WHERE NOT EXISTS (SELECT id FROM ancestry_detector WHERE ancestry_id = $1 AND detector_id = $2)`
|
||||
|
||||
func FindAncestryDetectors(tx *sql.Tx, id int64) ([]database.Detector, error) {
|
||||
detectors, err := detector.GetDetectors(tx, selectAncestryDetectors, id)
|
||||
return detectors, err
|
||||
}
|
||||
|
||||
func InsertAncestryDetectors(tx *sql.Tx, ancestryID int64, detectorIDs []int64) error {
|
||||
for _, detectorID := range detectorIDs {
|
||||
if _, err := tx.Exec(insertAncestryDetectors, ancestryID, detectorID); err != nil {
|
||||
return util.HandleError("insertAncestryDetectors", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ancestry
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/feature"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
const findAncestryFeatures = `
|
||||
SELECT namespace.name, namespace.version_format, feature.name,
|
||||
feature.version, feature.version_format, feature_type.name, ancestry_layer.ancestry_index,
|
||||
ancestry_feature.feature_detector_id, ancestry_feature.namespace_detector_id
|
||||
FROM namespace, feature, feature_type, namespaced_feature, ancestry_layer, ancestry_feature
|
||||
WHERE ancestry_layer.ancestry_id = $1
|
||||
AND feature_type.id = feature.type
|
||||
AND ancestry_feature.ancestry_layer_id = ancestry_layer.id
|
||||
AND ancestry_feature.namespaced_feature_id = namespaced_feature.id
|
||||
AND namespaced_feature.feature_id = feature.id
|
||||
AND namespaced_feature.namespace_id = namespace.id`
|
||||
|
||||
func FindAncestryFeatures(tx *sql.Tx, ancestryID int64, detectors detector.DetectorMap) (map[int64][]database.AncestryFeature, error) {
|
||||
// ancestry_index -> ancestry features
|
||||
featureMap := make(map[int64][]database.AncestryFeature)
|
||||
// retrieve ancestry layer's namespaced features
|
||||
rows, err := tx.Query(findAncestryFeatures, ancestryID)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("findAncestryFeatures", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
featureDetectorID int64
|
||||
namespaceDetectorID sql.NullInt64
|
||||
feature database.NamespacedFeature
|
||||
// index is used to determine which layer the feature belongs to.
|
||||
index sql.NullInt64
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&feature.Namespace.Name,
|
||||
&feature.Namespace.VersionFormat,
|
||||
&feature.Feature.Name,
|
||||
&feature.Feature.Version,
|
||||
&feature.Feature.VersionFormat,
|
||||
&feature.Feature.Type,
|
||||
&index,
|
||||
&featureDetectorID,
|
||||
&namespaceDetectorID,
|
||||
); err != nil {
|
||||
return nil, util.HandleError("findAncestryFeatures", err)
|
||||
}
|
||||
|
||||
if feature.Feature.VersionFormat != feature.Namespace.VersionFormat {
|
||||
// Feature must have the same version format as the associated
|
||||
// namespace version format.
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
|
||||
fDetector, ok := detectors.ByID[featureDetectorID]
|
||||
if !ok {
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
|
||||
var nsDetector database.Detector
|
||||
if !namespaceDetectorID.Valid {
|
||||
nsDetector = database.Detector{}
|
||||
} else {
|
||||
nsDetector, ok = detectors.ByID[namespaceDetectorID.Int64]
|
||||
if !ok {
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
}
|
||||
|
||||
featureMap[index.Int64] = append(featureMap[index.Int64], database.AncestryFeature{
|
||||
NamespacedFeature: feature,
|
||||
FeatureBy: fDetector,
|
||||
NamespaceBy: nsDetector,
|
||||
})
|
||||
}
|
||||
|
||||
return featureMap, nil
|
||||
}
|
||||
|
||||
func InsertAncestryFeatures(tx *sql.Tx, ancestryLayerID int64, layer database.AncestryLayer) error {
|
||||
detectors, err := detector.FindAllDetectors(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nsFeatureIDs, err := feature.FindNamespacedFeatureIDs(tx, layer.GetFeatures())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find the detectors for each feature
|
||||
stmt, err := tx.Prepare(insertAncestryFeatures)
|
||||
if err != nil {
|
||||
return util.HandleError("insertAncestryFeatures", err)
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
for index, id := range nsFeatureIDs {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
|
||||
var namespaceDetectorID sql.NullInt64
|
||||
var ok bool
|
||||
namespaceDetectorID.Int64, ok = detectors.ByValue[layer.Features[index].NamespaceBy]
|
||||
if ok {
|
||||
namespaceDetectorID.Valid = true
|
||||
}
|
||||
|
||||
featureDetectorID, ok := detectors.ByValue[layer.Features[index].FeatureBy]
|
||||
if !ok {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
|
||||
if _, err := stmt.Exec(ancestryLayerID, id, featureDetectorID, namespaceDetectorID); err != nil {
|
||||
return util.HandleError("insertAncestryFeatures", commonerr.CombineErrors(err, stmt.Close()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ancestry
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
findAncestryLayerHashes = `
|
||||
SELECT layer.hash, ancestry_layer.ancestry_index
|
||||
FROM layer, ancestry_layer
|
||||
WHERE ancestry_layer.ancestry_id = $1
|
||||
AND ancestry_layer.layer_id = layer.id
|
||||
ORDER BY ancestry_layer.ancestry_index ASC`
|
||||
insertAncestryLayers = `
|
||||
INSERT INTO ancestry_layer (ancestry_id, ancestry_index, layer_id) VALUES ($1, $2, $3)
|
||||
RETURNING id`
|
||||
)
|
||||
|
||||
func FindAncestryLayerHashes(tx *sql.Tx, ancestryID int64) (map[int64]string, error) {
|
||||
// retrieve layer indexes and hashes
|
||||
rows, err := tx.Query(findAncestryLayerHashes, ancestryID)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("findAncestryLayerHashes", err)
|
||||
}
|
||||
|
||||
layerHashes := map[int64]string{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
hash string
|
||||
index int64
|
||||
)
|
||||
|
||||
if err = rows.Scan(&hash, &index); err != nil {
|
||||
return nil, util.HandleError("findAncestryLayerHashes", err)
|
||||
}
|
||||
|
||||
if _, ok := layerHashes[index]; ok {
|
||||
// one ancestry index should correspond to only one layer
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
|
||||
layerHashes[index] = hash
|
||||
}
|
||||
|
||||
return layerHashes, nil
|
||||
}
|
||||
|
||||
// insertAncestryLayers inserts the ancestry layers along with its content into
|
||||
// the database. The layers are 0 based indexed in the original order.
|
||||
func InsertAncestryLayers(tx *sql.Tx, ancestryID int64, layers []int64) ([]int64, error) {
|
||||
stmt, err := tx.Prepare(insertAncestryLayers)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("insertAncestryLayers", err)
|
||||
}
|
||||
|
||||
ancestryLayerIDs := []int64{}
|
||||
for index, layerID := range layers {
|
||||
var ancestryLayerID sql.NullInt64
|
||||
if err := stmt.QueryRow(ancestryID, index, layerID).Scan(&ancestryLayerID); err != nil {
|
||||
return nil, util.HandleError("insertAncestryLayers", commonerr.CombineErrors(err, stmt.Close()))
|
||||
}
|
||||
|
||||
if !ancestryLayerID.Valid {
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
|
||||
ancestryLayerIDs = append(ancestryLayerIDs, ancestryLayerID.Int64)
|
||||
}
|
||||
|
||||
if err := stmt.Close(); err != nil {
|
||||
return nil, util.HandleError("insertAncestryLayers", err)
|
||||
}
|
||||
|
||||
return ancestryLayerIDs, nil
|
||||
}
|
||||
|
||||
func FindAncestryLayers(tx *sql.Tx, id int64) ([]database.AncestryLayer, error) {
|
||||
detectors, err := detector.FindAllDetectors(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layerMap, err := FindAncestryLayerHashes(tx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
featureMap, err := FindAncestryFeatures(tx, id, detectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layers := make([]database.AncestryLayer, len(layerMap))
|
||||
for index, layer := range layerMap {
|
||||
// index MUST match the ancestry layer slice index.
|
||||
if layers[index].Hash == "" && len(layers[index].Features) == 0 {
|
||||
layers[index] = database.AncestryLayer{
|
||||
Hash: layer,
|
||||
Features: featureMap[index],
|
||||
}
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"ancestry ID": id,
|
||||
"duplicated ancestry index": index,
|
||||
}).WithError(database.ErrInconsistent).Error("ancestry layers with same ancestry_index is not allowed")
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ancestry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
var upsertAncestryTests = []struct {
|
||||
in *database.Ancestry
|
||||
err string
|
||||
title string
|
||||
}{
|
||||
{
|
||||
title: "ancestry with invalid layer",
|
||||
in: &database.Ancestry{
|
||||
Name: "a1",
|
||||
Layers: []database.AncestryLayer{
|
||||
{
|
||||
Hash: "layer-non-existing",
|
||||
},
|
||||
},
|
||||
},
|
||||
err: database.ErrMissingEntities.Error(),
|
||||
},
|
||||
{
|
||||
title: "ancestry with invalid name",
|
||||
in: &database.Ancestry{},
|
||||
err: database.ErrInvalidParameters.Error(),
|
||||
},
|
||||
{
|
||||
title: "new valid ancestry",
|
||||
in: &database.Ancestry{
|
||||
Name: "a",
|
||||
Layers: []database.AncestryLayer{{Hash: "layer-0"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ancestry with invalid feature",
|
||||
in: &database.Ancestry{
|
||||
Name: "a",
|
||||
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
|
||||
Layers: []database.AncestryLayer{{Hash: "layer-1", Features: []database.AncestryFeature{
|
||||
{testutil.FakeNamespacedFeatures[1], testutil.FakeDetector[1], testutil.FakeDetector[2]},
|
||||
}}},
|
||||
},
|
||||
err: database.ErrMissingEntities.Error(),
|
||||
},
|
||||
{
|
||||
title: "replace old ancestry",
|
||||
in: &database.Ancestry{
|
||||
Name: "a",
|
||||
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
|
||||
Layers: []database.AncestryLayer{
|
||||
{"layer-1", []database.AncestryFeature{{testutil.RealNamespacedFeatures[1], testutil.RealDetectors[2], testutil.RealDetectors[1]}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestUpsertAncestry(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestUpsertAncestry")
|
||||
defer cleanup()
|
||||
|
||||
for _, test := range upsertAncestryTests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
err := UpsertAncestry(tx, *test.in)
|
||||
if test.err != "" {
|
||||
assert.EqualError(t, err, test.err, "unexpected error")
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
actual, ok, err := FindAncestry(tx, test.in.Name)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, ok)
|
||||
database.AssertAncestryEqual(t, test.in, &actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var findAncestryTests = []struct {
|
||||
title string
|
||||
in string
|
||||
|
||||
ancestry *database.Ancestry
|
||||
err string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
title: "missing ancestry",
|
||||
in: "ancestry-non",
|
||||
err: "",
|
||||
ancestry: nil,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
title: "valid ancestry",
|
||||
in: "ancestry-2",
|
||||
err: "",
|
||||
ok: true,
|
||||
ancestry: testutil.TakeAncestryPointerFromMap(testutil.RealAncestries, 2),
|
||||
},
|
||||
}
|
||||
|
||||
func TestFindAncestry(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestFindAncestry")
|
||||
defer cleanup()
|
||||
|
||||
for _, test := range findAncestryTests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
ancestry, ok, err := FindAncestry(tx, test.in)
|
||||
if test.err != "" {
|
||||
assert.EqualError(t, err, test.err, "unexpected error")
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, test.ok, ok)
|
||||
if test.ok {
|
||||
database.AssertAncestryEqual(t, test.ancestry, &ancestry)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt/dpkg"
|
||||
)
|
||||
|
||||
const (
|
||||
numVulnerabilities = 100
|
||||
numFeatureVersions = 100
|
||||
)
|
||||
|
||||
func TestRaceAffects(t *testing.T) {
|
||||
datastore, err := openDatabaseForTest("RaceAffects", false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer datastore.Close()
|
||||
|
||||
// Insert the Feature on which we'll work.
|
||||
feature := database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestRaceAffectsFeatureNamespace1",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestRaceAffecturesFeature1",
|
||||
}
|
||||
_, err = datastore.insertFeature(feature)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize random generator and enforce max procs.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
// Generate FeatureVersions.
|
||||
featureVersions := make([]database.FeatureVersion, numFeatureVersions)
|
||||
for i := 0; i < numFeatureVersions; i++ {
|
||||
version := rand.Intn(numFeatureVersions)
|
||||
|
||||
featureVersions[i] = database.FeatureVersion{
|
||||
Feature: feature,
|
||||
Version: strconv.Itoa(version),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate vulnerabilities.
|
||||
// They are mapped by fixed version, which will make verification really easy afterwards.
|
||||
vulnerabilities := make(map[int][]database.Vulnerability)
|
||||
for i := 0; i < numVulnerabilities; i++ {
|
||||
version := rand.Intn(numFeatureVersions) + 1
|
||||
|
||||
// if _, ok := vulnerabilities[version]; !ok {
|
||||
// vulnerabilities[version] = make([]database.Vulnerability)
|
||||
// }
|
||||
|
||||
vulnerability := database.Vulnerability{
|
||||
Name: uuid.New(),
|
||||
Namespace: feature.Namespace,
|
||||
FixedIn: []database.FeatureVersion{
|
||||
{
|
||||
Feature: feature,
|
||||
Version: strconv.Itoa(version),
|
||||
},
|
||||
},
|
||||
Severity: database.UnknownSeverity,
|
||||
}
|
||||
|
||||
vulnerabilities[version] = append(vulnerabilities[version], vulnerability)
|
||||
}
|
||||
|
||||
// Insert featureversions and vulnerabilities in parallel.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for _, vulnerabilitiesM := range vulnerabilities {
|
||||
for _, vulnerability := range vulnerabilitiesM {
|
||||
err = datastore.InsertVulnerabilities([]database.Vulnerability{vulnerability}, true)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
fmt.Println("finished to insert vulnerabilities")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < len(featureVersions); i++ {
|
||||
featureVersions[i].ID, err = datastore.insertFeatureVersion(featureVersions[i])
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
fmt.Println("finished to insert featureVersions")
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify consistency now.
|
||||
var actualAffectedNames []string
|
||||
var expectedAffectedNames []string
|
||||
|
||||
for _, featureVersion := range featureVersions {
|
||||
featureVersionVersion, _ := strconv.Atoi(featureVersion.Version)
|
||||
|
||||
// Get actual affects.
|
||||
rows, err := datastore.Query(searchComplexTestFeatureVersionAffects,
|
||||
featureVersion.ID)
|
||||
assert.Nil(t, err)
|
||||
defer rows.Close()
|
||||
|
||||
var vulnName string
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&vulnName)
|
||||
if !assert.Nil(t, err) {
|
||||
continue
|
||||
}
|
||||
actualAffectedNames = append(actualAffectedNames, vulnName)
|
||||
}
|
||||
if assert.Nil(t, rows.Err()) {
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
// Get expected affects.
|
||||
for i := numVulnerabilities; i > featureVersionVersion; i-- {
|
||||
for _, vulnerability := range vulnerabilities[i] {
|
||||
expectedAffectedNames = append(expectedAffectedNames, vulnerability.Name)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, compareStringLists(expectedAffectedNames, actualAffectedNames), 0)
|
||||
assert.Len(t, compareStringLists(actualAffectedNames, expectedAffectedNames), 0)
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
// Copyright 2018 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package detector
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
const (
|
||||
soiDetector = `
|
||||
INSERT INTO detector (name, version, dtype)
|
||||
SELECT CAST ($1 AS TEXT), CAST ($2 AS TEXT), CAST ($3 AS detector_type )
|
||||
WHERE NOT EXISTS (SELECT id FROM detector WHERE name = $1 AND version = $2 AND dtype = $3);`
|
||||
|
||||
findDetectorID = `SELECT id FROM detector WHERE name = $1 AND version = $2 AND dtype = $3`
|
||||
findAllDetectors = `SELECT id, name, version, dtype FROM detector`
|
||||
)
|
||||
|
||||
type DetectorMap struct {
|
||||
ByID map[int64]database.Detector
|
||||
ByValue map[database.Detector]int64
|
||||
}
|
||||
|
||||
func PersistDetectors(tx *sql.Tx, detectors []database.Detector) error {
|
||||
for _, d := range detectors {
|
||||
if !d.Valid() {
|
||||
log.WithField("detector", d).Debug("Invalid Detector")
|
||||
return database.ErrInvalidParameters
|
||||
}
|
||||
|
||||
r, err := tx.Exec(soiDetector, d.Name, d.Version, d.DType)
|
||||
if err != nil {
|
||||
return util.HandleError("soiDetector", err)
|
||||
}
|
||||
|
||||
count, err := r.RowsAffected()
|
||||
if err != nil {
|
||||
return util.HandleError("soiDetector", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
log.Debug("detector already exists: ", d)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findDetectorIDs retrieve ids of the detectors from the database, if any is not
|
||||
// found, return the error.
|
||||
func FindDetectorIDs(tx *sql.Tx, detectors []database.Detector) ([]int64, error) {
|
||||
ids := []int64{}
|
||||
for _, d := range detectors {
|
||||
id := sql.NullInt64{}
|
||||
err := tx.QueryRow(findDetectorID, d.Name, d.Version, d.DType).Scan(&id)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("findDetectorID", err)
|
||||
}
|
||||
|
||||
if !id.Valid {
|
||||
return nil, database.ErrInconsistent
|
||||
}
|
||||
|
||||
ids = append(ids, id.Int64)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func GetDetectors(tx *sql.Tx, query string, id int64) ([]database.Detector, error) {
|
||||
rows, err := tx.Query(query, id)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("getDetectors", err)
|
||||
}
|
||||
|
||||
detectors := []database.Detector{}
|
||||
for rows.Next() {
|
||||
d := database.Detector{}
|
||||
err := rows.Scan(&d.Name, &d.Version, &d.DType)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("getDetectors", err)
|
||||
}
|
||||
|
||||
if !d.Valid() {
|
||||
return nil, database.ErrInvalidDetector
|
||||
}
|
||||
|
||||
detectors = append(detectors, d)
|
||||
}
|
||||
|
||||
return detectors, nil
|
||||
}
|
||||
|
||||
func FindAllDetectors(tx *sql.Tx) (DetectorMap, error) {
|
||||
rows, err := tx.Query(findAllDetectors)
|
||||
if err != nil {
|
||||
return DetectorMap{}, util.HandleError("searchAllDetectors", err)
|
||||
}
|
||||
|
||||
detectors := DetectorMap{ByID: make(map[int64]database.Detector), ByValue: make(map[database.Detector]int64)}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
d database.Detector
|
||||
)
|
||||
if err := rows.Scan(&id, &d.Name, &d.Version, &d.DType); err != nil {
|
||||
return DetectorMap{}, util.HandleError("searchAllDetectors", err)
|
||||
}
|
||||
|
||||
detectors.ByID[id] = d
|
||||
detectors.ByValue[d] = id
|
||||
}
|
||||
|
||||
return detectors, nil
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
// Copyright 2018 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package detector
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/deckarep/golang-set"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
func testGetAllDetectors(tx *sql.Tx) []database.Detector {
|
||||
query := `SELECT name, version, dtype FROM detector`
|
||||
rows, err := tx.Query(query)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
detectors := []database.Detector{}
|
||||
for rows.Next() {
|
||||
d := database.Detector{}
|
||||
if err := rows.Scan(&d.Name, &d.Version, &d.DType); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
detectors = append(detectors, d)
|
||||
}
|
||||
|
||||
return detectors
|
||||
}
|
||||
|
||||
var persistDetectorTests = []struct {
|
||||
title string
|
||||
in []database.Detector
|
||||
err string
|
||||
}{
|
||||
{
|
||||
title: "invalid detector",
|
||||
in: []database.Detector{
|
||||
{},
|
||||
database.NewFeatureDetector("name", "2.0"),
|
||||
},
|
||||
err: database.ErrInvalidParameters.Error(),
|
||||
},
|
||||
{
|
||||
title: "invalid detector 2",
|
||||
in: []database.Detector{
|
||||
database.NewFeatureDetector("name", "2.0"),
|
||||
{"name", "1.0", "random not valid dtype"},
|
||||
},
|
||||
err: database.ErrInvalidParameters.Error(),
|
||||
},
|
||||
{
|
||||
title: "detectors with some different fields",
|
||||
in: []database.Detector{
|
||||
database.NewFeatureDetector("name", "2.0"),
|
||||
database.NewFeatureDetector("name", "1.0"),
|
||||
database.NewNamespaceDetector("name", "1.0"),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "duplicated detectors (parameter level)",
|
||||
in: []database.Detector{
|
||||
database.NewFeatureDetector("name", "1.0"),
|
||||
database.NewFeatureDetector("name", "1.0"),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "duplicated detectors (db level)",
|
||||
in: []database.Detector{
|
||||
database.NewNamespaceDetector("os-release", "1.0"),
|
||||
database.NewNamespaceDetector("os-release", "1.0"),
|
||||
database.NewFeatureDetector("dpkg", "1.0"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestPersistDetector(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "PersistDetector")
|
||||
defer cleanup()
|
||||
|
||||
for _, test := range persistDetectorTests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
err := PersistDetectors(tx, test.in)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
return
|
||||
}
|
||||
|
||||
detectors := testGetAllDetectors(tx)
|
||||
|
||||
// ensure no duplicated detectors
|
||||
detectorSet := mapset.NewSet()
|
||||
for _, d := range detectors {
|
||||
require.False(t, detectorSet.Contains(d), "duplicated: %v", d)
|
||||
detectorSet.Add(d)
|
||||
}
|
||||
|
||||
// ensure all persisted detectors are actually saved
|
||||
for _, d := range test.in {
|
||||
require.True(t, detectorSet.Contains(d), "detector: %v, detectors: %v", d, detectorSet)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,245 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
func (pgSQL *pgSQL) insertFeature(feature database.Feature) (int, error) {
|
||||
if feature.Name == "" {
|
||||
return 0, commonerr.NewBadRequestError("could not find/insert invalid Feature")
|
||||
}
|
||||
|
||||
// Do cache lookup.
|
||||
if pgSQL.cache != nil {
|
||||
promCacheQueriesTotal.WithLabelValues("feature").Inc()
|
||||
id, found := pgSQL.cache.Get("feature:" + feature.Namespace.Name + ":" + feature.Name)
|
||||
if found {
|
||||
promCacheHitsTotal.WithLabelValues("feature").Inc()
|
||||
return id.(int), nil
|
||||
}
|
||||
}
|
||||
|
||||
// We do `defer observeQueryTime` here because we don't want to observe cached features.
|
||||
defer observeQueryTime("insertFeature", "all", time.Now())
|
||||
|
||||
// Find or create Namespace.
|
||||
namespaceID, err := pgSQL.insertNamespace(feature.Namespace)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Find or create Feature.
|
||||
var id int
|
||||
err = pgSQL.QueryRow(soiFeature, feature.Name, namespaceID).Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handleError("soiFeature", err)
|
||||
}
|
||||
|
||||
if pgSQL.cache != nil {
|
||||
pgSQL.cache.Add("feature:"+feature.Namespace.Name+":"+feature.Name, id)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (pgSQL *pgSQL) insertFeatureVersion(fv database.FeatureVersion) (id int, err error) {
|
||||
err = versionfmt.Valid(fv.Feature.Namespace.VersionFormat, fv.Version)
|
||||
if err != nil {
|
||||
return 0, commonerr.NewBadRequestError("could not find/insert invalid FeatureVersion")
|
||||
}
|
||||
|
||||
// Do cache lookup.
|
||||
cacheIndex := strings.Join([]string{"featureversion", fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":")
|
||||
if pgSQL.cache != nil {
|
||||
promCacheQueriesTotal.WithLabelValues("featureversion").Inc()
|
||||
id, found := pgSQL.cache.Get(cacheIndex)
|
||||
if found {
|
||||
promCacheHitsTotal.WithLabelValues("featureversion").Inc()
|
||||
return id.(int), nil
|
||||
}
|
||||
}
|
||||
|
||||
// We do `defer observeQueryTime` here because we don't want to observe cached featureversions.
|
||||
defer observeQueryTime("insertFeatureVersion", "all", time.Now())
|
||||
|
||||
// Find or create Feature first.
|
||||
t := time.Now()
|
||||
featureID, err := pgSQL.insertFeature(fv.Feature)
|
||||
observeQueryTime("insertFeatureVersion", "insertFeature", t)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fv.Feature.ID = featureID
|
||||
|
||||
// Try to find the FeatureVersion.
|
||||
//
|
||||
// In a populated database, the likelihood of the FeatureVersion already being there is high.
|
||||
// If we can find it here, we then avoid using a transaction and locking the database.
|
||||
err = pgSQL.QueryRow(searchFeatureVersion, featureID, fv.Version).Scan(&fv.ID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return 0, handleError("searchFeatureVersion", err)
|
||||
}
|
||||
if err == nil {
|
||||
if pgSQL.cache != nil {
|
||||
pgSQL.cache.Add(cacheIndex, fv.ID)
|
||||
}
|
||||
|
||||
return fv.ID, nil
|
||||
}
|
||||
|
||||
// Begin transaction.
|
||||
tx, err := pgSQL.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return 0, handleError("insertFeatureVersion.Begin()", err)
|
||||
}
|
||||
|
||||
// Lock Vulnerability_Affects_FeatureVersion exclusively.
|
||||
// We want to prevent InsertVulnerability to modify it.
|
||||
promConcurrentLockVAFV.Inc()
|
||||
defer promConcurrentLockVAFV.Dec()
|
||||
t = time.Now()
|
||||
_, err = tx.Exec(lockVulnerabilityAffects)
|
||||
observeQueryTime("insertFeatureVersion", "lock", t)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return 0, handleError("insertFeatureVersion.lockVulnerabilityAffects", err)
|
||||
}
|
||||
|
||||
// Find or create FeatureVersion.
|
||||
var created bool
|
||||
|
||||
t = time.Now()
|
||||
err = tx.QueryRow(soiFeatureVersion, featureID, fv.Version).Scan(&created, &fv.ID)
|
||||
observeQueryTime("insertFeatureVersion", "soiFeatureVersion", t)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return 0, handleError("soiFeatureVersion", err)
|
||||
}
|
||||
|
||||
if !created {
|
||||
// The featureVersion already existed, no need to link it to
|
||||
// vulnerabilities.
|
||||
tx.Commit()
|
||||
|
||||
if pgSQL.cache != nil {
|
||||
pgSQL.cache.Add(cacheIndex, fv.ID)
|
||||
}
|
||||
|
||||
return fv.ID, nil
|
||||
}
|
||||
|
||||
// Link the new FeatureVersion with every vulnerabilities that affect it, by inserting in
|
||||
// Vulnerability_Affects_FeatureVersion.
|
||||
t = time.Now()
|
||||
err = linkFeatureVersionToVulnerabilities(tx, fv)
|
||||
observeQueryTime("insertFeatureVersion", "linkFeatureVersionToVulnerabilities", t)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit transaction.
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return 0, handleError("insertFeatureVersion.Commit()", err)
|
||||
}
|
||||
|
||||
if pgSQL.cache != nil {
|
||||
pgSQL.cache.Add(cacheIndex, fv.ID)
|
||||
}
|
||||
|
||||
return fv.ID, nil
|
||||
}
|
||||
|
||||
// TODO(Quentin-M): Batch me
|
||||
func (pgSQL *pgSQL) insertFeatureVersions(featureVersions []database.FeatureVersion) ([]int, error) {
|
||||
IDs := make([]int, 0, len(featureVersions))
|
||||
|
||||
for i := 0; i < len(featureVersions); i++ {
|
||||
id, err := pgSQL.insertFeatureVersion(featureVersions[i])
|
||||
if err != nil {
|
||||
return IDs, err
|
||||
}
|
||||
IDs = append(IDs, id)
|
||||
}
|
||||
|
||||
return IDs, nil
|
||||
}
|
||||
|
||||
type vulnerabilityAffectsFeatureVersion struct {
|
||||
vulnerabilityID int
|
||||
fixedInID int
|
||||
fixedInVersion string
|
||||
}
|
||||
|
||||
func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.FeatureVersion) error {
|
||||
// Select every vulnerability and the fixed version that affect this Feature.
|
||||
// TODO(Quentin-M): LIMIT
|
||||
rows, err := tx.Query(searchVulnerabilityFixedInFeature, featureVersion.Feature.ID)
|
||||
if err != nil {
|
||||
return handleError("searchVulnerabilityFixedInFeature", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var affects []vulnerabilityAffectsFeatureVersion
|
||||
for rows.Next() {
|
||||
var affect vulnerabilityAffectsFeatureVersion
|
||||
|
||||
err := rows.Scan(&affect.fixedInID, &affect.vulnerabilityID, &affect.fixedInVersion)
|
||||
if err != nil {
|
||||
return handleError("searchVulnerabilityFixedInFeature.Scan()", err)
|
||||
}
|
||||
|
||||
cmp, err := versionfmt.Compare(featureVersion.Feature.Namespace.VersionFormat, featureVersion.Version, affect.fixedInVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmp < 0 {
|
||||
// The version of the FeatureVersion we are inserting is lower than the fixed version on this
|
||||
// Vulnerability, thus, this FeatureVersion is affected by it.
|
||||
affects = append(affects, affect)
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return handleError("searchVulnerabilityFixedInFeature.Rows()", err)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Insert into Vulnerability_Affects_FeatureVersion.
|
||||
for _, affect := range affects {
|
||||
// TODO(Quentin-M): Batch me.
|
||||
_, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, affect.vulnerabilityID,
|
||||
featureVersion.ID, affect.fixedInID)
|
||||
if err != nil {
|
||||
return handleError("insertVulnerabilityAffectsFeatureVersion", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
func queryPersistFeature(count int) string {
|
||||
return util.QueryPersist(count,
|
||||
"feature",
|
||||
"feature_name_version_version_format_type_key",
|
||||
"name",
|
||||
"version",
|
||||
"version_format",
|
||||
"type")
|
||||
}
|
||||
|
||||
func querySearchFeatureID(featureCount int) string {
|
||||
return fmt.Sprintf(`
|
||||
SELECT id, name, version, version_format, type
|
||||
FROM Feature WHERE (name, version, version_format, type) IN (%s)`,
|
||||
util.QueryString(4, featureCount),
|
||||
)
|
||||
}
|
||||
|
||||
func PersistFeatures(tx *sql.Tx, features []database.Feature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sorting is needed before inserting into database to prevent deadlock.
|
||||
sort.Slice(features, func(i, j int) bool {
|
||||
return features[i].Name < features[j].Name ||
|
||||
features[i].Version < features[j].Version ||
|
||||
features[i].VersionFormat < features[j].VersionFormat
|
||||
})
|
||||
|
||||
// TODO(Sida): A better interface for bulk insertion is needed.
|
||||
keys := make([]interface{}, 0, len(features)*3)
|
||||
for _, f := range features {
|
||||
keys = append(keys, f.Name, f.Version, f.VersionFormat, types.ByName[f.Type])
|
||||
if f.Name == "" || f.Version == "" || f.VersionFormat == "" {
|
||||
return commonerr.NewBadRequestError("Empty feature name, version or version format is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(queryPersistFeature(len(features)), keys...)
|
||||
return util.HandleError("queryPersistFeature", err)
|
||||
}
|
||||
|
||||
func FindFeatureIDs(tx *sql.Tx, fs []database.Feature) ([]sql.NullInt64, error) {
|
||||
if len(fs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fMap := map[database.Feature]sql.NullInt64{}
|
||||
|
||||
keys := make([]interface{}, 0, len(fs)*4)
|
||||
for _, f := range fs {
|
||||
typeID := types.ByName[f.Type]
|
||||
keys = append(keys, f.Name, f.Version, f.VersionFormat, typeID)
|
||||
fMap[f] = sql.NullInt64{}
|
||||
}
|
||||
|
||||
rows, err := tx.Query(querySearchFeatureID(len(fs)), keys...)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("querySearchFeatureID", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
id sql.NullInt64
|
||||
f database.Feature
|
||||
)
|
||||
for rows.Next() {
|
||||
var typeID int
|
||||
err := rows.Scan(&id, &f.Name, &f.Version, &f.VersionFormat, &typeID)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("querySearchFeatureID", err)
|
||||
}
|
||||
|
||||
f.Type = types.ByID[typeID]
|
||||
fMap[f] = id
|
||||
}
|
||||
|
||||
ids := make([]sql.NullInt64, len(fs))
|
||||
for i, f := range fs {
|
||||
ids[i] = fMap[f]
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
func TestPersistFeatures(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTx(t, "TestPersistFeatures")
|
||||
defer cleanup()
|
||||
|
||||
invalid := database.Feature{}
|
||||
valid := *database.NewBinaryPackage("mount", "2.31.1-0.4ubuntu3.1", "dpkg")
|
||||
|
||||
// invalid
|
||||
require.NotNil(t, PersistFeatures(tx, []database.Feature{invalid}))
|
||||
// existing
|
||||
require.Nil(t, PersistFeatures(tx, []database.Feature{valid}))
|
||||
require.Nil(t, PersistFeatures(tx, []database.Feature{valid}))
|
||||
|
||||
features := selectAllFeatures(t, tx)
|
||||
assert.Equal(t, []database.Feature{valid}, features)
|
||||
}
|
||||
|
||||
func TestPersistNamespacedFeatures(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestPersistNamespacedFeatures")
|
||||
defer cleanup()
|
||||
|
||||
// existing features
|
||||
f1 := database.NewSourcePackage("ourchat", "0.5", "dpkg")
|
||||
// non-existing features
|
||||
f2 := database.NewSourcePackage("fake!", "", "")
|
||||
// exising namespace
|
||||
n1 := database.NewNamespace("debian:7", "dpkg")
|
||||
// non-existing namespace
|
||||
n2 := database.NewNamespace("debian:non", "dpkg")
|
||||
// existing namespaced feature
|
||||
nf1 := database.NewNamespacedFeature(n1, f1)
|
||||
// invalid namespaced feature
|
||||
nf2 := database.NewNamespacedFeature(n2, f2)
|
||||
// namespaced features with namespaces or features not in the database will
|
||||
// generate error.
|
||||
assert.Nil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{}))
|
||||
assert.NotNil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{*nf1, *nf2}))
|
||||
// valid case: insert nf3
|
||||
assert.Nil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{*nf1}))
|
||||
|
||||
all := listNamespacedFeatures(t, tx)
|
||||
assert.Contains(t, all, *nf1)
|
||||
}
|
||||
|
||||
func listNamespacedFeatures(t *testing.T, tx *sql.Tx) []database.NamespacedFeature {
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rows, err := tx.Query(`SELECT f.name, f.version, f.version_format, f.type, n.name, n.version_format
|
||||
FROM feature AS f, namespace AS n, namespaced_feature AS nf
|
||||
WHERE nf.feature_id = f.id AND nf.namespace_id = n.id`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
nf := []database.NamespacedFeature{}
|
||||
for rows.Next() {
|
||||
f := database.NamespacedFeature{}
|
||||
var typeID int
|
||||
err := rows.Scan(&f.Name, &f.Version, &f.VersionFormat, &typeID, &f.Namespace.Name, &f.Namespace.VersionFormat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
f.Type = types.ByID[typeID]
|
||||
nf = append(nf, f)
|
||||
}
|
||||
|
||||
return nf
|
||||
}
|
||||
|
||||
func selectAllFeatures(t *testing.T, tx *sql.Tx) []database.Feature {
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rows, err := tx.Query("SELECT name, version, version_format, type FROM feature")
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
fs := []database.Feature{}
|
||||
for rows.Next() {
|
||||
f := database.Feature{}
|
||||
var typeID int
|
||||
err := rows.Scan(&f.Name, &f.Version, &f.VersionFormat, &typeID)
|
||||
f.Type = types.ByID[typeID]
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
fs = append(fs, f)
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
func TestFindNamespacedFeatureIDs(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestFindNamespacedFeatureIDs")
|
||||
defer cleanup()
|
||||
|
||||
features := []database.NamespacedFeature{}
|
||||
expectedIDs := []int{}
|
||||
for id, feature := range testutil.RealNamespacedFeatures {
|
||||
features = append(features, feature)
|
||||
expectedIDs = append(expectedIDs, id)
|
||||
}
|
||||
|
||||
features = append(features, testutil.RealNamespacedFeatures[1]) // test duplicated
|
||||
expectedIDs = append(expectedIDs, 1)
|
||||
|
||||
namespace := testutil.RealNamespaces[1]
|
||||
features = append(features, *database.NewNamespacedFeature(&namespace, database.NewBinaryPackage("not-found", "1.0", "dpkg"))) // test not found feature
|
||||
|
||||
ids, err := FindNamespacedFeatureIDs(tx, features)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, ids, len(expectedIDs)+1)
|
||||
for i, id := range ids {
|
||||
if i == len(ids)-1 {
|
||||
require.False(t, id.Valid)
|
||||
} else {
|
||||
require.True(t, id.Valid)
|
||||
require.Equal(t, expectedIDs[i], int(id.Int64))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
)
|
||||
|
||||
const (
|
||||
selectAllFeatureTypes = `SELECT id, name FROM feature_type`
|
||||
)
|
||||
|
||||
type FeatureTypes struct {
|
||||
ByID map[int]database.FeatureType
|
||||
ByName map[database.FeatureType]int
|
||||
}
|
||||
|
||||
func newFeatureTypes() *FeatureTypes {
|
||||
return &FeatureTypes{make(map[int]database.FeatureType), make(map[database.FeatureType]int)}
|
||||
}
|
||||
|
||||
func GetFeatureTypeMap(tx *sql.Tx) (*FeatureTypes, error) {
|
||||
rows, err := tx.Query(selectAllFeatureTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types := newFeatureTypes()
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
name database.FeatureType
|
||||
)
|
||||
if err := rows.Scan(&id, &name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types.ByID[id] = name
|
||||
types.ByName[name] = id
|
||||
}
|
||||
|
||||
return types, nil
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
func TestGetFeatureTypeMap(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTx(t, "TestGetFeatureTypeMap")
|
||||
defer cleanup()
|
||||
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
require.Nil(t, err, err.Error())
|
||||
}
|
||||
|
||||
require.Equal(t, database.SourcePackage, types.ByID[1])
|
||||
require.Equal(t, database.BinaryPackage, types.ByID[2])
|
||||
require.Equal(t, 1, types.ByName[database.SourcePackage])
|
||||
require.Equal(t, 2, types.ByName[database.BinaryPackage])
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/namespace"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
var soiNamespacedFeature = `
|
||||
WITH new_feature_ns AS (
|
||||
INSERT INTO namespaced_feature(feature_id, namespace_id)
|
||||
SELECT CAST ($1 AS INTEGER), CAST ($2 AS INTEGER)
|
||||
WHERE NOT EXISTS ( SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2)
|
||||
RETURNING id
|
||||
)
|
||||
SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2
|
||||
UNION
|
||||
SELECT id FROM new_feature_ns`
|
||||
|
||||
func queryPersistNamespacedFeature(count int) string {
|
||||
return util.QueryPersist(count, "namespaced_feature",
|
||||
"namespaced_feature_namespace_id_feature_id_key",
|
||||
"feature_id",
|
||||
"namespace_id")
|
||||
}
|
||||
|
||||
func querySearchNamespacedFeature(nsfCount int) string {
|
||||
return fmt.Sprintf(`
|
||||
SELECT nf.id, f.name, f.version, f.version_format, t.name, n.name
|
||||
FROM namespaced_feature AS nf, feature AS f, namespace AS n, feature_type AS t
|
||||
WHERE nf.feature_id = f.id
|
||||
AND nf.namespace_id = n.id
|
||||
AND n.version_format = f.version_format
|
||||
AND f.type = t.id
|
||||
AND (f.name, f.version, f.version_format, t.name, n.name) IN (%s)`,
|
||||
util.QueryString(5, nsfCount),
|
||||
)
|
||||
}
|
||||
|
||||
type namespacedFeatureWithID struct {
|
||||
database.NamespacedFeature
|
||||
|
||||
ID int64
|
||||
}
|
||||
|
||||
func PersistNamespacedFeatures(tx *sql.Tx, features []database.NamespacedFeature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nsIDs := map[database.Namespace]sql.NullInt64{}
|
||||
fIDs := map[database.Feature]sql.NullInt64{}
|
||||
for _, f := range features {
|
||||
nsIDs[f.Namespace] = sql.NullInt64{}
|
||||
fIDs[f.Feature] = sql.NullInt64{}
|
||||
}
|
||||
|
||||
fToFind := []database.Feature{}
|
||||
for f := range fIDs {
|
||||
fToFind = append(fToFind, f)
|
||||
}
|
||||
|
||||
sort.Slice(fToFind, func(i, j int) bool {
|
||||
return fToFind[i].Name < fToFind[j].Name ||
|
||||
fToFind[i].Version < fToFind[j].Version ||
|
||||
fToFind[i].VersionFormat < fToFind[j].VersionFormat
|
||||
})
|
||||
|
||||
if ids, err := FindFeatureIDs(tx, fToFind); err == nil {
|
||||
for i, id := range ids {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
fIDs[fToFind[i]] = id
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
nsToFind := []database.Namespace{}
|
||||
for ns := range nsIDs {
|
||||
nsToFind = append(nsToFind, ns)
|
||||
}
|
||||
|
||||
if ids, err := namespace.FindNamespaceIDs(tx, nsToFind); err == nil {
|
||||
for i, id := range ids {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
nsIDs[nsToFind[i]] = id
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
keys := make([]interface{}, 0, len(features)*2)
|
||||
for _, f := range features {
|
||||
keys = append(keys, fIDs[f.Feature], nsIDs[f.Namespace])
|
||||
}
|
||||
|
||||
_, err := tx.Exec(queryPersistNamespacedFeature(len(features)), keys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindNamespacedFeatureIDs(tx *sql.Tx, nfs []database.NamespacedFeature) ([]sql.NullInt64, error) {
|
||||
if len(nfs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nfsMap := map[database.NamespacedFeature]int64{}
|
||||
keys := make([]interface{}, 0, len(nfs)*5)
|
||||
for _, nf := range nfs {
|
||||
keys = append(keys, nf.Name, nf.Version, nf.VersionFormat, nf.Type, nf.Namespace.Name)
|
||||
}
|
||||
|
||||
rows, err := tx.Query(querySearchNamespacedFeature(len(nfs)), keys...)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("searchNamespacedFeature", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var (
|
||||
id int64
|
||||
nf database.NamespacedFeature
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&id, &nf.Name, &nf.Version, &nf.VersionFormat, &nf.Type, &nf.Namespace.Name)
|
||||
nf.Namespace.VersionFormat = nf.VersionFormat
|
||||
if err != nil {
|
||||
return nil, util.HandleError("searchNamespacedFeature", err)
|
||||
}
|
||||
nfsMap[nf] = id
|
||||
}
|
||||
|
||||
ids := make([]sql.NullInt64, len(nfs))
|
||||
for i, nf := range nfs {
|
||||
if id, ok := nfsMap[nf]; ok {
|
||||
ids[i] = sql.NullInt64{id, true}
|
||||
} else {
|
||||
ids[i] = sql.NullInt64{}
|
||||
}
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt/dpkg"
|
||||
)
|
||||
|
||||
func TestInsertFeature(t *testing.T) {
|
||||
datastore, err := openDatabaseForTest("InsertFeature", false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer datastore.Close()
|
||||
|
||||
// Invalid Feature.
|
||||
id0, err := datastore.insertFeature(database.Feature{})
|
||||
assert.NotNil(t, err)
|
||||
assert.Zero(t, id0)
|
||||
|
||||
id0, err = datastore.insertFeature(database.Feature{
|
||||
Namespace: database.Namespace{},
|
||||
Name: "TestInsertFeature0",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
assert.Zero(t, id0)
|
||||
|
||||
// Insert Feature and ensure we can find it.
|
||||
feature := database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertFeatureNamespace1",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertFeature1",
|
||||
}
|
||||
id1, err := datastore.insertFeature(feature)
|
||||
assert.Nil(t, err)
|
||||
id2, err := datastore.insertFeature(feature)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, id1, id2)
|
||||
|
||||
// Insert invalid FeatureVersion.
|
||||
for _, invalidFeatureVersion := range []database.FeatureVersion{
|
||||
{
|
||||
Feature: database.Feature{},
|
||||
Version: "1.0",
|
||||
},
|
||||
{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{},
|
||||
Name: "TestInsertFeature2",
|
||||
},
|
||||
Version: "1.0",
|
||||
},
|
||||
{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertFeatureNamespace2",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertFeature2",
|
||||
},
|
||||
Version: "",
|
||||
},
|
||||
{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertFeatureNamespace2",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertFeature2",
|
||||
},
|
||||
Version: "bad version",
|
||||
},
|
||||
} {
|
||||
id3, err := datastore.insertFeatureVersion(invalidFeatureVersion)
|
||||
assert.Error(t, err)
|
||||
assert.Zero(t, id3)
|
||||
}
|
||||
|
||||
// Insert FeatureVersion and ensure we can find it.
|
||||
featureVersion := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertFeatureNamespace1",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertFeature1",
|
||||
},
|
||||
Version: "2:3.0-imba",
|
||||
}
|
||||
id4, err := datastore.insertFeatureVersion(featureVersion)
|
||||
assert.Nil(t, err)
|
||||
id5, err := datastore.insertFeatureVersion(featureVersion)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, id4, id5)
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
// InsertKeyValue stores (or updates) a single key / value tuple.
|
||||
func (pgSQL *pgSQL) InsertKeyValue(key, value string) (err error) {
|
||||
if key == "" || value == "" {
|
||||
log.Warning("could not insert a flag which has an empty name or value")
|
||||
return commonerr.NewBadRequestError("could not insert a flag which has an empty name or value")
|
||||
}
|
||||
|
||||
defer observeQueryTime("InsertKeyValue", "all", time.Now())
|
||||
|
||||
// Upsert.
|
||||
//
|
||||
// Note: UPSERT works only on >= PostgreSQL 9.5 which is not yet supported by AWS RDS.
|
||||
// The best solution is currently the use of http://dba.stackexchange.com/a/13477
|
||||
// but the key/value storage doesn't need to be super-efficient and super-safe at the
|
||||
// moment so we can just use a client-side solution with transactions, based on
|
||||
// http://postgresql.org/docs/current/static/plpgsql-control-structures.html.
|
||||
// TODO(Quentin-M): Enable Upsert as soon as 9.5 is stable.
|
||||
|
||||
for {
|
||||
// First, try to update.
|
||||
r, err := pgSQL.Exec(updateKeyValue, value, key)
|
||||
if err != nil {
|
||||
return handleError("updateKeyValue", err)
|
||||
}
|
||||
if n, _ := r.RowsAffected(); n > 0 {
|
||||
// Updated successfully.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to insert the key.
|
||||
// If someone else inserts the same key concurrently, we could get a unique-key violation error.
|
||||
_, err = pgSQL.Exec(insertKeyValue, key, value)
|
||||
if err != nil {
|
||||
if isErrUniqueViolation(err) {
|
||||
// Got unique constraint violation, retry.
|
||||
continue
|
||||
}
|
||||
return handleError("insertKeyValue", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetValue reads a single key / value tuple and returns an empty string if the key doesn't exist.
|
||||
func (pgSQL *pgSQL) GetKeyValue(key string) (string, error) {
|
||||
defer observeQueryTime("GetKeyValue", "all", time.Now())
|
||||
|
||||
var value string
|
||||
err := pgSQL.QueryRow(searchKeyValue, key).Scan(&value)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", handleError("searchKeyValue", err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package keyvalue
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database/pgsql/monitoring"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
const (
|
||||
searchKeyValue = `SELECT value FROM KeyValue WHERE key = $1`
|
||||
upsertKeyValue = `
|
||||
INSERT INTO KeyValue(key, value)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT ON CONSTRAINT keyvalue_key_key
|
||||
DO UPDATE SET key=$1, value=$2`
|
||||
)
|
||||
|
||||
func UpdateKeyValue(tx *sql.Tx, key, value string) (err error) {
|
||||
if key == "" || value == "" {
|
||||
log.Warning("could not insert a flag which has an empty name or value")
|
||||
return commonerr.NewBadRequestError("could not insert a flag which has an empty name or value")
|
||||
}
|
||||
|
||||
defer monitoring.ObserveQueryTime("PersistKeyValue", "all", time.Now())
|
||||
|
||||
_, err = tx.Exec(upsertKeyValue, key, value)
|
||||
if err != nil {
|
||||
return util.HandleError("insertKeyValue", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindKeyValue(tx *sql.Tx, key string) (string, bool, error) {
|
||||
defer monitoring.ObserveQueryTime("FindKeyValue", "all", time.Now())
|
||||
|
||||
var value string
|
||||
err := tx.QueryRow(searchKeyValue, key).Scan(&value)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", false, util.HandleError("searchKeyValue", err)
|
||||
}
|
||||
|
||||
return value, true, nil
|
||||
}
|
@ -0,0 +1,436 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/guregu/null/zero"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities bool) (database.Layer, error) {
|
||||
subquery := "all"
|
||||
if withFeatures {
|
||||
subquery += "/features"
|
||||
} else if withVulnerabilities {
|
||||
subquery += "/features+vulnerabilities"
|
||||
}
|
||||
defer observeQueryTime("FindLayer", subquery, time.Now())
|
||||
|
||||
// Find the layer
|
||||
var (
|
||||
layer database.Layer
|
||||
parentID zero.Int
|
||||
parentName zero.String
|
||||
nsID zero.Int
|
||||
nsName sql.NullString
|
||||
nsVersionFormat sql.NullString
|
||||
)
|
||||
|
||||
t := time.Now()
|
||||
err := pgSQL.QueryRow(searchLayer, name).Scan(
|
||||
&layer.ID,
|
||||
&layer.Name,
|
||||
&layer.EngineVersion,
|
||||
&parentID,
|
||||
&parentName,
|
||||
&nsID,
|
||||
&nsName,
|
||||
&nsVersionFormat,
|
||||
)
|
||||
observeQueryTime("FindLayer", "searchLayer", t)
|
||||
|
||||
if err != nil {
|
||||
return layer, handleError("searchLayer", err)
|
||||
}
|
||||
|
||||
if !parentID.IsZero() {
|
||||
layer.Parent = &database.Layer{
|
||||
Model: database.Model{ID: int(parentID.Int64)},
|
||||
Name: parentName.String,
|
||||
}
|
||||
}
|
||||
if !nsID.IsZero() {
|
||||
layer.Namespace = &database.Namespace{
|
||||
Model: database.Model{ID: int(nsID.Int64)},
|
||||
Name: nsName.String,
|
||||
VersionFormat: nsVersionFormat.String,
|
||||
}
|
||||
}
|
||||
|
||||
// Find its features
|
||||
if withFeatures || withVulnerabilities {
|
||||
// Create a transaction to disable hash/merge joins as our experiments have shown that
|
||||
// PostgreSQL 9.4 makes bad planning decisions about:
|
||||
// - joining the layer tree to feature versions and feature
|
||||
// - joining the feature versions to affected/fixed feature version and vulnerabilities
|
||||
// It would for instance do a merge join between affected feature versions (300 rows, estimated
|
||||
// 3000 rows) and fixed in feature version (100k rows). In this case, it is much more
|
||||
// preferred to use a nested loop.
|
||||
tx, err := pgSQL.Begin()
|
||||
if err != nil {
|
||||
return layer, handleError("FindLayer.Begin()", err)
|
||||
}
|
||||
defer tx.Commit()
|
||||
|
||||
_, err = tx.Exec(disableHashJoin)
|
||||
if err != nil {
|
||||
log.WithError(err).Warningf("FindLayer: could not disable hash join")
|
||||
}
|
||||
_, err = tx.Exec(disableMergeJoin)
|
||||
if err != nil {
|
||||
log.WithError(err).Warningf("FindLayer: could not disable merge join")
|
||||
}
|
||||
|
||||
t = time.Now()
|
||||
featureVersions, err := getLayerFeatureVersions(tx, layer.ID)
|
||||
observeQueryTime("FindLayer", "getLayerFeatureVersions", t)
|
||||
|
||||
if err != nil {
|
||||
return layer, err
|
||||
}
|
||||
|
||||
layer.Features = featureVersions
|
||||
|
||||
if withVulnerabilities {
|
||||
// Load the vulnerabilities that affect the FeatureVersions.
|
||||
t = time.Now()
|
||||
err := loadAffectedBy(tx, layer.Features)
|
||||
observeQueryTime("FindLayer", "loadAffectedBy", t)
|
||||
|
||||
if err != nil {
|
||||
return layer, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
// getLayerFeatureVersions returns list of database.FeatureVersion that a database.Layer has.
|
||||
func getLayerFeatureVersions(tx *sql.Tx, layerID int) ([]database.FeatureVersion, error) {
|
||||
var featureVersions []database.FeatureVersion
|
||||
|
||||
// Query.
|
||||
rows, err := tx.Query(searchLayerFeatureVersion, layerID)
|
||||
if err != nil {
|
||||
return featureVersions, handleError("searchLayerFeatureVersion", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Scan query.
|
||||
var modification string
|
||||
mapFeatureVersions := make(map[int]database.FeatureVersion)
|
||||
for rows.Next() {
|
||||
var fv database.FeatureVersion
|
||||
err = rows.Scan(
|
||||
&fv.ID,
|
||||
&modification,
|
||||
&fv.Feature.Namespace.ID,
|
||||
&fv.Feature.Namespace.Name,
|
||||
&fv.Feature.Namespace.VersionFormat,
|
||||
&fv.Feature.ID,
|
||||
&fv.Feature.Name,
|
||||
&fv.ID,
|
||||
&fv.Version,
|
||||
&fv.AddedBy.ID,
|
||||
&fv.AddedBy.Name,
|
||||
)
|
||||
if err != nil {
|
||||
return featureVersions, handleError("searchLayerFeatureVersion.Scan()", err)
|
||||
}
|
||||
|
||||
// Do transitive closure.
|
||||
switch modification {
|
||||
case "add":
|
||||
mapFeatureVersions[fv.ID] = fv
|
||||
case "del":
|
||||
delete(mapFeatureVersions, fv.ID)
|
||||
default:
|
||||
log.WithField("modification", modification).Warning("unknown Layer_diff_FeatureVersion's modification")
|
||||
return featureVersions, database.ErrInconsistent
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return featureVersions, handleError("searchLayerFeatureVersion.Rows()", err)
|
||||
}
|
||||
|
||||
// Build result by converting our map to a slice.
|
||||
for _, featureVersion := range mapFeatureVersions {
|
||||
featureVersions = append(featureVersions, featureVersion)
|
||||
}
|
||||
|
||||
return featureVersions, nil
|
||||
}
|
||||
|
||||
// loadAffectedBy returns the list of database.Vulnerability that affect the given
|
||||
// FeatureVersion.
|
||||
func loadAffectedBy(tx *sql.Tx, featureVersions []database.FeatureVersion) error {
|
||||
if len(featureVersions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Construct list of FeatureVersion IDs, we will do a single query
|
||||
featureVersionIDs := make([]int, 0, len(featureVersions))
|
||||
for i := 0; i < len(featureVersions); i++ {
|
||||
featureVersionIDs = append(featureVersionIDs, featureVersions[i].ID)
|
||||
}
|
||||
|
||||
rows, err := tx.Query(searchFeatureVersionVulnerability,
|
||||
buildInputArray(featureVersionIDs))
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return handleError("searchFeatureVersionVulnerability", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
vulnerabilities := make(map[int][]database.Vulnerability, len(featureVersions))
|
||||
var featureversionID int
|
||||
for rows.Next() {
|
||||
var vulnerability database.Vulnerability
|
||||
err := rows.Scan(
|
||||
&featureversionID,
|
||||
&vulnerability.ID,
|
||||
&vulnerability.Name,
|
||||
&vulnerability.Description,
|
||||
&vulnerability.Link,
|
||||
&vulnerability.Severity,
|
||||
&vulnerability.Metadata,
|
||||
&vulnerability.Namespace.Name,
|
||||
&vulnerability.Namespace.VersionFormat,
|
||||
&vulnerability.FixedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return handleError("searchFeatureVersionVulnerability.Scan()", err)
|
||||
}
|
||||
vulnerabilities[featureversionID] = append(vulnerabilities[featureversionID], vulnerability)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return handleError("searchFeatureVersionVulnerability.Rows()", err)
|
||||
}
|
||||
|
||||
// Assign vulnerabilities to every FeatureVersions
|
||||
for i := 0; i < len(featureVersions); i++ {
|
||||
featureVersions[i].AffectedBy = vulnerabilities[featureVersions[i].ID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Internally, only Feature additions/removals are stored for each layer. If a layer has a parent,
|
||||
// the Feature list will be compared to the parent's Feature list and the difference will be stored.
|
||||
// Note that when the Namespace of a layer differs from its parent, it is expected that several
|
||||
// Feature that were already included a parent will have their Namespace updated as well
|
||||
// (happens when Feature detectors relies on the detected layer Namespace). However, if the listed
|
||||
// Feature has the same Name/Version as its parent, InsertLayer considers that the Feature hasn't
|
||||
// been modified.
|
||||
func (pgSQL *pgSQL) InsertLayer(layer database.Layer) error {
|
||||
tf := time.Now()
|
||||
|
||||
// Verify parameters
|
||||
if layer.Name == "" {
|
||||
log.Warning("could not insert a layer which has an empty Name")
|
||||
return commonerr.NewBadRequestError("could not insert a layer which has an empty Name")
|
||||
}
|
||||
|
||||
// Get a potentially existing layer.
|
||||
existingLayer, err := pgSQL.FindLayer(layer.Name, true, false)
|
||||
if err != nil && err != commonerr.ErrNotFound {
|
||||
return err
|
||||
} else if err == nil {
|
||||
if existingLayer.EngineVersion >= layer.EngineVersion {
|
||||
// The layer exists and has an equal or higher engine version, do nothing.
|
||||
return nil
|
||||
}
|
||||
|
||||
layer.ID = existingLayer.ID
|
||||
}
|
||||
|
||||
// We do `defer observeQueryTime` here because we don't want to observe existing layers.
|
||||
defer observeQueryTime("InsertLayer", "all", tf)
|
||||
|
||||
// Get parent ID.
|
||||
var parentID zero.Int
|
||||
if layer.Parent != nil {
|
||||
if layer.Parent.ID == 0 {
|
||||
log.Warning("Parent is expected to be retrieved from database when inserting a layer.")
|
||||
return commonerr.NewBadRequestError("Parent is expected to be retrieved from database when inserting a layer.")
|
||||
}
|
||||
|
||||
parentID = zero.IntFrom(int64(layer.Parent.ID))
|
||||
}
|
||||
|
||||
// Find or insert namespace if provided.
|
||||
var namespaceID zero.Int
|
||||
if layer.Namespace != nil {
|
||||
n, err := pgSQL.insertNamespace(*layer.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespaceID = zero.IntFrom(int64(n))
|
||||
} else if layer.Namespace == nil && layer.Parent != nil {
|
||||
// Import the Namespace from the parent if it has one and this layer doesn't specify one.
|
||||
if layer.Parent.Namespace != nil {
|
||||
namespaceID = zero.IntFrom(int64(layer.Parent.Namespace.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// Begin transaction.
|
||||
tx, err := pgSQL.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return handleError("InsertLayer.Begin()", err)
|
||||
}
|
||||
|
||||
if layer.ID == 0 {
|
||||
// Insert a new layer.
|
||||
err = tx.QueryRow(insertLayer, layer.Name, layer.EngineVersion, parentID, namespaceID).
|
||||
Scan(&layer.ID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
|
||||
if isErrUniqueViolation(err) {
|
||||
// Ignore this error, another process collided.
|
||||
log.Debug("Attempted to insert duplicate layer.")
|
||||
return nil
|
||||
}
|
||||
return handleError("insertLayer", err)
|
||||
}
|
||||
} else {
|
||||
// Update an existing layer.
|
||||
_, err = tx.Exec(updateLayer, layer.ID, layer.EngineVersion, namespaceID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return handleError("updateLayer", err)
|
||||
}
|
||||
|
||||
// Remove all existing Layer_diff_FeatureVersion.
|
||||
_, err = tx.Exec(removeLayerDiffFeatureVersion, layer.ID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return handleError("removeLayerDiffFeatureVersion", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update Layer_diff_FeatureVersion now.
|
||||
err = pgSQL.updateDiffFeatureVersions(tx, &layer, &existingLayer)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// Commit transaction.
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return handleError("InsertLayer.Commit()", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pgSQL *pgSQL) updateDiffFeatureVersions(tx *sql.Tx, layer, existingLayer *database.Layer) error {
|
||||
// add and del are the FeatureVersion diff we should insert.
|
||||
var add []database.FeatureVersion
|
||||
var del []database.FeatureVersion
|
||||
|
||||
if layer.Parent == nil {
|
||||
// There is no parent, every Features are added.
|
||||
add = append(add, layer.Features...)
|
||||
} else if layer.Parent != nil {
|
||||
// There is a parent, we need to diff the Features with it.
|
||||
|
||||
// Build name:version structures.
|
||||
layerFeaturesMapNV, layerFeaturesNV := createNV(layer.Features)
|
||||
parentLayerFeaturesMapNV, parentLayerFeaturesNV := createNV(layer.Parent.Features)
|
||||
|
||||
// Calculate the added and deleted FeatureVersions name:version.
|
||||
addNV := compareStringLists(layerFeaturesNV, parentLayerFeaturesNV)
|
||||
delNV := compareStringLists(parentLayerFeaturesNV, layerFeaturesNV)
|
||||
|
||||
// Fill the structures containing the added and deleted FeatureVersions.
|
||||
for _, nv := range addNV {
|
||||
add = append(add, *layerFeaturesMapNV[nv])
|
||||
}
|
||||
for _, nv := range delNV {
|
||||
del = append(del, *parentLayerFeaturesMapNV[nv])
|
||||
}
|
||||
}
|
||||
|
||||
// Insert FeatureVersions in the database.
|
||||
addIDs, err := pgSQL.insertFeatureVersions(add)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delIDs, err := pgSQL.insertFeatureVersions(del)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert diff in the database.
|
||||
if len(addIDs) > 0 {
|
||||
_, err = tx.Exec(insertLayerDiffFeatureVersion, layer.ID, "add", buildInputArray(addIDs))
|
||||
if err != nil {
|
||||
return handleError("insertLayerDiffFeatureVersion.Add", err)
|
||||
}
|
||||
}
|
||||
if len(delIDs) > 0 {
|
||||
_, err = tx.Exec(insertLayerDiffFeatureVersion, layer.ID, "del", buildInputArray(delIDs))
|
||||
if err != nil {
|
||||
return handleError("insertLayerDiffFeatureVersion.Del", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNV(features []database.FeatureVersion) (map[string]*database.FeatureVersion, []string) {
|
||||
mapNV := make(map[string]*database.FeatureVersion, 0)
|
||||
sliceNV := make([]string, 0, len(features))
|
||||
|
||||
for i := 0; i < len(features); i++ {
|
||||
fv := &features[i]
|
||||
nv := strings.Join([]string{fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":")
|
||||
mapNV[nv] = fv
|
||||
sliceNV = append(sliceNV, nv)
|
||||
}
|
||||
|
||||
return mapNV, sliceNV
|
||||
}
|
||||
|
||||
func (pgSQL *pgSQL) DeleteLayer(name string) error {
|
||||
defer observeQueryTime("DeleteLayer", "all", time.Now())
|
||||
|
||||
result, err := pgSQL.Exec(removeLayer, name)
|
||||
if err != nil {
|
||||
return handleError("removeLayer", err)
|
||||
}
|
||||
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return handleError("removeLayer.RowsAffected()", err)
|
||||
}
|
||||
|
||||
if affected <= 0 {
|
||||
return commonerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/deckarep/golang-set"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
const (
|
||||
soiLayer = `
|
||||
WITH new_layer AS (
|
||||
INSERT INTO layer (hash)
|
||||
SELECT CAST ($1 AS VARCHAR)
|
||||
WHERE NOT EXISTS (SELECT id FROM layer WHERE hash = $1)
|
||||
RETURNING id
|
||||
)
|
||||
SELECT id FROM new_Layer
|
||||
UNION
|
||||
SELECT id FROM layer WHERE hash = $1`
|
||||
|
||||
findLayerID = `SELECT id FROM layer WHERE hash = $1`
|
||||
)
|
||||
|
||||
func FindLayer(tx *sql.Tx, hash string) (database.Layer, bool, error) {
|
||||
layer := database.Layer{Hash: hash}
|
||||
if hash == "" {
|
||||
return layer, false, commonerr.NewBadRequestError("non empty layer hash is expected.")
|
||||
}
|
||||
|
||||
layerID, ok, err := FindLayerID(tx, hash)
|
||||
if err != nil || !ok {
|
||||
return layer, ok, err
|
||||
}
|
||||
|
||||
detectorMap, err := detector.FindAllDetectors(tx)
|
||||
if err != nil {
|
||||
return layer, false, err
|
||||
}
|
||||
|
||||
if layer.By, err = FindLayerDetectors(tx, layerID); err != nil {
|
||||
return layer, false, err
|
||||
}
|
||||
|
||||
if layer.Features, err = FindLayerFeatures(tx, layerID, detectorMap); err != nil {
|
||||
return layer, false, err
|
||||
}
|
||||
|
||||
if layer.Namespaces, err = FindLayerNamespaces(tx, layerID, detectorMap); err != nil {
|
||||
return layer, false, err
|
||||
}
|
||||
|
||||
return layer, true, nil
|
||||
}
|
||||
|
||||
func sanitizePersistLayerInput(hash string, features []database.LayerFeature, namespaces []database.LayerNamespace, detectedBy []database.Detector) error {
|
||||
if hash == "" {
|
||||
return commonerr.NewBadRequestError("expected non-empty layer hash")
|
||||
}
|
||||
|
||||
detectedBySet := mapset.NewSet()
|
||||
for _, d := range detectedBy {
|
||||
detectedBySet.Add(d)
|
||||
}
|
||||
|
||||
for _, f := range features {
|
||||
if !detectedBySet.Contains(f.By) {
|
||||
return database.ErrInvalidParameters
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range namespaces {
|
||||
if !detectedBySet.Contains(n.By) {
|
||||
return database.ErrInvalidParameters
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PersistLayer saves the content of a layer to the database.
|
||||
func PersistLayer(tx *sql.Tx, hash string, features []database.LayerFeature, namespaces []database.LayerNamespace, detectedBy []database.Detector) error {
|
||||
var (
|
||||
err error
|
||||
id int64
|
||||
detectorIDs []int64
|
||||
)
|
||||
|
||||
if err = sanitizePersistLayerInput(hash, features, namespaces, detectedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id, err = SoiLayer(tx, hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if detectorIDs, err = detector.FindDetectorIDs(tx, detectedBy); err != nil {
|
||||
if err == commonerr.ErrNotFound {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err = PersistLayerDetectors(tx, id, detectorIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = PersistAllLayerFeatures(tx, id, features); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = PersistAllLayerNamespaces(tx, id, namespaces); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindLayerID(tx *sql.Tx, hash string) (int64, bool, error) {
|
||||
var layerID int64
|
||||
err := tx.QueryRow(findLayerID, hash).Scan(&layerID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return layerID, false, nil
|
||||
}
|
||||
|
||||
return layerID, false, util.HandleError("findLayerID", err)
|
||||
}
|
||||
|
||||
return layerID, true, nil
|
||||
}
|
||||
|
||||
func FindLayerIDs(tx *sql.Tx, hashes []string) ([]int64, bool, error) {
|
||||
layerIDs := make([]int64, 0, len(hashes))
|
||||
for _, hash := range hashes {
|
||||
id, ok, err := FindLayerID(tx, hash)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
layerIDs = append(layerIDs, id)
|
||||
}
|
||||
|
||||
return layerIDs, true, nil
|
||||
}
|
||||
|
||||
func SoiLayer(tx *sql.Tx, hash string) (int64, error) {
|
||||
var id int64
|
||||
if err := tx.QueryRow(soiLayer, hash).Scan(&id); err != nil {
|
||||
return 0, util.HandleError("soiLayer", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/deckarep/golang-set"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
const (
|
||||
selectLayerDetectors = `
|
||||
SELECT d.name, d.version, d.dtype
|
||||
FROM layer_detector, detector AS d
|
||||
WHERE layer_detector.detector_id = d.id AND layer_detector.layer_id = $1;`
|
||||
|
||||
persistLayerDetector = `
|
||||
INSERT INTO layer_detector (layer_id, detector_id)
|
||||
SELECT $1, $2
|
||||
WHERE NOT EXISTS (SELECT id FROM layer_detector WHERE layer_id = $1 AND detector_id = $2)`
|
||||
)
|
||||
|
||||
func PersistLayerDetector(tx *sql.Tx, layerID int64, detectorID int64) error {
|
||||
if _, err := tx.Exec(persistLayerDetector, layerID, detectorID); err != nil {
|
||||
return util.HandleError("persistLayerDetector", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PersistLayerDetectors(tx *sql.Tx, layerID int64, detectorIDs []int64) error {
|
||||
alreadySaved := mapset.NewSet()
|
||||
for _, id := range detectorIDs {
|
||||
if alreadySaved.Contains(id) {
|
||||
continue
|
||||
}
|
||||
|
||||
alreadySaved.Add(id)
|
||||
if err := PersistLayerDetector(tx, layerID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindLayerDetectors(tx *sql.Tx, id int64) ([]database.Detector, error) {
|
||||
detectors, err := detector.GetDetectors(tx, selectLayerDetectors, id)
|
||||
return detectors, err
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/clair/database/pgsql/namespace"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/feature"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
const findLayerFeatures = `
|
||||
SELECT
|
||||
f.name, f.version, f.version_format, ft.name, lf.detector_id, ns.name, ns.version_format
|
||||
FROM
|
||||
layer_feature AS lf
|
||||
LEFT JOIN feature f on f.id = lf.feature_id
|
||||
LEFT JOIN feature_type ft on ft.id = f.type
|
||||
LEFT JOIN namespace ns ON ns.id = lf.namespace_id
|
||||
|
||||
WHERE lf.layer_id = $1`
|
||||
|
||||
func queryPersistLayerFeature(count int) string {
|
||||
return util.QueryPersist(count,
|
||||
"layer_feature",
|
||||
"layer_feature_layer_id_feature_id_namespace_id_key",
|
||||
"layer_id",
|
||||
"feature_id",
|
||||
"detector_id",
|
||||
"namespace_id")
|
||||
}
|
||||
|
||||
// dbLayerFeature represents the layer_feature table
|
||||
type dbLayerFeature struct {
|
||||
layerID int64
|
||||
featureID int64
|
||||
detectorID int64
|
||||
namespaceID sql.NullInt64
|
||||
}
|
||||
|
||||
func FindLayerFeatures(tx *sql.Tx, layerID int64, detectors detector.DetectorMap) ([]database.LayerFeature, error) {
|
||||
rows, err := tx.Query(findLayerFeatures, layerID)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("findLayerFeatures", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
features := []database.LayerFeature{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
detectorID int64
|
||||
feature database.LayerFeature
|
||||
)
|
||||
var namespaceName, namespaceVersion sql.NullString
|
||||
if err := rows.Scan(&feature.Name, &feature.Version, &feature.VersionFormat, &feature.Type, &detectorID, &namespaceName, &namespaceVersion); err != nil {
|
||||
return nil, util.HandleError("findLayerFeatures", err)
|
||||
}
|
||||
feature.PotentialNamespace.Name = namespaceName.String
|
||||
feature.PotentialNamespace.VersionFormat = namespaceVersion.String
|
||||
|
||||
feature.By = detectors.ByID[detectorID]
|
||||
features = append(features, feature)
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
func PersistAllLayerFeatures(tx *sql.Tx, layerID int64, features []database.LayerFeature) error {
|
||||
detectorMap, err := detector.FindAllDetectors(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var namespaces []database.Namespace
|
||||
for _, feature := range features {
|
||||
namespaces = append(namespaces, feature.PotentialNamespace)
|
||||
}
|
||||
nameSpaceIDs, _ := namespace.FindNamespaceIDs(tx, namespaces)
|
||||
featureNamespaceMap := map[database.Namespace]sql.NullInt64{}
|
||||
rawFeatures := make([]database.Feature, 0, len(features))
|
||||
for i, f := range features {
|
||||
rawFeatures = append(rawFeatures, f.Feature)
|
||||
if f.PotentialNamespace.Valid() {
|
||||
featureNamespaceMap[f.PotentialNamespace] = nameSpaceIDs[i]
|
||||
}
|
||||
}
|
||||
|
||||
featureIDs, err := feature.FindFeatureIDs(tx, rawFeatures)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var namespaceID sql.NullInt64
|
||||
dbFeatures := make([]dbLayerFeature, 0, len(features))
|
||||
for i, f := range features {
|
||||
detectorID := detectorMap.ByValue[f.By]
|
||||
featureID := featureIDs[i].Int64
|
||||
if !featureIDs[i].Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
namespaceID = featureNamespaceMap[f.PotentialNamespace]
|
||||
|
||||
dbFeatures = append(dbFeatures, dbLayerFeature{layerID, featureID, detectorID, namespaceID})
|
||||
}
|
||||
|
||||
if err := PersistLayerFeatures(tx, dbFeatures); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PersistLayerFeatures(tx *sql.Tx, features []dbLayerFeature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(features, func(i, j int) bool {
|
||||
return features[i].featureID < features[j].featureID
|
||||
})
|
||||
keys := make([]interface{}, 0, len(features)*4)
|
||||
|
||||
for _, f := range features {
|
||||
keys = append(keys, f.layerID, f.featureID, f.detectorID, f.namespaceID)
|
||||
}
|
||||
|
||||
_, err := tx.Exec(queryPersistLayerFeature(len(features)), keys...)
|
||||
if err != nil {
|
||||
return util.HandleError("queryPersistLayerFeature", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/detector"
|
||||
"github.com/coreos/clair/database/pgsql/namespace"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
const findLayerNamespaces = `
|
||||
SELECT ns.name, ns.version_format, ln.detector_id
|
||||
FROM layer_namespace AS ln, namespace AS ns
|
||||
WHERE ln.namespace_id = ns.id
|
||||
AND ln.layer_id = $1`
|
||||
|
||||
func queryPersistLayerNamespace(count int) string {
|
||||
return util.QueryPersist(count,
|
||||
"layer_namespace",
|
||||
"layer_namespace_layer_id_namespace_id_key",
|
||||
"layer_id",
|
||||
"namespace_id",
|
||||
"detector_id")
|
||||
}
|
||||
|
||||
// dbLayerNamespace represents the layer_namespace table.
|
||||
type dbLayerNamespace struct {
|
||||
layerID int64
|
||||
namespaceID int64
|
||||
detectorID int64
|
||||
}
|
||||
|
||||
func FindLayerNamespaces(tx *sql.Tx, layerID int64, detectors detector.DetectorMap) ([]database.LayerNamespace, error) {
|
||||
rows, err := tx.Query(findLayerNamespaces, layerID)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("findLayerNamespaces", err)
|
||||
}
|
||||
|
||||
namespaces := []database.LayerNamespace{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
namespace database.LayerNamespace
|
||||
detectorID int64
|
||||
)
|
||||
|
||||
if err := rows.Scan(&namespace.Name, &namespace.VersionFormat, &detectorID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespace.By = detectors.ByID[detectorID]
|
||||
namespaces = append(namespaces, namespace)
|
||||
}
|
||||
|
||||
return namespaces, nil
|
||||
}
|
||||
|
||||
func PersistAllLayerNamespaces(tx *sql.Tx, layerID int64, namespaces []database.LayerNamespace) error {
|
||||
detectorMap, err := detector.FindAllDetectors(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(sidac): This kind of type conversion is very useless and wasteful,
|
||||
// we need interfaces around the database models to reduce these kind of
|
||||
// operations.
|
||||
rawNamespaces := make([]database.Namespace, 0, len(namespaces))
|
||||
for _, ns := range namespaces {
|
||||
rawNamespaces = append(rawNamespaces, ns.Namespace)
|
||||
}
|
||||
|
||||
rawNamespaceIDs, err := namespace.FindNamespaceIDs(tx, rawNamespaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbLayerNamespaces := make([]dbLayerNamespace, 0, len(namespaces))
|
||||
for i, ns := range namespaces {
|
||||
detectorID := detectorMap.ByValue[ns.By]
|
||||
namespaceID := rawNamespaceIDs[i].Int64
|
||||
if !rawNamespaceIDs[i].Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
|
||||
dbLayerNamespaces = append(dbLayerNamespaces, dbLayerNamespace{layerID, namespaceID, detectorID})
|
||||
}
|
||||
|
||||
return PersistLayerNamespaces(tx, dbLayerNamespaces)
|
||||
}
|
||||
|
||||
func PersistLayerNamespaces(tx *sql.Tx, namespaces []dbLayerNamespace) error {
|
||||
if len(namespaces) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// for every bulk persist operation, the input data should be sorted.
|
||||
sort.Slice(namespaces, func(i, j int) bool {
|
||||
return namespaces[i].namespaceID < namespaces[j].namespaceID
|
||||
})
|
||||
|
||||
keys := make([]interface{}, 0, len(namespaces)*3)
|
||||
for _, row := range namespaces {
|
||||
keys = append(keys, row.layerID, row.namespaceID, row.detectorID)
|
||||
}
|
||||
|
||||
_, err := tx.Exec(queryPersistLayerNamespace(len(namespaces)), keys...)
|
||||
if err != nil {
|
||||
return util.HandleError("queryPersistLayerNamespace", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
var persistLayerTests = []struct {
|
||||
title string
|
||||
name string
|
||||
by []database.Detector
|
||||
features []database.LayerFeature
|
||||
namespaces []database.LayerNamespace
|
||||
layer *database.Layer
|
||||
err string
|
||||
}{
|
||||
{
|
||||
title: "invalid layer name",
|
||||
name: "",
|
||||
err: "expected non-empty layer hash",
|
||||
},
|
||||
{
|
||||
title: "layer with inconsistent feature and detectors",
|
||||
name: "random-forest",
|
||||
by: []database.Detector{testutil.RealDetectors[2]},
|
||||
features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[1], testutil.RealDetectors[1], database.Namespace{}},
|
||||
},
|
||||
err: "parameters are not valid",
|
||||
},
|
||||
{
|
||||
title: "layer with non-existing feature",
|
||||
name: "random-forest",
|
||||
err: "associated immutable entities are missing in the database",
|
||||
by: []database.Detector{testutil.RealDetectors[2]},
|
||||
features: []database.LayerFeature{
|
||||
{testutil.FakeFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "layer with non-existing namespace",
|
||||
name: "random-forest2",
|
||||
err: "associated immutable entities are missing in the database",
|
||||
by: []database.Detector{testutil.RealDetectors[1]},
|
||||
namespaces: []database.LayerNamespace{
|
||||
{testutil.FakeNamespaces[1], testutil.RealDetectors[1]},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "layer with non-existing detector",
|
||||
name: "random-forest3",
|
||||
err: "associated immutable entities are missing in the database",
|
||||
by: []database.Detector{testutil.FakeDetector[1]},
|
||||
},
|
||||
{
|
||||
|
||||
title: "valid layer",
|
||||
name: "hamsterhouse",
|
||||
by: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
|
||||
features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
|
||||
{testutil.RealFeatures[2], testutil.RealDetectors[2], database.Namespace{}},
|
||||
},
|
||||
namespaces: []database.LayerNamespace{
|
||||
{testutil.RealNamespaces[1], testutil.RealDetectors[1]},
|
||||
},
|
||||
layer: &database.Layer{
|
||||
Hash: "hamsterhouse",
|
||||
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
|
||||
Features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
|
||||
{testutil.RealFeatures[2], testutil.RealDetectors[2], database.Namespace{}},
|
||||
},
|
||||
Namespaces: []database.LayerNamespace{
|
||||
{testutil.RealNamespaces[1], testutil.RealDetectors[1]},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "update existing layer",
|
||||
name: "layer-1",
|
||||
by: []database.Detector{testutil.RealDetectors[3], testutil.RealDetectors[4]},
|
||||
features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[4], testutil.RealDetectors[3], database.Namespace{}},
|
||||
},
|
||||
namespaces: []database.LayerNamespace{
|
||||
{testutil.RealNamespaces[3], testutil.RealDetectors[4]},
|
||||
},
|
||||
layer: &database.Layer{
|
||||
Hash: "layer-1",
|
||||
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2], testutil.RealDetectors[3], testutil.RealDetectors[4]},
|
||||
Features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
|
||||
{testutil.RealFeatures[2], testutil.RealDetectors[2], database.Namespace{}},
|
||||
{testutil.RealFeatures[4], testutil.RealDetectors[3], database.Namespace{}},
|
||||
},
|
||||
Namespaces: []database.LayerNamespace{
|
||||
{testutil.RealNamespaces[1], testutil.RealDetectors[1]},
|
||||
{testutil.RealNamespaces[3], testutil.RealDetectors[4]},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: "layer with potential namespace",
|
||||
name: "layer-potential-namespace",
|
||||
by: []database.Detector{testutil.RealDetectors[3]},
|
||||
features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[4], testutil.RealDetectors[3], testutil.RealNamespaces[4]},
|
||||
},
|
||||
namespaces: []database.LayerNamespace{
|
||||
{testutil.RealNamespaces[3], testutil.RealDetectors[3]},
|
||||
},
|
||||
layer: &database.Layer{
|
||||
Hash: "layer-potential-namespace",
|
||||
By: []database.Detector{testutil.RealDetectors[3]},
|
||||
Features: []database.LayerFeature{
|
||||
{testutil.RealFeatures[4], testutil.RealDetectors[3], testutil.RealNamespaces[4]},
|
||||
},
|
||||
Namespaces: []database.LayerNamespace{
|
||||
{testutil.RealNamespaces[3], testutil.RealDetectors[3]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestPersistLayer(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "PersistLayer")
|
||||
defer cleanup()
|
||||
|
||||
for _, test := range persistLayerTests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
err := PersistLayer(tx, test.name, test.features, test.namespaces, test.by)
|
||||
if test.err != "" {
|
||||
assert.EqualError(t, err, test.err, "unexpected error")
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, err)
|
||||
if test.layer != nil {
|
||||
layer, ok, err := FindLayer(tx, test.name)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, ok)
|
||||
database.AssertLayerEqual(t, test.layer, &layer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var findLayerTests = []struct {
|
||||
title string
|
||||
in string
|
||||
|
||||
out *database.Layer
|
||||
err string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
title: "invalid layer name",
|
||||
in: "",
|
||||
err: "non empty layer hash is expected.",
|
||||
},
|
||||
{
|
||||
title: "non-existing layer",
|
||||
in: "layer-non-existing",
|
||||
ok: false,
|
||||
out: nil,
|
||||
},
|
||||
{
|
||||
title: "existing layer",
|
||||
in: "layer-4",
|
||||
ok: true,
|
||||
out: testutil.TakeLayerPointerFromMap(testutil.RealLayers, 6),
|
||||
},
|
||||
}
|
||||
|
||||
func TestFindLayer(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "FindLayer")
|
||||
defer cleanup()
|
||||
|
||||
for _, test := range findLayerTests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
layer, ok, err := FindLayer(tx, test.in)
|
||||
if test.err != "" {
|
||||
assert.EqualError(t, err, test.err, "unexpected error")
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, test.ok, ok)
|
||||
if test.ok {
|
||||
database.AssertLayerEqual(t, test.out, &layer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,384 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt/dpkg"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
func TestFindLayer(t *testing.T) {
|
||||
datastore, err := openDatabaseForTest("FindLayer", true)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer datastore.Close()
|
||||
|
||||
// Layer-0: no parent, no namespace, no feature, no vulnerability
|
||||
layer, err := datastore.FindLayer("layer-0", false, false)
|
||||
if assert.Nil(t, err) && assert.NotNil(t, layer) {
|
||||
assert.Equal(t, "layer-0", layer.Name)
|
||||
assert.Nil(t, layer.Namespace)
|
||||
assert.Nil(t, layer.Parent)
|
||||
assert.Equal(t, 1, layer.EngineVersion)
|
||||
assert.Len(t, layer.Features, 0)
|
||||
}
|
||||
|
||||
layer, err = datastore.FindLayer("layer-0", true, false)
|
||||
if assert.Nil(t, err) && assert.NotNil(t, layer) {
|
||||
assert.Len(t, layer.Features, 0)
|
||||
}
|
||||
|
||||
// Layer-1: one parent, adds two features, one vulnerability
|
||||
layer, err = datastore.FindLayer("layer-1", false, false)
|
||||
if assert.Nil(t, err) && assert.NotNil(t, layer) {
|
||||
assert.Equal(t, layer.Name, "layer-1")
|
||||
assert.Equal(t, "debian:7", layer.Namespace.Name)
|
||||
if assert.NotNil(t, layer.Parent) {
|
||||
assert.Equal(t, "layer-0", layer.Parent.Name)
|
||||
}
|
||||
assert.Equal(t, 1, layer.EngineVersion)
|
||||
assert.Len(t, layer.Features, 0)
|
||||
}
|
||||
|
||||
layer, err = datastore.FindLayer("layer-1", true, false)
|
||||
if assert.Nil(t, err) && assert.NotNil(t, layer) && assert.Len(t, layer.Features, 2) {
|
||||
for _, featureVersion := range layer.Features {
|
||||
assert.Equal(t, "debian:7", featureVersion.Feature.Namespace.Name)
|
||||
|
||||
switch featureVersion.Feature.Name {
|
||||
case "wechat":
|
||||
assert.Equal(t, "0.5", featureVersion.Version)
|
||||
case "openssl":
|
||||
assert.Equal(t, "1.0", featureVersion.Version)
|
||||
default:
|
||||
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layer, err = datastore.FindLayer("layer-1", true, true)
|
||||
if assert.Nil(t, err) && assert.NotNil(t, layer) && assert.Len(t, layer.Features, 2) {
|
||||
for _, featureVersion := range layer.Features {
|
||||
assert.Equal(t, "debian:7", featureVersion.Feature.Namespace.Name)
|
||||
|
||||
switch featureVersion.Feature.Name {
|
||||
case "wechat":
|
||||
assert.Equal(t, "0.5", featureVersion.Version)
|
||||
case "openssl":
|
||||
assert.Equal(t, "1.0", featureVersion.Version)
|
||||
|
||||
if assert.Len(t, featureVersion.AffectedBy, 1) {
|
||||
assert.Equal(t, "debian:7", featureVersion.AffectedBy[0].Namespace.Name)
|
||||
assert.Equal(t, "CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Name)
|
||||
assert.Equal(t, database.HighSeverity, featureVersion.AffectedBy[0].Severity)
|
||||
assert.Equal(t, "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", featureVersion.AffectedBy[0].Description)
|
||||
assert.Equal(t, "http://google.com/#q=CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Link)
|
||||
assert.Equal(t, "2.0", featureVersion.AffectedBy[0].FixedBy)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertLayer(t *testing.T) {
|
||||
datastore, err := openDatabaseForTest("InsertLayer", false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer datastore.Close()
|
||||
|
||||
// Insert invalid layer.
|
||||
testInsertLayerInvalid(t, datastore)
|
||||
|
||||
// Insert a layer tree.
|
||||
testInsertLayerTree(t, datastore)
|
||||
|
||||
// Update layer.
|
||||
testInsertLayerUpdate(t, datastore)
|
||||
|
||||
// Delete layer.
|
||||
testInsertLayerDelete(t, datastore)
|
||||
}
|
||||
|
||||
func testInsertLayerInvalid(t *testing.T, datastore database.Datastore) {
|
||||
invalidLayers := []database.Layer{
|
||||
{},
|
||||
{Name: "layer0", Parent: &database.Layer{}},
|
||||
{Name: "layer0", Parent: &database.Layer{Name: "UnknownLayer"}},
|
||||
}
|
||||
|
||||
for _, invalidLayer := range invalidLayers {
|
||||
err := datastore.InsertLayer(invalidLayer)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
|
||||
f1 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace2",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature1",
|
||||
},
|
||||
Version: "1.0",
|
||||
}
|
||||
f2 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace2",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature2",
|
||||
},
|
||||
Version: "0.34",
|
||||
}
|
||||
f3 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace2",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature3",
|
||||
},
|
||||
Version: "0.56",
|
||||
}
|
||||
f4 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace3",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature2",
|
||||
},
|
||||
Version: "0.34",
|
||||
}
|
||||
f5 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace3",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature3",
|
||||
},
|
||||
Version: "0.56",
|
||||
}
|
||||
f6 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace3",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature4",
|
||||
},
|
||||
Version: "0.666",
|
||||
}
|
||||
|
||||
layers := []database.Layer{
|
||||
{
|
||||
Name: "TestInsertLayer1",
|
||||
},
|
||||
{
|
||||
Name: "TestInsertLayer2",
|
||||
Parent: &database.Layer{Name: "TestInsertLayer1"},
|
||||
Namespace: &database.Namespace{
|
||||
Name: "TestInsertLayerNamespace1",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
},
|
||||
// This layer changes the namespace and adds Features.
|
||||
{
|
||||
Name: "TestInsertLayer3",
|
||||
Parent: &database.Layer{Name: "TestInsertLayer2"},
|
||||
Namespace: &database.Namespace{
|
||||
Name: "TestInsertLayerNamespace2",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Features: []database.FeatureVersion{f1, f2, f3},
|
||||
},
|
||||
// This layer covers the case where the last layer doesn't provide any new Feature.
|
||||
{
|
||||
Name: "TestInsertLayer4a",
|
||||
Parent: &database.Layer{Name: "TestInsertLayer3"},
|
||||
Features: []database.FeatureVersion{f1, f2, f3},
|
||||
},
|
||||
// This layer covers the case where the last layer provides Features.
|
||||
// It also modifies the Namespace ("upgrade") but keeps some Features not upgraded, their
|
||||
// Namespaces should then remain unchanged.
|
||||
{
|
||||
Name: "TestInsertLayer4b",
|
||||
Parent: &database.Layer{Name: "TestInsertLayer3"},
|
||||
Namespace: &database.Namespace{
|
||||
Name: "TestInsertLayerNamespace3",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Features: []database.FeatureVersion{
|
||||
// Deletes TestInsertLayerFeature1.
|
||||
// Keep TestInsertLayerFeature2 (old Namespace should be kept):
|
||||
f4,
|
||||
// Upgrades TestInsertLayerFeature3 (with new Namespace):
|
||||
f5,
|
||||
// Adds TestInsertLayerFeature4:
|
||||
f6,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
retrievedLayers := make(map[string]database.Layer)
|
||||
for _, layer := range layers {
|
||||
if layer.Parent != nil {
|
||||
// Retrieve from database its parent and assign.
|
||||
parent := retrievedLayers[layer.Parent.Name]
|
||||
layer.Parent = &parent
|
||||
}
|
||||
|
||||
err = datastore.InsertLayer(layer)
|
||||
assert.Nil(t, err)
|
||||
|
||||
retrievedLayers[layer.Name], err = datastore.FindLayer(layer.Name, true, false)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
l4a := retrievedLayers["TestInsertLayer4a"]
|
||||
if assert.NotNil(t, l4a.Namespace) {
|
||||
assert.Equal(t, "TestInsertLayerNamespace2", l4a.Namespace.Name)
|
||||
}
|
||||
assert.Len(t, l4a.Features, 3)
|
||||
for _, featureVersion := range l4a.Features {
|
||||
if cmpFV(featureVersion, f1) && cmpFV(featureVersion, f2) && cmpFV(featureVersion, f3) {
|
||||
assert.Error(t, fmt.Errorf("TestInsertLayer4a contains an unexpected package: %#v. Should contain %#v and %#v and %#v.", featureVersion, f1, f2, f3))
|
||||
}
|
||||
}
|
||||
|
||||
l4b := retrievedLayers["TestInsertLayer4b"]
|
||||
if assert.NotNil(t, l4b.Namespace) {
|
||||
assert.Equal(t, "TestInsertLayerNamespace3", l4b.Namespace.Name)
|
||||
}
|
||||
assert.Len(t, l4b.Features, 3)
|
||||
for _, featureVersion := range l4b.Features {
|
||||
if cmpFV(featureVersion, f2) && cmpFV(featureVersion, f5) && cmpFV(featureVersion, f6) {
|
||||
assert.Error(t, fmt.Errorf("TestInsertLayer4a contains an unexpected package: %#v. Should contain %#v and %#v and %#v.", featureVersion, f2, f4, f6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) {
|
||||
f7 := database.FeatureVersion{
|
||||
Feature: database.Feature{
|
||||
Namespace: database.Namespace{
|
||||
Name: "TestInsertLayerNamespace3",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Name: "TestInsertLayerFeature7",
|
||||
},
|
||||
Version: "0.01",
|
||||
}
|
||||
|
||||
l3, _ := datastore.FindLayer("TestInsertLayer3", true, false)
|
||||
l3u := database.Layer{
|
||||
Name: l3.Name,
|
||||
Parent: l3.Parent,
|
||||
Namespace: &database.Namespace{
|
||||
Name: "TestInsertLayerNamespaceUpdated1",
|
||||
VersionFormat: dpkg.ParserName,
|
||||
},
|
||||
Features: []database.FeatureVersion{f7},
|
||||
}
|
||||
|
||||
l4u := database.Layer{
|
||||
Name: "TestInsertLayer4",
|
||||
Parent: &database.Layer{Name: "TestInsertLayer3"},
|
||||
Features: []database.FeatureVersion{f7},
|
||||
EngineVersion: 2,
|
||||
}
|
||||
|
||||
// Try to re-insert without increasing the EngineVersion.
|
||||
err := datastore.InsertLayer(l3u)
|
||||
assert.Nil(t, err)
|
||||
|
||||
l3uf, err := datastore.FindLayer(l3u.Name, true, false)
|
||||
if assert.Nil(t, err) {
|
||||
assert.Equal(t, l3.Namespace.Name, l3uf.Namespace.Name)
|
||||
assert.Equal(t, l3.EngineVersion, l3uf.EngineVersion)
|
||||
assert.Len(t, l3uf.Features, len(l3.Features))
|
||||
}
|
||||
|
||||
// Update layer l3.
|
||||
// Verify that the Namespace, EngineVersion and FeatureVersions got updated.
|
||||
l3u.EngineVersion = 2
|
||||
err = datastore.InsertLayer(l3u)
|
||||
assert.Nil(t, err)
|
||||
|
||||
l3uf, err = datastore.FindLayer(l3u.Name, true, false)
|
||||
if assert.Nil(t, err) {
|
||||
assert.Equal(t, l3u.Namespace.Name, l3uf.Namespace.Name)
|
||||
assert.Equal(t, l3u.EngineVersion, l3uf.EngineVersion)
|
||||
if assert.Len(t, l3uf.Features, 1) {
|
||||
assert.True(t, cmpFV(l3uf.Features[0], f7), "Updated layer should have %#v but actually have %#v", f7, l3uf.Features[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Update layer l4.
|
||||
// Verify that the Namespace got updated from its new Parent's, and also verify the
|
||||
// EnginVersion and FeatureVersions.
|
||||
l4u.Parent = &l3uf
|
||||
err = datastore.InsertLayer(l4u)
|
||||
assert.Nil(t, err)
|
||||
|
||||
l4uf, err := datastore.FindLayer(l3u.Name, true, false)
|
||||
if assert.Nil(t, err) {
|
||||
assert.Equal(t, l3u.Namespace.Name, l4uf.Namespace.Name)
|
||||
assert.Equal(t, l4u.EngineVersion, l4uf.EngineVersion)
|
||||
if assert.Len(t, l4uf.Features, 1) {
|
||||
assert.True(t, cmpFV(l3uf.Features[0], f7), "Updated layer should have %#v but actually have %#v", f7, l4uf.Features[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertLayerDelete(t *testing.T, datastore database.Datastore) {
|
||||
err := datastore.DeleteLayer("TestInsertLayerX")
|
||||
assert.Equal(t, commonerr.ErrNotFound, err)
|
||||
|
||||
err = datastore.DeleteLayer("TestInsertLayer3")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = datastore.FindLayer("TestInsertLayer3", false, false)
|
||||
assert.Equal(t, commonerr.ErrNotFound, err)
|
||||
|
||||
_, err = datastore.FindLayer("TestInsertLayer4a", false, false)
|
||||
assert.Equal(t, commonerr.ErrNotFound, err)
|
||||
|
||||
_, err = datastore.FindLayer("TestInsertLayer4b", true, false)
|
||||
assert.Equal(t, commonerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
func cmpFV(a, b database.FeatureVersion) bool {
|
||||
return a.Feature.Name == b.Feature.Name &&
|
||||
a.Feature.Namespace.Name == b.Feature.Namespace.Name &&
|
||||
a.Version == b.Version
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
// Copyright 2017 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
// Lock tries to set a temporary lock in the database.
|
||||
//
|
||||
// Lock does not block, instead, it returns true and its expiration time
|
||||
// is the lock has been successfully acquired or false otherwise
|
||||
func (pgSQL *pgSQL) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time) {
|
||||
if name == "" || owner == "" || duration == 0 {
|
||||
log.Warning("could not create an invalid lock")
|
||||
return false, time.Time{}
|
||||
}
|
||||
|
||||
defer observeQueryTime("Lock", "all", time.Now())
|
||||
|
||||
// Compute expiration.
|
||||
until := time.Now().Add(duration)
|
||||
|
||||
if renew {
|
||||
// Renew lock.
|
||||
r, err := pgSQL.Exec(updateLock, name, owner, until)
|
||||
if err != nil {
|
||||
handleError("updateLock", err)
|
||||
return false, until
|
||||
}
|
||||
if n, _ := r.RowsAffected(); n > 0 {
|
||||
// Updated successfully.
|
||||
return true, until
|
||||
}
|
||||
} else {
|
||||
// Prune locks.
|
||||
pgSQL.pruneLocks()
|
||||
}
|
||||
|
||||
// Lock.
|
||||
_, err := pgSQL.Exec(insertLock, name, owner, until)
|
||||
if err != nil {
|
||||
if !isErrUniqueViolation(err) {
|
||||
handleError("insertLock", err)
|
||||
}
|
||||
return false, until
|
||||
}
|
||||
|
||||
return true, until
|
||||
}
|
||||
|
||||
// Unlock unlocks a lock specified by its name if I own it
|
||||
func (pgSQL *pgSQL) Unlock(name, owner string) {
|
||||
if name == "" || owner == "" {
|
||||
log.Warning("could not delete an invalid lock")
|
||||
return
|
||||
}
|
||||
|
||||
defer observeQueryTime("Unlock", "all", time.Now())
|
||||
|
||||
pgSQL.Exec(removeLock, name, owner)
|
||||
}
|
||||
|
||||
// FindLock returns the owner of a lock specified by its name and its
|
||||
// expiration time.
|
||||
func (pgSQL *pgSQL) FindLock(name string) (string, time.Time, error) {
|
||||
if name == "" {
|
||||
log.Warning("could not find an invalid lock")
|
||||
return "", time.Time{}, commonerr.NewBadRequestError("could not find an invalid lock")
|
||||
}
|
||||
|
||||
defer observeQueryTime("FindLock", "all", time.Now())
|
||||
|
||||
var owner string
|
||||
var until time.Time
|
||||
err := pgSQL.QueryRow(searchLock, name).Scan(&owner, &until)
|
||||
if err != nil {
|
||||
return owner, until, handleError("searchLock", err)
|
||||
}
|
||||
|
||||
return owner, until, nil
|
||||
}
|
||||
|
||||
// pruneLocks removes every expired locks from the database
|
||||
func (pgSQL *pgSQL) pruneLocks() {
|
||||
defer observeQueryTime("pruneLocks", "all", time.Now())
|
||||
|
||||
if _, err := pgSQL.Exec(removeLockExpired); err != nil {
|
||||
handleError("removeLockExpired", err)
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package lock
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/clair/database/pgsql/monitoring"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
searchLock = `SELECT until FROM Lock WHERE name = $1`
|
||||
updateLock = `UPDATE Lock SET until = $3 WHERE name = $1 AND owner = $2`
|
||||
removeLock = `DELETE FROM Lock WHERE name = $1 AND owner = $2`
|
||||
removeLockExpired = `DELETE FROM LOCK WHERE until < $1`
|
||||
|
||||
soiLock = `
|
||||
WITH new_lock AS (
|
||||
INSERT INTO lock (name, owner, until)
|
||||
SELECT CAST ($1 AS TEXT), CAST ($2 AS TEXT), CAST ($3 AS TIMESTAMP)
|
||||
WHERE NOT EXISTS (SELECT id FROM lock WHERE name = $1)
|
||||
RETURNING owner, until
|
||||
)
|
||||
SELECT * FROM new_lock
|
||||
UNION
|
||||
SELECT owner, until FROM lock WHERE name = $1`
|
||||
)
|
||||
|
||||
func AcquireLock(tx *sql.Tx, lockName, whoami string, desiredDuration time.Duration) (bool, time.Time, error) {
|
||||
if lockName == "" || whoami == "" || desiredDuration == 0 {
|
||||
panic("invalid lock parameters")
|
||||
}
|
||||
|
||||
if err := PruneLocks(tx); err != nil {
|
||||
return false, time.Time{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
desiredLockedUntil = time.Now().UTC().Add(desiredDuration)
|
||||
|
||||
lockedUntil time.Time
|
||||
lockOwner string
|
||||
)
|
||||
|
||||
defer monitoring.ObserveQueryTime("Lock", "soiLock", time.Now())
|
||||
err := tx.QueryRow(soiLock, lockName, whoami, desiredLockedUntil).Scan(&lockOwner, &lockedUntil)
|
||||
return lockOwner == whoami, lockedUntil, util.HandleError("AcquireLock", err)
|
||||
}
|
||||
|
||||
func ExtendLock(tx *sql.Tx, lockName, whoami string, desiredDuration time.Duration) (bool, time.Time, error) {
|
||||
if lockName == "" || whoami == "" || desiredDuration == 0 {
|
||||
panic("invalid lock parameters")
|
||||
}
|
||||
|
||||
desiredLockedUntil := time.Now().Add(desiredDuration)
|
||||
|
||||
defer monitoring.ObserveQueryTime("Lock", "update", time.Now())
|
||||
result, err := tx.Exec(updateLock, lockName, whoami, desiredLockedUntil)
|
||||
if err != nil {
|
||||
return false, time.Time{}, util.HandleError("updateLock", err)
|
||||
}
|
||||
|
||||
if numRows, err := result.RowsAffected(); err == nil {
|
||||
// This is the only happy path.
|
||||
return numRows > 0, desiredLockedUntil, nil
|
||||
}
|
||||
|
||||
return false, time.Time{}, util.HandleError("updateLock", err)
|
||||
}
|
||||
|
||||
func ReleaseLock(tx *sql.Tx, name, owner string) error {
|
||||
if name == "" || owner == "" {
|
||||
panic("invalid lock parameters")
|
||||
}
|
||||
|
||||
defer monitoring.ObserveQueryTime("Unlock", "all", time.Now())
|
||||
_, err := tx.Exec(removeLock, name, owner)
|
||||
return err
|
||||
}
|
||||
|
||||
// pruneLocks removes every expired locks from the database
|
||||
func PruneLocks(tx *sql.Tx) error {
|
||||
defer monitoring.ObserveQueryTime("pruneLocks", "all", time.Now())
|
||||
|
||||
if r, err := tx.Exec(removeLockExpired, time.Now().UTC()); err != nil {
|
||||
return util.HandleError("removeLockExpired", err)
|
||||
} else if affected, err := r.RowsAffected(); err != nil {
|
||||
return util.HandleError("removeLockExpired", err)
|
||||
} else {
|
||||
log.Debugf("Pruned %d Locks", affected)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
// Copyright 2019 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package lock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAcquireLockReturnsExistingLockDuration(t *testing.T) {
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "Lock")
|
||||
defer cleanup()
|
||||
|
||||
acquired, originalExpiration, err := AcquireLock(tx, "test1", "owner1", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, acquired)
|
||||
|
||||
acquired2, expiration, err := AcquireLock(tx, "test1", "owner2", time.Hour)
|
||||
require.Nil(t, err)
|
||||
require.False(t, acquired2)
|
||||
require.Equal(t, expiration, originalExpiration)
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
db, cleanup := testutil.CreateTestDBWithFixture(t, "Lock")
|
||||
defer cleanup()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a first lock.
|
||||
l, _, err := AcquireLock(tx, "test1", "owner1", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, true)
|
||||
|
||||
// lock again by itself, the previous lock is not expired yet.
|
||||
l, _, err = AcquireLock(tx, "test1", "owner1", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, false)
|
||||
|
||||
// Try to renew the same lock with another owner.
|
||||
l, _, err = ExtendLock(tx, "test1", "owner2", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.False(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, false)
|
||||
|
||||
l, _, err = AcquireLock(tx, "test1", "owner2", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.False(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, false)
|
||||
|
||||
// Renew the lock.
|
||||
l, _, err = ExtendLock(tx, "test1", "owner1", 2*time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, true)
|
||||
|
||||
// Unlock and then relock by someone else.
|
||||
err = ReleaseLock(tx, "test1", "owner1")
|
||||
require.Nil(t, err)
|
||||
tx = testutil.RestartTransaction(db, tx, true)
|
||||
|
||||
l, _, err = AcquireLock(tx, "test1", "owner2", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, true)
|
||||
|
||||
// Create a second lock which is actually already expired ...
|
||||
l, _, err = AcquireLock(tx, "test2", "owner1", -time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, true)
|
||||
|
||||
// Take over the lock
|
||||
l, _, err = AcquireLock(tx, "test2", "owner2", time.Minute)
|
||||
require.Nil(t, err)
|
||||
require.True(t, l)
|
||||
tx = testutil.RestartTransaction(db, tx, true)
|
||||
|
||||
require.Nil(t, tx.Rollback())
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
datastore, err := openDatabaseForTest("InsertNamespace", false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer datastore.Close()
|
||||
|
||||
var l bool
|
||||
var et time.Time
|
||||
|
||||
// Create a first lock.
|
||||
l, _ = datastore.Lock("test1", "owner1", time.Minute, false)
|
||||
assert.True(t, l)
|
||||
|
||||
// Try to lock the same lock with another owner.
|
||||
l, _ = datastore.Lock("test1", "owner2", time.Minute, true)
|
||||
assert.False(t, l)
|
||||
|
||||
l, _ = datastore.Lock("test1", "owner2", time.Minute, false)
|
||||
assert.False(t, l)
|
||||
|
||||
// Renew the lock.
|
||||
l, _ = datastore.Lock("test1", "owner1", 2*time.Minute, true)
|
||||
assert.True(t, l)
|
||||
|
||||
// Unlock and then relock by someone else.
|
||||
datastore.Unlock("test1", "owner1")
|
||||
|
||||
l, et = datastore.Lock("test1", "owner2", time.Minute, false)
|
||||
assert.True(t, l)
|
||||
|
||||
// LockInfo
|
||||
o, et2, err := datastore.FindLock("test1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "owner2", o)
|
||||
assert.Equal(t, et.Second(), et2.Second())
|
||||
|
||||
// Create a second lock which is actually already expired ...
|
||||
l, _ = datastore.Lock("test2", "owner1", -time.Minute, false)
|
||||
assert.True(t, l)
|
||||
|
||||
// Take over the lock
|
||||
l, _ = datastore.Lock("test2", "owner2", time.Minute, false)
|
||||
assert.True(t, l)
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/remind101/migrate"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// This migration removes the data maintained by the previous migration tool
|
||||
// (liamstask/goose), and if it was present, mark the 00002_initial_schema
|
||||
// migration as done.
|
||||
RegisterMigration(migrate.Migration{
|
||||
ID: 1,
|
||||
Up: func(tx *sql.Tx) error {
|
||||
// Verify that goose was in use before, otherwise skip this migration.
|
||||
var e bool
|
||||
err := tx.QueryRow("SELECT true FROM pg_class WHERE relname = $1", "goose_db_version").Scan(&e)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete goose's data.
|
||||
_, err = tx.Exec("DROP TABLE goose_db_version CASCADE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Mark the '00002_initial_schema' as done.
|
||||
_, err = tx.Exec("INSERT INTO schema_migrations (version) VALUES (2)")
|
||||
|
||||
return err
|
||||
},
|
||||
Down: migrate.Queries([]string{}),
|
||||
})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue