Compare commits

..

17 Commits

Author SHA1 Message Date
Jimmy Zelinskie aa5729df66
Merge pull request #530 from meringu/patch-1
6 years ago
Henry Muru Paenga a80ca551cf imgfmt: download using http proxy from env
6 years ago
Jimmy Zelinskie ee1b3ff7f1
Merge pull request #568 from MackJM/release-2.0
6 years ago
Jean Michel MacKay af7d890865 Moved the ubuntu updater to use the new git repo of cve-tracker, replacing bzr
6 years ago
Jimmy Zelinskie 8bea33576d
Merge pull request #562 from ninjaMog/ubuntu-tracker-update
6 years ago
Jimmy Zelinskie 5d5c193089
Merge pull request #565 from ninjaMog/nvd-endpoint-update
6 years ago
Nick Johns 086f92a617 /ext/vulnmdsrc/nvd Switched NVD URLs to https
6 years ago
Nick Johns d1cadb4cdc ext/vulnsrc/ubuntu: updated tracker src
6 years ago
Jimmy Zelinskie f882e1c210 database: add ubuntu bionic namespace mapping
6 years ago
Jimmy Zelinskie 47450faf88
Merge pull request #554 from usr42/release-2.0_go1.10
6 years ago
usr42 ad98b97a6d Use golang:1.10-alpine with v2.0.2
6 years ago
Brad Ison 21715797c9
Merge pull request #531 from bison/oracle-regex
6 years ago
Joe Ray e650d58583
featurens: Ensure RHEL is correctly identified
6 years ago
Jimmy Zelinskie f8a1359a60 Merge pull request #423 from jzelinskie/sleep-updater
7 years ago
Jimmy Zelinskie b2519a044a Merge pull request #407 from swestcott/kubernetes-config-fix
7 years ago
Jimmy Zelinskie 2453d67c36 Merge pull request #413 from transcedentalia/master
7 years ago
Jimmy Zelinskie 4f8d6bee1e Merge pull request #416 from tianon/debian-buster
7 years ago

@ -1,5 +1,4 @@
.dockerignore
.travis.yml
.*
*.md
DCO
LICENSE

@ -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"

12
.github/stale.yml vendored

@ -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

@ -1,24 +1,10 @@
language: go
go:
- "1.11.x"
sudo: required
env:
global:
- PATH=$HOME/.local/bin:$PATH
install:
- curl https://glide.sh/get | sh
- mkdir -p $HOME/.local/bin
- curl -o $HOME/.local/bin/prototool -sSL https://github.com/uber/prototool/releases/download/v0.1.0/prototool-$(uname -s)-$(uname -m)
- chmod +x $HOME/.local/bin/prototool
script:
- diff -u <(echo -n) <(gofmt -l -s $(go list -f '{{.Dir}}') | grep -v '/vendor/')
- prototool format -d api/v3/clairpb/clair.proto
- prototool lint api/v3/clairpb/clair.proto
- go test $(glide novendor | grep -v contrib)
dist: trusty
@ -31,6 +17,11 @@ notifications:
matrix:
include:
- addons:
apt:
packages:
- rpm
postgresql: 9.4
- addons:
apt:
packages:

@ -12,16 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.10-alpine AS build
RUN apk add --no-cache git build-base
ADD . /go/src/github.com/coreos/clair/
WORKDIR /go/src/github.com/coreos/clair/
RUN export CLAIR_VERSION=$(git describe --tag --always --dirty) && \
go build -ldflags "-X github.com/coreos/clair/pkg/version.Version=$CLAIR_VERSION" github.com/coreos/clair/cmd/clair
FROM golang:1.10-alpine
FROM alpine:3.8
COPY --from=build /go/src/github.com/coreos/clair/clair /clair
RUN apk add --no-cache git rpm xz ca-certificates dumb-init
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/clair"]
VOLUME /config
EXPOSE 6060 6061
ADD . /go/src/github.com/coreos/clair/
WORKDIR /go/src/github.com/coreos/clair/
RUN apk add --no-cache git rpm xz && \
go install -v github.com/coreos/clair/cmd/clair && \
mv /go/bin/clair /clair && \
rm -rf /go /usr/local/go
ENTRYPOINT ["/clair"]

@ -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.

@ -8,12 +8,6 @@ This document tracks projects that integrate with Clair. [Join the community](ht
[Dockyard](https://github.com/Huawei/dockyard): an open source container registry with Clair integration.
[Yair](https://github.com/yfoelling/yair): a lightweight command-line for working with clair with many different outputs. Mainly designed for usage in a CI Job.
[Claircli](https://github.com/joelee2012/claircli): A simple cmd tool to interact with CoreOS Clair.
[Paclair](https://github.com/yebinama/paclair): a Python3 CLI tool to interact with Clair (easily configurable to access private registries).
[Clairctl](https://github.com/jgsqware/clairctl): a lightweight command-line tool for working locally with Clair and generate HTML report.
[Clair-SQS](https://github.com/zalando-incubator/clair-sqs): a container containing Clair and additional processes that integrate Clair with [Amazon SQS][sqs].
@ -26,10 +20,5 @@ This document tracks projects that integrate with Clair. [Join the community](ht
[check_openvz_mirror_with_clair](https://github.com/FastVPSEestiOu/check_openvz_mirror_with_clair): a tool to use Clair to analyze OpenVZ templates
[Portus](http://port.us.org/features/6_security_scanning.html#coreos-clair): an authorization service and frontend for Docker registry (v2).
[clair-scanner](https://github.com/arminc/clair-scanner): a project similar to 'analyze-local-images' with a whitelisting feature
[sqs]: https://aws.amazon.com/sqs/
[clair-singularity](https://github.com/dctrud/clair-singularity): a command-line tool to scan [Singularity](http://singularity.lbl.gov/) container images using Clair.

@ -1,14 +1,13 @@
# Notifications
Notifications are a way for Clair to inform another service that changes to tracked vulnerabilities have occurred.
Because changes to vulnerabilities also contain the set of affected images, Clair sends only the name of the notification to another service, then depends on that service read and mark the notification as read using Clair's API.
Notifications are a way for Clair to inform an endpoint that changes to tracked vulnerabilities have occurred.
Because notification data can require pagination, Clair should only send the name of a notification.
If a notification is not marked as read, Clair will resend notifications at a configured interval.
It is expected that the receiving endpoint calls the Clair API for reading notifications and marking them as read after being notified.
If the notification is never marked as read, Clair will continue attempting to send the same notification to the endpoint indefinitely.
# Webhook
## Webhook
Notifications are an extensible component of Clair, but out of the box Clair supports [webhooks].
The webhooks look like the following:
Webhook is an out-of-the-box notifier that sends the following JSON object via an HTTP POST:
```json
{
@ -18,7 +17,11 @@ The webhooks look like the following:
}
```
If you're interested in adding your own notification senders, read the documentation on [adding new drivers].
## Custom Notifiers
[webhooks]: https://en.wikipedia.org/wiki/Webhook
[adding new drivers]: /Documentation/drivers-and-data-sources.md#adding-new-drivers
Clair can also be compiled with custom notifiers by importing them in `main.go`.
Custom notifiers are any Go package that implements the `Notifier` interface and registers themselves with the `notifier` package.
Notifiers are registered in [init()] similar to drivers for Go's standard [database/sql] package.
[init()]: https://golang.org/doc/effective_go.html#init
[database/sql]: https://godoc.org/database/sql

@ -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/

@ -4,4 +4,5 @@ This document tracks people and use cases for Clair in production. [Join the com
## [Quay.io](https://quay.io/)
Quay.io is hosted and enterprise-ready container registry. It displays the results of a Clair security scan for each image uploaded to the registry.
Quay is CoreOS' enterprise-ready container registry. It displays the results of a Clair security scan on each hosted image's page.

@ -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,6 +1,3 @@
[![Build Status](https://drone.nixaid.com/api/badges/arno/clair/status.svg)](https://drone.nixaid.com/arno/clair)
----
# Clair
[![Build Status](https://api.travis-ci.org/coreos/clair.svg?branch=master "Build Status")](https://travis-ci.org/coreos/clair)
@ -14,12 +11,12 @@ Please use [releases] instead of the `master` branch in order to get stable bina
![Clair Logo](https://cloud.githubusercontent.com/assets/343539/21630811/c5081e5c-d202-11e6-92eb-919d5999c77a.png)
Clair is an open source project for the [static analysis] of vulnerabilities in application containers (currently including [appc] and [docker]).
Clair is an open source project for the static analysis of vulnerabilities in application containers (currently including [appc] and [docker]).
1. In regular intervals, Clair ingests vulnerability metadata from a configured set of sources and stores it in the database.
2. Clients use the Clair API to index their container images; this creates a list of _features_ present in the image and stores them in the database.
3. Clients use the Clair API to query the database for vulnerabilities of a particular image; correlating vulnerabilities and features is done for each request, avoiding the need to rescan images.
4. When updates to vulnerability metadata occur, a notification can be sent to alert systems that a change has occured.
2. Clients use the Clair API to index their container images; this parses a list of installed _source packages_ and stores them in the database.
3. Clients use the Clair API to query the database; correlating data is done in real time, rather than a cached result that needs re-scanning.
4. When updates to vulnerability metadata occur, a webhook containg the affected images can be configured to page or block deployments.
Our goal is to enable a more transparent view of the security of container-based infrastructure.
Thus, the project was named `Clair` after the French term which translates to *clear*, *bright*, *transparent*.
@ -27,35 +24,199 @@ Thus, the project was named `Clair` after the French term which translates to *c
[appc]: https://github.com/appc/spec
[docker]: https://github.com/docker/docker/blob/master/image/spec/v1.2.md
[releases]: https://github.com/coreos/clair/releases
[static analysis]: https://en.wikipedia.org/wiki/Static_program_analysis
## Getting Started
## When would I use Clair?
* You've found an image by searching the internet and want to determine if it's safe enough for you to use in production.
* You're regularly deploying into a containerized production environment and want operations to alert or block deployments on insecure software.
## Documentation
* [The CoreOS website] has a rendered version of the latest stable documentation
* [Inside the Documentation directory] is the source markdown files for documentation
[The CoreOS website]: https://coreos.com/clair/docs/latest/
[Inside the Documentation directory]: /Documentation
## Deploying Clair
### 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
### Commercially Supported
Clair is professionally supported as a data source for the [Quay] Security Scanning feature.
The setup documentation for using Clair for this environment can be found on the [Quay documentation] on the [CoreOS] website.
Be sure to adjust the version of the documentation to the version of Quay being used in your deployment.
[Quay]: https://quay.io
[Quay documentation]: https://coreos.com/quay-enterprise/docs/latest/clair.html
[CoreOS]: https://coreos.com
### Community Supported
**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 database instance is required for all instructions.
Clair currently supports and tests against:
* [Postgres] 9.4
* [Postgres] 9.5
* [Postgres] 9.6
[Postgres]: https://www.postgresql.org
#### Kubernetes
If you don't have a local Kubernetes cluster already, check out [minikube].
[minikube]: https://github.com/kubernetes/minikube
```
git clone https://github.com/coreos/clair
cd clair/contrib/k8s
kubectl create secret generic clairsecret --from-file=./config.yaml
kubectl create -f clair-kubernetes.yaml
```
#### Docker Compose
```sh
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/docker-compose.yml -o $HOME/docker-compose.yml
$ mkdir $HOME/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.example.yaml -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.example.yaml -o $PWD/clair_config/config.yaml
$ docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
$ docker run -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 to 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
```
## Frequently Asked Questions
### Who's using Clair?
You can find [production users] and third party [integrations] documented in their respective pages of the local documentation.
[production users]: https://github.com/coreos/clair/blob/master/Documentation/production-users.md
[integrations]: https://github.com/coreos/clair/blob/master/Documentation/integrations.md
### What do you mean by static analysis?
There are two major ways to perform analysis of programs: [Static Analysis] and [Dynamic Analysis].
Clair has been designed to perform *static analysis*; containers never need to be executed.
Rather, the filesystem of the container image is inspected and *features* are indexed into a database.
By indexing the features of an image into the database, images only need to be rescanned when new *detectors* are added.
[Static Analysis]: https://en.wikipedia.org/wiki/Static_program_analysis
[Dynamic Analysis]: https://en.wikipedia.org/wiki/Dynamic_program_analysis
### What data sources does Clair currently support?
| 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] |
| [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/
[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
### What do most deployments look like?
From a high-level, most deployments integrate with the registry workflow rather than manual API usage by a human.
They typically take up a form similar to the following diagram:
![Simple Clair Diagram](https://cloud.githubusercontent.com/assets/343539/21630809/c1adfbd2-d202-11e6-9dfe-9024139d0a28.png)
### 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.
* Learn [the terminology] and about the [drivers and data sources] that power Clair
* Watch [presentations] on the high-level goals and design of Clair
* Follow instructions to get Clair [up and running]
* Explore [the API] on SwaggerHub
* Discover third party [integrations] that help integrate Clair with your infrastructure
* Read the rest of the documentation on the [CoreOS website] or in the [Documentation directory]
### What terminology do I need to understand to work with Clair internals?
[the terminology]: /Documentation/terminology.md
[drivers and data sources]: /Documentation/drivers-and-data-sources.md
[presentations]: /Documentation/presentations.md
[up and running]: /Documentation/running-clair.md
[the API]: https://app.swaggerhub.com/apis/coreos/clair/3.0
[integrations]: /Documentation/integrations.md
[CoreOS website]: https://coreos.com/clair/docs/latest/
[Documentation directory]: /Documentation
- *Image* - a tarball of the contents of a container
- *Layer* - an *appc* or *Docker* image that may or may not be dependent on another image
- *Feature* - anything that when present could be an indication of a *vulnerability* (e.g. the presence of a file or an installed software package)
- *Feature Namespace* - a context around *features* and *vulnerabilities* (e.g. an operating system)
- *Vulnerability Updater* - a Go package that tracks upstream vulnerability data and imports them into Clair
- *Vulnerability Metadata Appender* - a Go package that tracks upstream vulnerability metadata and appends them into vulnerabilities managed by Clair
## Contact
### How can I customize Clair?
- IRC: #[clair](irc://irc.freenode.org:6667/#clair) on freenode.org
- Bugs: [issues](https://github.com/coreos/clair/issues)
The major components of Clair are all programmatically extensible in the same way Go's standard [database/sql] package is extensible.
Everything extensible is located in the `ext` directory.
## Contributing
Custom behavior can be accomplished by creating a package that contains a type that implements an interface declared in Clair and registering that interface in [init()].
To expose the new behavior, unqualified imports to the package must be added in your own custom [main.go], which should then start Clair using `Boot(*config.Config)`.
See [CONTRIBUTING](.github/CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
[database/sql]: https://godoc.org/database/sql
[init()]: https://golang.org/doc/effective_go.html#init
[main.go]: https://github.com/coreos/clair/blob/master/cmd/clair/main.go
## License
### Are there any public presentations on Clair?
Clair is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
- _Clair: The Container Image Security Analyzer @ ContainerDays Boston 2016_ - [Event](http://dynamicinfradays.org/events/2016-boston/) [Video](https://www.youtube.com/watch?v=Kri67PtPv6s) [Slides](https://docs.google.com/presentation/d/1ExQGZs-pQ56TpW_ifcUl2l_ml87fpCMY6-wdug87OFU)
- _Identifying Common Vulnerabilities and Exposures in Containers with Clair @ CoreOS Fest 2016_ - [Event](https://coreos.com/fest/) [Video](https://www.youtube.com/watch?v=YDCa51BK2q0) [Slides](https://docs.google.com/presentation/d/1pHSI_5LcjnZzZBPiL1cFTZ4LvhzKtzh86eE010XWNLY)
- _Clair: A Container Image Security Analyzer @ Microservices NYC_ - [Event](https://www.meetup.com/Microservices-NYC/events/230023492/) [Video](https://www.youtube.com/watch?v=ynwKi2yhIX4) [Slides](https://docs.google.com/presentation/d/1ly9wQKQIlI7rlb0JNU1_P-rPDHU4xdRCCM3rxOdjcgc)
- _Clair: A Container Image Security Analyzer @ Container Orchestration NYC_ - [Event](https://www.meetup.com/Container-Orchestration-NYC/events/229779466/) [Video](https://www.youtube.com/watch?v=wTfCOUDNV_M) [Slides](https://docs.google.com/presentation/d/1ly9wQKQIlI7rlb0JNU1_P-rPDHU4xdRCCM3rxOdjcgc)

@ -8,12 +8,8 @@ The [milestones defined in GitHub](https://github.com/coreos/clair/milestones) r
The roadmap below outlines new features that will be added to Clair, and while subject to change, define what future stable will look like.
- Support multiple namespaces per image
- This enables language-level package managers (e.g. npm, pip) in the future
- Take advantage of OCI/Docker content-addressiblity to avoid duplicated work
- This simplifies the amount of work required for an offline clair in the future
- Support mappings between source packages and binary packages
- Versioned detectors that are present in API results
- This will enable clients to determine when images need to be reindexed
- gRPC API that works on sets of layers rather than individual layers
- Structured logging in JSON
- This enables language-level package managers (e.g. npm, pip)
- Improve coverage and readability of documentation
- Decouple the project from Postgres
- gRPC API supporting direct uploads of images
- Support operating Clair without internet access

@ -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))
})
}
}

@ -1,4 +1,4 @@
// Copyright 2018 clair authors
// 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.
@ -15,13 +15,17 @@
package api
import (
"context"
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net"
"net/http"
"strconv"
"time"
log "github.com/sirupsen/logrus"
"github.com/tylerb/graceful"
"github.com/coreos/clair/api/v3"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/stopper"
)
@ -30,17 +34,44 @@ const timeoutResponse = `{"Error":{"Message":"Clair failed to respond within the
// Config is the configuration for the API service.
type Config struct {
Addr string
HealthAddr string
Port int
HealthPort int
Timeout time.Duration
PaginationKey string
CertFile, KeyFile, CAFile string
}
func Run(cfg *Config, store database.Datastore) {
err := v3.ListenAndServe(cfg.Addr, cfg.CertFile, cfg.KeyFile, cfg.CAFile, store)
func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) {
defer st.End()
// Do not run the API service if there is no config.
if cfg == nil {
log.Info("main API service is disabled.")
return
}
log.WithField("port", cfg.Port).Info("starting main API")
tlsConfig, err := tlsClientConfig(cfg.CAFile)
if err != nil {
log.WithError(err).Fatal("could not initialize gRPC server")
log.WithError(err).Fatal("could not initialize client cert authentication")
}
if tlsConfig != nil {
log.Info("main API configured with client certificate authentication")
}
srv := &graceful.Server{
Timeout: 0, // Already handled by our TimeOut middleware
NoSignalHandling: true, // We want to use our own Stopper
Server: &http.Server{
Addr: ":" + strconv.Itoa(cfg.Port),
TLSConfig: tlsConfig,
Handler: http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse),
},
}
listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile)
log.Info("main API stopped")
}
func RunHealth(cfg *Config, store database.Datastore, st *stopper.Stopper) {
@ -51,22 +82,69 @@ func RunHealth(cfg *Config, store database.Datastore, st *stopper.Stopper) {
log.Info("health API service is disabled.")
return
}
log.WithField("addr", cfg.HealthAddr).Info("starting health API")
log.WithField("port", cfg.HealthPort).Info("starting health API")
srv := http.Server{
Addr: cfg.HealthAddr,
Handler: http.TimeoutHandler(newHealthHandler(store), cfg.Timeout, timeoutResponse),
srv := &graceful.Server{
Timeout: 10 * time.Second, // Interrupt health checks when stopping
NoSignalHandling: true, // We want to use our own Stopper
Server: &http.Server{
Addr: ":" + strconv.Itoa(cfg.HealthPort),
Handler: http.TimeoutHandler(newHealthHandler(store), cfg.Timeout, timeoutResponse),
},
}
listenAndServeWithStopper(srv, st, "", "")
log.Info("health API stopped")
}
// listenAndServeWithStopper wraps graceful.Server's
// ListenAndServe/ListenAndServeTLS and adds the ability to interrupt them with
// the provided stopper.Stopper.
func listenAndServeWithStopper(srv *graceful.Server, st *stopper.Stopper, certFile, keyFile string) {
go func() {
<-st.Chan()
srv.Shutdown(context.TODO())
srv.Stop(0)
}()
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
var err error
if certFile != "" && keyFile != "" {
log.Info("API: TLS Enabled")
err = srv.ListenAndServeTLS(certFile, keyFile)
} else {
err = srv.ListenAndServe()
}
log.Info("health API stopped")
if err != nil {
if opErr, ok := err.(*net.OpError); !ok || (ok && opErr.Op != "accept") {
log.Fatal(err)
}
}
}
// tlsClientConfig initializes a *tls.Config using the given CA. The resulting
// *tls.Config is meant to be used to configure an HTTP server to do client
// certificate authentication.
//
// If no CA is given, a nil *tls.Config is returned; no client certificate will
// be required and verified. In other words, authentication will be disabled.
func tlsClientConfig(caPath string) (*tls.Config, error) {
if caPath == "" {
return nil, nil
}
caCert, err := ioutil.ReadFile(caPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
return tlsConfig, nil
}

@ -16,9 +16,12 @@ package api
import (
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/api/v1"
"github.com/coreos/clair/database"
)
@ -26,6 +29,34 @@ import (
// depending on the API version specified in the request URI.
type router map[string]*httprouter.Router
// Let's hope we never have more than 99 API versions.
const apiVersionLength = len("v99")
func newAPIHandler(cfg *Config, store database.Datastore) http.Handler {
router := make(router)
router["/v1"] = v1.NewRouter(store, cfg.PaginationKey)
return router
}
func (rtr router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlStr := r.URL.String()
var version string
if len(urlStr) >= apiVersionLength {
version = urlStr[:apiVersionLength]
}
if router, _ := rtr[version]; router != nil {
// Remove the version number from the request path to let the router do its
// job but do not update the RequestURI
r.URL.Path = strings.Replace(r.URL.Path, version, "", 1)
router.ServeHTTP(w, r)
return
}
log.WithFields(log.Fields{"status": http.StatusNotFound, "method": r.Method, "request uri": r.RequestURI, "remote addr": r.RemoteAddr}).Info("Served HTTP request")
http.NotFound(w, r)
}
func newHealthHandler(store database.Datastore) http.Handler {
router := httprouter.New()
router.GET("/health", healthHandler(store))

@ -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: &notification})
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 &noti, 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
}

@ -5,24 +5,24 @@
"confidence": 1
},
{
"project": "github.com/beorn7/perks/quantile",
"license": "MIT License",
"confidence": 0.989
"project": "github.com/coreos/go-systemd/journal",
"license": "Apache License 2.0",
"confidence": 0.997
},
{
"project": "github.com/coreos/pkg/timeutil",
"project": "github.com/coreos/pkg",
"license": "Apache License 2.0",
"confidence": 1
},
{
"project": "github.com/golang/protobuf/proto",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.92
"project": "github.com/davecgh/go-spew/spew",
"license": "ISC License",
"confidence": 1
},
{
"project": "github.com/google/uuid",
"project": "github.com/golang/protobuf/proto",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.966
"confidence": 0.92
},
{
"project": "github.com/matttproud/golang_protobuf_extensions/pbutil",
@ -34,6 +34,11 @@
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.966
},
{
"project": "github.com/pmezard/go-difflib/difflib",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.983
},
{
"project": "github.com/prometheus/client_golang/prometheus",
"license": "Apache License 2.0",
@ -50,28 +55,13 @@
"confidence": 1
},
{
"project": "github.com/prometheus/procfs/xfs",
"project": "github.com/prometheus/procfs",
"license": "Apache License 2.0",
"confidence": 1
},
{
"project": "github.com/sirupsen/logrus",
"license": "MIT License",
"confidence": 1
},
{
"project": "github.com/stretchr/testify/assert",
"license": "MIT License",
"confidence": 0.943
},
{
"project": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew",
"license": "ISC License",
"confidence": 0.985
},
{
"project": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.983
}
]

@ -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,4 +1,4 @@
// Copyright 2018 clair authors
// 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.
@ -20,15 +20,13 @@ import (
"os"
"time"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/coreos/clair"
"github.com/coreos/clair/api"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/notification"
"github.com/coreos/clair/ext/vulnsrc"
"github.com/coreos/clair/pkg/pagination"
"github.com/fernet/fernet-go"
)
// ErrDatasourceNotLoaded is returned when the datasource variable in the
@ -56,12 +54,11 @@ func DefaultConfig() Config {
Type: "pgsql",
},
Updater: &clair.UpdaterConfig{
EnabledUpdaters: vulnsrc.ListUpdaters(),
Interval: 1 * time.Hour,
Interval: 1 * time.Hour,
},
API: &api.Config{
HealthAddr: "0.0.0.0:6061",
Addr: "0.0.0.0:6060",
Port: 6060,
HealthPort: 6061,
Timeout: 900 * time.Second,
},
Notifier: &notification.Config{
@ -99,12 +96,16 @@ func LoadConfig(path string) (config *Config, err error) {
config = &cfgFile.Clair
// Generate a pagination key if none is provided.
if v, ok := config.Database.Options["paginationkey"]; !ok || v == nil || v.(string) == "" {
log.Warn("pagination key is empty, generating...")
config.Database.Options["paginationkey"] = pagination.Must(pagination.NewKey()).String()
if config.API.PaginationKey == "" {
var key fernet.Key
if err = key.Generate(); err != nil {
return
}
config.API.PaginationKey = key.Encode()
} else {
_, err = pagination.KeyFromString(config.Database.Options["paginationkey"].(string))
_, err = fernet.DecodeKey(config.API.PaginationKey)
if err != nil {
err = errors.New("Invalid Pagination key; must be 32-bit URL-safe base64")
return
}
}

@ -1,4 +1,4 @@
// Copyright 2018 clair authors
// 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.
@ -30,10 +30,9 @@ import (
"github.com/coreos/clair"
"github.com/coreos/clair/api"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/vulnsrc"
"github.com/coreos/clair/ext/imagefmt"
"github.com/coreos/clair/pkg/formatter"
"github.com/coreos/clair/pkg/stopper"
"github.com/coreos/clair/pkg/strutil"
// Register database driver.
_ "github.com/coreos/clair/database/pgsql"
@ -52,26 +51,12 @@ import (
_ "github.com/coreos/clair/ext/notification/webhook"
_ "github.com/coreos/clair/ext/vulnmdsrc/nvd"
_ "github.com/coreos/clair/ext/vulnsrc/alpine"
_ "github.com/coreos/clair/ext/vulnsrc/amzn"
_ "github.com/coreos/clair/ext/vulnsrc/debian"
_ "github.com/coreos/clair/ext/vulnsrc/oracle"
_ "github.com/coreos/clair/ext/vulnsrc/rhel"
_ "github.com/coreos/clair/ext/vulnsrc/suse"
_ "github.com/coreos/clair/ext/vulnsrc/ubuntu"
)
// MaxDBConnectionAttempts is the total number of tries that Clair will use to
// initially connect to a database at start-up.
const MaxDBConnectionAttempts = 20
// BinaryDependencies are the programs that Clair expects to be on the $PATH
// because it creates subprocesses of these programs.
var BinaryDependencies = []string{
"git",
"rpm",
"xz",
}
func waitForSignals(signals ...os.Signal) {
interrupts := make(chan os.Signal, 1)
signal.Notify(interrupts, signals...)
@ -100,46 +85,25 @@ func stopCPUProfiling(f *os.File) {
log.Info("stopped CPU profiling")
}
func configClairVersion(config *Config) {
clair.EnabledUpdaters = strutil.Intersect(config.Updater.EnabledUpdaters, vulnsrc.ListUpdaters())
log.WithFields(log.Fields{
"Detectors": database.SerializeDetectors(clair.EnabledDetectors()),
"Updaters": clair.EnabledUpdaters,
}).Info("enabled Clair extensions")
}
// Boot starts Clair instance with the provided config.
func Boot(config *Config) {
rand.Seed(time.Now().UnixNano())
st := stopper.NewStopper()
// Open database
var db database.Datastore
var dbError error
for attempts := 1; attempts <= MaxDBConnectionAttempts; attempts++ {
db, dbError = database.Open(config.Database)
if dbError == nil {
break
}
log.WithError(dbError).Error("failed to connect to database")
time.Sleep(time.Duration(attempts) * time.Second)
}
if dbError != nil {
log.Fatal(dbError)
db, err := database.Open(config.Database)
if err != nil {
log.Fatal(err)
}
defer db.Close()
clair.RegisterConfiguredDetectors(db)
// Start notifier
st.Begin()
go clair.RunNotifier(config.Notifier, db, st)
// Start API
go api.Run(config.API, db)
st.Begin()
go api.Run(config.API, db, st)
st.Begin()
go api.RunHealth(config.API, db, st)
@ -153,47 +117,46 @@ func Boot(config *Config) {
st.Stop()
}
// Initialize logging system
func configureLogger(flagLogLevel *string) {
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
if err != nil {
log.WithError(err).Error("failed to set logger parser level")
}
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
}
func main() {
// Parse command-line arguments
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
flagConfigPath := flag.String("config", "/etc/clair/config.yaml", "Load configuration from the specified file.")
flagCPUProfilePath := flag.String("cpu-profile", "", "Write a CPU profile to the specified file before exiting.")
flagLogLevel := flag.String("log-level", "info", "Define the logging level.")
flagInsecureTLS := flag.Bool("insecure-tls", false, "Disable TLS server's certificate chain and hostname verification when pulling layers.")
flag.Parse()
configureLogger(flagLogLevel)
// Check for dependencies.
for _, bin := range BinaryDependencies {
for _, bin := range []string{"git", "rpm", "xz"} {
_, err := exec.LookPath(bin)
if err != nil {
log.WithError(err).WithField("dependency", bin).Fatal("failed to find dependency")
}
}
// Load configuration
config, err := LoadConfig(*flagConfigPath)
if err != nil {
log.WithError(err).Fatal("failed to load configuration")
}
// Initialize logging system
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
// Enable CPU Profiling if specified
if *flagCPUProfilePath != "" {
defer stopCPUProfiling(startCPUProfiling(*flagCPUProfilePath))
}
// configure updater and worker
configClairVersion(config)
// Enable TLS server's certificate chain and hostname verification
// when pulling layers if specified
if *flagInsecureTLS {
imagefmt.SetInsecureTLS(*flagInsecureTLS)
}
Boot(config)
}

@ -1,61 +0,0 @@
## CoreOS Community Code of Conduct
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently applying these
principles to every aspect of managing this project. Project maintainers who do
not follow or enforce the Code of Conduct may be permanently removed from the
project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer, Brandon Philips
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/
### CoreOS Events Code of Conduct
CoreOS events are working conferences intended for professional networking and
collaboration in the CoreOS community. Attendees are expected to behave
according to professional standards and in accordance with their employers
policies on appropriate workplace behavior.
While at CoreOS events or related social networking opportunities, attendees
should not engage in discriminatory or offensive speech or actions including
but not limited to gender, sexuality, race, age, disability, or religion.
Speakers should be especially aware of these concerns.
CoreOS does not condone any statements by speakers contrary to these standards.
CoreOS reserves the right to deny entrance and/or eject from an event (without
refund) any individual found to be engaging in discriminatory or offensive
speech or actions.
Please bring any concerns to the immediate attention of designated on-site
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.

@ -26,22 +26,22 @@ clair:
# 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:
api:
# v3 grpc/RESTful API server address
addr: "0.0.0.0:6060"
# API server port
port: 6060
# Health server address
# Health server port
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthaddr: "0.0.0.0:6061"
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
@ -55,13 +55,6 @@ clair:
# Frequency the database will be updated with vulnerabilities from the default data sources
# The value 0 disables the updater entirely.
interval: 2h
enabledupdaters:
- debian
- ubuntu
- rhel
- oracle
- alpine
- suse
notifier:
# Number of attempts before the notification is marked as failed to be sent
@ -79,9 +72,9 @@ clair:
# https://github.com/cloudflare/cfssl
# https://github.com/coreos/etcd-ca
servername:
cafile:
keyfile:
certfile:
cafile:
keyfile:
certfile:
# Optional HTTP Proxy: must be a valid URL (including the scheme).
proxy:

@ -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,4 +1,4 @@
// Copyright 2019 clair authors
// 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.
@ -17,29 +17,20 @@
package database
import (
"errors"
"fmt"
"time"
"github.com/coreos/clair/pkg/pagination"
)
var (
// ErrBackendException is an error that occurs when the database backend
// does not work properly (ie. unreachable).
ErrBackendException = NewStorageError("an error occurred when querying the backend")
// ErrBackendException is an error that occurs when the database backend does
// not work properly (ie. unreachable).
ErrBackendException = errors.New("database: an error occured when querying the backend")
// ErrInconsistent is an error that occurs when a database consistency check
// fails (i.e. when an entity which is supposed to be unique is detected
// twice)
ErrInconsistent = NewStorageError("inconsistent database")
// ErrInvalidParameters is an error that occurs when the parameters are not valid.
ErrInvalidParameters = NewStorageError("parameters are not valid")
// ErrMissingEntities is an error that occurs when an associated immutable
// entity doesn't exist in the database. This error can indicate a wrong
// implementation or corrupted database.
ErrMissingEntities = NewStorageError("associated immutable entities are missing in the database")
ErrInconsistent = errors.New("database: inconsistent database")
)
// RegistrableComponentConfig is a configuration block that can be used to
@ -52,8 +43,8 @@ type RegistrableComponentConfig struct {
var drivers = make(map[string]Driver)
// Driver is a function that opens a Datastore specified by its database driver
// type and specific configuration.
// Driver is a function that opens a Datastore specified by its database driver type and specific
// configuration.
type Driver func(RegistrableComponentConfig) (Datastore, error)
// Register makes a Constructor available by the provided name.
@ -79,131 +70,148 @@ func Open(cfg RegistrableComponentConfig) (Datastore, error) {
return driver(cfg)
}
// Session contains the required operations on a persistent data store for a
// Clair deployment.
//
// Session is started by Datastore.Begin and terminated with Commit or Rollback.
// Besides Commit and Rollback, other functions cannot be called after the
// session is terminated.
// Any function is not guaranteed to be called successfully if there's a session
// failure.
type Session interface {
// Commit commits changes to datastore.
//
// Commit call after Rollback does no-op.
Commit() error
// Datastore represents the required operations on a persistent data store for
// a Clair deployment.
type Datastore interface {
// ListNamespaces returns the entire list of known Namespaces.
ListNamespaces() ([]Namespace, error)
// Rollback drops changes to datastore.
// InsertLayer stores a Layer in the database.
//
// Rollback call after Commit does no-op.
Rollback() error
// UpsertAncestry inserts or replaces an ancestry and its namespaced
// features and processors used to scan the ancestry.
UpsertAncestry(Ancestry) error
// FindAncestry retrieves an ancestry with all detected
// namespaced features. If the ancestry is not found, return false.
FindAncestry(name string) (ancestry Ancestry, found bool, err error)
// PersistDetector inserts a slice of detectors if not in the database.
PersistDetectors(detectors []Detector) error
// PersistFeatures inserts a set of features if not in the database.
PersistFeatures(features []Feature) error
// PersistNamespacedFeatures inserts a set of namespaced features if not in
// the database.
PersistNamespacedFeatures([]NamespacedFeature) error
// CacheAffectedNamespacedFeatures relates the namespaced features with the
// vulnerabilities affecting these features.
// A Layer is uniquely identified by its Name.
// The Name and EngineVersion fields are mandatory.
// If a Parent is specified, it is expected that it has been retrieved using
// FindLayer.
// If a Layer that already exists is inserted and the EngineVersion of the
// given Layer is higher than the stored one, the stored Layer should be
// updated.
// The function has to be idempotent, inserting a layer that already exists
// shouldn't return an error.
InsertLayer(Layer) error
// FindLayer retrieves a Layer from the database.
//
// NOTE(Sida): it's not necessary for every database implementation and so
// this function may have a better home.
CacheAffectedNamespacedFeatures([]NamespacedFeature) error
// FindAffectedNamespacedFeatures retrieves a set of namespaced features
// with affecting vulnerabilities.
FindAffectedNamespacedFeatures(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error)
// PersistNamespaces inserts a set of namespaces if not in the database.
PersistNamespaces([]Namespace) error
// PersistLayer appends a layer's content in the database.
// When `withFeatures` is true, the Features field should be filled.
// When `withVulnerabilities` is true, the Features field should be filled
// and their AffectedBy fields should contain every vulnerabilities that
// affect them.
FindLayer(name string, withFeatures, withVulnerabilities bool) (Layer, error)
// DeleteLayer deletes a Layer from the database and every layers that are
// based on it, recursively.
DeleteLayer(name string) error
// ListVulnerabilities returns the list of vulnerabilities of a particular
// Namespace.
//
// If any feature, namespace, or detector is not in the database, it returns not found error.
PersistLayer(hash string, features []LayerFeature, namespaces []LayerNamespace, detectedBy []Detector) error
// FindLayer returns a layer with all detected features and
// namespaces.
FindLayer(hash string) (layer Layer, found bool, err error)
// InsertVulnerabilities inserts a set of UNIQUE vulnerabilities with
// affected features into database, assuming that all vulnerabilities
// provided are NOT in database and all vulnerabilities' namespaces are
// already in the database.
InsertVulnerabilities([]VulnerabilityWithAffected) error
// FindVulnerability retrieves a set of Vulnerabilities with affected
// features.
FindVulnerabilities([]VulnerabilityID) ([]NullableVulnerability, error)
// DeleteVulnerability removes a set of Vulnerabilities assuming that the
// requested vulnerabilities are in the database.
DeleteVulnerabilities([]VulnerabilityID) error
// InsertVulnerabilityNotifications inserts a set of unique vulnerability
// notifications into datastore, assuming that they are not in the database.
InsertVulnerabilityNotifications([]VulnerabilityNotification) error
// FindNewNotification retrieves a notification, which has never been
// notified or notified before a certain time.
FindNewNotification(notifiedBefore time.Time) (hook NotificationHook, found bool, err error)
// The Limit and page parameters are used to paginate the return list.
// The first given page should be 0.
// The function should return the next available page. If there are no more
// pages, -1 has to be returned.
ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error)
// InsertVulnerabilities stores the given Vulnerabilities in the database,
// updating them if necessary.
//
// A vulnerability is uniquely identified by its Namespace and its Name.
// The FixedIn field may only contain a partial list of Features that are
// affected by the Vulnerability, along with the version in which the
// vulnerability is fixed. It is the responsibility of the implementation to
// update the list properly.
// A version equals to versionfmt.MinVersion means that the given Feature is
// not being affected by the Vulnerability at all and thus, should be removed
// from the list.
// It is important that Features should be unique in the FixedIn list. For
// example, it doesn't make sense to have two `openssl` Feature listed as a
// Vulnerability can only be fixed in one Version. This is true because
// Vulnerabilities and Features are namespaced (i.e. specific to one
// operating system).
// Each vulnerability insertion or update has to create a Notification that
// will contain the old and the updated Vulnerability, unless
// createNotification equals to true.
InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error
// FindVulnerability retrieves a Vulnerability from the database, including
// the FixedIn list.
FindVulnerability(namespaceName, name string) (Vulnerability, error)
// DeleteVulnerability removes a Vulnerability from the database.
//
// It has to create a Notification that will contain the old Vulnerability.
DeleteVulnerability(namespaceName, name string) error
// FindVulnerabilityNotification retrieves a vulnerability notification with
// affected ancestries affected by old or new vulnerability.
// InsertVulnerabilityFixes adds new FixedIn Feature or update the Versions
// of existing ones to the specified Vulnerability in the database.
//
// Because the number of affected ancestries maybe large, they are paginated
// and their pages are specified by the pagination token, which should be
// considered first page when it's empty.
FindVulnerabilityNotification(name string, limit int, oldVulnerabilityPage pagination.Token, newVulnerabilityPage pagination.Token) (noti VulnerabilityNotificationWithVulnerable, found bool, err error)
// It has has to create a Notification that will contain the old and the
// updated Vulnerability.
InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error
// MarkNotificationAsRead marks a Notification as notified now, assuming
// the requested notification is in the database.
MarkNotificationAsRead(name string) error
// DeleteVulnerabilityFix removes a FixedIn Feature from the specified
// Vulnerability in the database. It can be used to store the fact that a
// Vulnerability no longer affects the given Feature in any Version.
//
// It has has to create a Notification that will contain the old and the
// updated Vulnerability.
DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error
// DeleteNotification removes a Notification in the database.
// GetAvailableNotification returns the Name, Created, Notified and Deleted
// fields of a Notification that should be handled.
//
// The renotify interval defines how much time after being marked as Notified
// by SetNotificationNotified, a Notification that hasn't been deleted should
// be returned again by this function.
// A Notification for which there is a valid Lock with the same Name should
// not be returned.
GetAvailableNotification(renotifyInterval time.Duration) (VulnerabilityNotification, error)
// GetNotification returns a Notification, including its OldVulnerability and
// NewVulnerability fields.
//
// On these Vulnerabilities, LayersIntroducingVulnerability should be filled
// with every Layer that introduces the Vulnerability (i.e. adds at least one
// affected FeatureVersion).
// The Limit and page parameters are used to paginate
// LayersIntroducingVulnerability. The first given page should be
// VulnerabilityNotificationFirstPage. The function will then return the next
// available page. If there is no more page, NoVulnerabilityNotificationPage
// has to be returned.
GetNotification(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error)
// SetNotificationNotified marks a Notification as notified and thus, makes
// it unavailable for GetAvailableNotification, until the renotify duration
// is elapsed.
SetNotificationNotified(name string) error
// DeleteNotification marks a Notification as deleted, and thus, makes it
// unavailable for GetAvailableNotification.
DeleteNotification(name string) error
// UpdateKeyValue stores or updates a simple key/value pair.
UpdateKeyValue(key, value string) error
// InsertKeyValue stores or updates a simple key/value pair in the database.
InsertKeyValue(key, value string) error
// FindKeyValue retrieves a value from the given key.
FindKeyValue(key string) (value string, found bool, err error)
// AcquireLock acquires a brand new lock in the database with a given name
// for the given duration.
// GetKeyValue retrieves a value from the database from the given key.
//
// A lock can only have one owner.
// This method should NOT block until a lock is acquired.
AcquireLock(name, owner string, duration time.Duration) (acquired bool, expiration time.Time, err error)
// It returns an empty string if there is no such key.
GetKeyValue(key string) (string, error)
// ExtendLock extends an existing lock such that the lock will expire at the
// current time plus the provided duration.
// Lock creates or renew a Lock in the database with the given name, owner
// and duration.
//
// This method should return immediately with an error if the lock does not
// exist.
ExtendLock(name, owner string, duration time.Duration) (extended bool, expiration time.Time, err error)
// ReleaseLock releases an existing lock.
ReleaseLock(name, owner string) error
}
// Datastore represents a persistent data store
type Datastore interface {
// Begin starts a session to change.
Begin() (Session, error)
// After the specified duration, the Lock expires by itself if it hasn't been
// unlocked, and thus, let other users create a Lock with the same name.
// However, the owner can renew its Lock by setting renew to true.
// Lock should not block, it should instead returns whether the Lock has been
// successfully acquired/renewed. If it's the case, the expiration time of
// that Lock is returned as well.
Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
// Unlock releases an existing Lock.
Unlock(name, owner string)
// FindLock returns the owner of a Lock specified by the name, and its
// expiration time if it exists.
FindLock(name string) (string, time.Time, error)
// Ping returns the health status of the database.
Ping() bool

@ -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
}

@ -14,230 +14,163 @@
package database
import (
"time"
import "time"
"github.com/coreos/clair/pkg/pagination"
)
// MockSession implements Session and enables overriding each available method.
// MockDatastore implements Datastore and enables overriding each available method.
// The default behavior of each method is to simply panic.
type MockSession struct {
FctCommit func() error
FctRollback func() error
FctUpsertAncestry func(Ancestry) error
FctFindAncestry func(name string) (Ancestry, bool, error)
FctFindAffectedNamespacedFeatures func(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error)
FctPersistNamespaces func([]Namespace) error
FctPersistFeatures func([]Feature) error
FctPersistDetectors func(detectors []Detector) error
FctPersistNamespacedFeatures func([]NamespacedFeature) error
FctCacheAffectedNamespacedFeatures func([]NamespacedFeature) error
FctPersistLayer func(hash string, features []LayerFeature, namespaces []LayerNamespace, by []Detector) error
FctFindLayer func(name string) (Layer, bool, error)
FctInsertVulnerabilities func([]VulnerabilityWithAffected) error
FctFindVulnerabilities func([]VulnerabilityID) ([]NullableVulnerability, error)
FctDeleteVulnerabilities func([]VulnerabilityID) error
FctInsertVulnerabilityNotifications func([]VulnerabilityNotification) error
FctFindNewNotification func(lastNotified time.Time) (NotificationHook, bool, error)
FctFindVulnerabilityNotification func(name string, limit int, oldPage pagination.Token, newPage pagination.Token) (
vuln VulnerabilityNotificationWithVulnerable, ok bool, err error)
FctMarkNotificationAsRead func(name string) error
FctDeleteNotification func(name string) error
FctUpdateKeyValue func(key, value string) error
FctFindKeyValue func(key string) (string, bool, error)
FctAcquireLock func(name, owner string, duration time.Duration) (bool, time.Time, error)
FctExtendLock func(name, owner string, duration time.Duration) (bool, time.Time, error)
FctReleaseLock func(name, owner string) error
}
func (ms *MockSession) Commit() error {
if ms.FctCommit != nil {
return ms.FctCommit()
}
panic("required mock function not implemented")
}
func (ms *MockSession) Rollback() error {
if ms.FctRollback != nil {
return ms.FctRollback()
}
panic("required mock function not implemented")
}
func (ms *MockSession) UpsertAncestry(ancestry Ancestry) error {
if ms.FctUpsertAncestry != nil {
return ms.FctUpsertAncestry(ancestry)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindAncestry(name string) (Ancestry, bool, error) {
if ms.FctFindAncestry != nil {
return ms.FctFindAncestry(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindAffectedNamespacedFeatures(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error) {
if ms.FctFindAffectedNamespacedFeatures != nil {
return ms.FctFindAffectedNamespacedFeatures(features)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistDetectors(detectors []Detector) error {
if ms.FctPersistDetectors != nil {
return ms.FctPersistDetectors(detectors)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistNamespaces(namespaces []Namespace) error {
if ms.FctPersistNamespaces != nil {
return ms.FctPersistNamespaces(namespaces)
}
panic("required mock function not implemented")
type MockDatastore struct {
FctListNamespaces func() ([]Namespace, error)
FctInsertLayer func(Layer) error
FctFindLayer func(name string, withFeatures, withVulnerabilities bool) (Layer, error)
FctDeleteLayer func(name string) error
FctListVulnerabilities func(namespaceName string, limit int, page int) ([]Vulnerability, int, error)
FctInsertVulnerabilities func(vulnerabilities []Vulnerability, createNotification bool) error
FctFindVulnerability func(namespaceName, name string) (Vulnerability, error)
FctDeleteVulnerability func(namespaceName, name string) error
FctInsertVulnerabilityFixes func(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error
FctDeleteVulnerabilityFix func(vulnerabilityNamespace, vulnerabilityName, featureName string) error
FctGetAvailableNotification func(renotifyInterval time.Duration) (VulnerabilityNotification, error)
FctGetNotification func(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error)
FctSetNotificationNotified func(name string) error
FctDeleteNotification func(name string) error
FctInsertKeyValue func(key, value string) error
FctGetKeyValue func(key string) (string, error)
FctLock func(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
FctUnlock func(name, owner string)
FctFindLock func(name string) (string, time.Time, error)
FctPing func() bool
FctClose func()
}
func (ms *MockSession) PersistFeatures(features []Feature) error {
if ms.FctPersistFeatures != nil {
return ms.FctPersistFeatures(features)
func (mds *MockDatastore) ListNamespaces() ([]Namespace, error) {
if mds.FctListNamespaces != nil {
return mds.FctListNamespaces()
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistNamespacedFeatures(namespacedFeatures []NamespacedFeature) error {
if ms.FctPersistNamespacedFeatures != nil {
return ms.FctPersistNamespacedFeatures(namespacedFeatures)
func (mds *MockDatastore) InsertLayer(layer Layer) error {
if mds.FctInsertLayer != nil {
return mds.FctInsertLayer(layer)
}
panic("required mock function not implemented")
}
func (ms *MockSession) CacheAffectedNamespacedFeatures(namespacedFeatures []NamespacedFeature) error {
if ms.FctCacheAffectedNamespacedFeatures != nil {
return ms.FctCacheAffectedNamespacedFeatures(namespacedFeatures)
func (mds *MockDatastore) FindLayer(name string, withFeatures, withVulnerabilities bool) (Layer, error) {
if mds.FctFindLayer != nil {
return mds.FctFindLayer(name, withFeatures, withVulnerabilities)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistLayer(hash string, features []LayerFeature, namespaces []LayerNamespace, detectors []Detector) error {
if ms.FctPersistLayer != nil {
return ms.FctPersistLayer(hash, features, namespaces, detectors)
func (mds *MockDatastore) DeleteLayer(name string) error {
if mds.FctDeleteLayer != nil {
return mds.FctDeleteLayer(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindLayer(name string) (Layer, bool, error) {
if ms.FctFindLayer != nil {
return ms.FctFindLayer(name)
func (mds *MockDatastore) ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error) {
if mds.FctListVulnerabilities != nil {
return mds.FctListVulnerabilities(namespaceName, limit, page)
}
panic("required mock function not implemented")
}
func (ms *MockSession) InsertVulnerabilities(vulnerabilities []VulnerabilityWithAffected) error {
if ms.FctInsertVulnerabilities != nil {
return ms.FctInsertVulnerabilities(vulnerabilities)
func (mds *MockDatastore) InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error {
if mds.FctInsertVulnerabilities != nil {
return mds.FctInsertVulnerabilities(vulnerabilities, createNotification)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindVulnerabilities(vulnerabilityIDs []VulnerabilityID) ([]NullableVulnerability, error) {
if ms.FctFindVulnerabilities != nil {
return ms.FctFindVulnerabilities(vulnerabilityIDs)
func (mds *MockDatastore) FindVulnerability(namespaceName, name string) (Vulnerability, error) {
if mds.FctFindVulnerability != nil {
return mds.FctFindVulnerability(namespaceName, name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) DeleteVulnerabilities(VulnerabilityIDs []VulnerabilityID) error {
if ms.FctDeleteVulnerabilities != nil {
return ms.FctDeleteVulnerabilities(VulnerabilityIDs)
func (mds *MockDatastore) DeleteVulnerability(namespaceName, name string) error {
if mds.FctDeleteVulnerability != nil {
return mds.FctDeleteVulnerability(namespaceName, name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) InsertVulnerabilityNotifications(vulnerabilityNotifications []VulnerabilityNotification) error {
if ms.FctInsertVulnerabilityNotifications != nil {
return ms.FctInsertVulnerabilityNotifications(vulnerabilityNotifications)
func (mds *MockDatastore) InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error {
if mds.FctInsertVulnerabilityFixes != nil {
return mds.FctInsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName, fixes)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindNewNotification(lastNotified time.Time) (NotificationHook, bool, error) {
if ms.FctFindNewNotification != nil {
return ms.FctFindNewNotification(lastNotified)
func (mds *MockDatastore) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error {
if mds.FctDeleteVulnerabilityFix != nil {
return mds.FctDeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindVulnerabilityNotification(name string, limit int, oldPage pagination.Token, newPage pagination.Token) (
VulnerabilityNotificationWithVulnerable, bool, error) {
if ms.FctFindVulnerabilityNotification != nil {
return ms.FctFindVulnerabilityNotification(name, limit, oldPage, newPage)
func (mds *MockDatastore) GetAvailableNotification(renotifyInterval time.Duration) (VulnerabilityNotification, error) {
if mds.FctGetAvailableNotification != nil {
return mds.FctGetAvailableNotification(renotifyInterval)
}
panic("required mock function not implemented")
}
func (ms *MockSession) MarkNotificationAsRead(name string) error {
if ms.FctMarkNotificationAsRead != nil {
return ms.FctMarkNotificationAsRead(name)
func (mds *MockDatastore) GetNotification(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error) {
if mds.FctGetNotification != nil {
return mds.FctGetNotification(name, limit, page)
}
panic("required mock function not implemented")
}
func (ms *MockSession) DeleteNotification(name string) error {
if ms.FctDeleteNotification != nil {
return ms.FctDeleteNotification(name)
func (mds *MockDatastore) SetNotificationNotified(name string) error {
if mds.FctSetNotificationNotified != nil {
return mds.FctSetNotificationNotified(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) UpdateKeyValue(key, value string) error {
if ms.FctUpdateKeyValue != nil {
return ms.FctUpdateKeyValue(key, value)
func (mds *MockDatastore) DeleteNotification(name string) error {
if mds.FctDeleteNotification != nil {
return mds.FctDeleteNotification(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindKeyValue(key string) (string, bool, error) {
if ms.FctFindKeyValue != nil {
return ms.FctFindKeyValue(key)
func (mds *MockDatastore) InsertKeyValue(key, value string) error {
if mds.FctInsertKeyValue != nil {
return mds.FctInsertKeyValue(key, value)
}
panic("required mock function not implemented")
}
func (ms *MockSession) AcquireLock(name, owner string, duration time.Duration) (bool, time.Time, error) {
if ms.FctAcquireLock != nil {
return ms.FctAcquireLock(name, owner, duration)
func (mds *MockDatastore) GetKeyValue(key string) (string, error) {
if mds.FctGetKeyValue != nil {
return mds.FctGetKeyValue(key)
}
panic("required mock function not implemented")
}
func (ms *MockSession) ExtendLock(name, owner string, duration time.Duration) (bool, time.Time, error) {
if ms.FctExtendLock != nil {
return ms.FctExtendLock(name, owner, duration)
func (mds *MockDatastore) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time) {
if mds.FctLock != nil {
return mds.FctLock(name, owner, duration, renew)
}
panic("required mock function not implemented")
}
func (ms *MockSession) ReleaseLock(name, owner string) error {
if ms.FctReleaseLock != nil {
return ms.FctReleaseLock(name, owner)
func (mds *MockDatastore) Unlock(name, owner string) {
if mds.FctUnlock != nil {
mds.FctUnlock(name, owner)
return
}
panic("required mock function not implemented")
}
// MockDatastore implements Datastore and enables overriding each available method.
// The default behavior of each method is to simply panic.
type MockDatastore struct {
FctBegin func() (Session, error)
FctPing func() bool
FctClose func()
}
func (mds *MockDatastore) Begin() (Session, error) {
if mds.FctBegin != nil {
return mds.FctBegin()
func (mds *MockDatastore) FindLock(name string) (string, time.Time, error) {
if mds.FctFindLock != nil {
return mds.FctFindLock(name)
}
panic("required mock function not implemented")
}

@ -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
}

@ -46,6 +46,4 @@ var UbuntuReleasesMapping = map[string]string{
"zesty": "17.04",
"artful": "17.10",
"bionic": "18.04",
"cosmic": "18.10",
"disco": "19.04",
}

@ -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
}

@ -12,40 +12,41 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package keyvalue
package pgsql
import (
"testing"
"github.com/coreos/clair/database/pgsql/testutil"
"github.com/stretchr/testify/assert"
)
func TestKeyValue(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "KeyValue")
defer cleanup()
datastore, err := openDatabaseForTest("KeyValue", false)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
// Get non-existing key/value
f, ok, err := FindKeyValue(tx, "test")
f, err := datastore.GetKeyValue("test")
assert.Nil(t, err)
assert.False(t, ok)
assert.Empty(t, "", f)
// Try to insert invalid key/value.
assert.Error(t, UpdateKeyValue(tx, "test", ""))
assert.Error(t, UpdateKeyValue(tx, "", "test"))
assert.Error(t, UpdateKeyValue(tx, "", ""))
assert.Error(t, datastore.InsertKeyValue("test", ""))
assert.Error(t, datastore.InsertKeyValue("", "test"))
assert.Error(t, datastore.InsertKeyValue("", ""))
// Insert and verify.
assert.Nil(t, UpdateKeyValue(tx, "test", "test1"))
f, ok, err = FindKeyValue(tx, "test")
assert.Nil(t, datastore.InsertKeyValue("test", "test1"))
f, err = datastore.GetKeyValue("test")
assert.Nil(t, err)
assert.True(t, ok)
assert.Equal(t, "test1", f)
// Update and verify.
assert.Nil(t, UpdateKeyValue(tx, "test", "test2"))
f, ok, err = FindKeyValue(tx, "test")
assert.Nil(t, datastore.InsertKeyValue("test", "test2"))
f, err = datastore.GetKeyValue("test")
assert.Nil(t, err)
assert.True(t, ok)
assert.Equal(t, "test2", f)
}

@ -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…
Cancel
Save