Initial commit
This commit is contained in:
commit
3ec262dd51
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
.*
|
||||
*.md
|
||||
DCO
|
||||
LICENSE
|
||||
NOTICE
|
||||
docs
|
||||
cloudconfig
|
71
CONTRIBUTING.md
Executable file
71
CONTRIBUTING.md
Executable file
@ -0,0 +1,71 @@
|
||||
# How to Contribute
|
||||
|
||||
CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via
|
||||
GitHub pull requests. This document outlines some of the conventions on
|
||||
development workflow, commit message formatting, contact points and other
|
||||
resources to make it easier to get your contribution accepted.
|
||||
|
||||
# Certificate of Origin
|
||||
|
||||
By contributing to this project you agree to the Developer Certificate of
|
||||
Origin (DCO). This document was created by the Linux Kernel community and is a
|
||||
simple statement that you, as a contributor, have the legal right to make the
|
||||
contribution. See the [DCO](DCO) file for details.
|
||||
|
||||
# Email and Chat
|
||||
|
||||
The project currently uses the general CoreOS email list and IRC channel:
|
||||
- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev)
|
||||
- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org
|
||||
|
||||
Please avoid emailing maintainers found in the MAINTAINERS file directly. They
|
||||
are very busy and read the mailing lists.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Fork the repository on GitHub
|
||||
- Read the [README](README.md) for build and test instructions
|
||||
- Play with the project, submit bugs, submit patches!
|
||||
|
||||
## Contribution Flow
|
||||
|
||||
This is a rough outline of what a contributor's workflow looks like:
|
||||
|
||||
- Create a topic branch from where you want to base your work (usually master).
|
||||
- Make commits of logical units.
|
||||
- Make sure your commit messages are in the proper format (see below).
|
||||
- Push your changes to a topic branch in your fork of the repository.
|
||||
- Make sure the tests pass, and add any new tests as appropriate.
|
||||
- Submit a pull request to the original repository.
|
||||
|
||||
Thanks for your contributions!
|
||||
|
||||
### Format of the Commit Message
|
||||
|
||||
We follow a rough convention for commit messages that is designed to answer two
|
||||
questions: what changed and why. The subject line should feature the what and
|
||||
the body of the commit should describe the why.
|
||||
|
||||
```
|
||||
scripts: add the test-cluster command
|
||||
|
||||
this uses tmux to setup a test cluster that you can easily kill and
|
||||
start for debugging.
|
||||
|
||||
Fixes #38
|
||||
```
|
||||
|
||||
The format can be described more formally as follows:
|
||||
|
||||
```
|
||||
<subsystem>: <what changed>
|
||||
<BLANK LINE>
|
||||
<why this change was made>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The first line is the subject and should be no longer than 70 characters, the
|
||||
second line is always blank, and other lines should be wrapped at 80 characters.
|
||||
This allows the message to be easier to read on GitHub as well as in various
|
||||
git tools.
|
36
DCO
Executable file
36
DCO
Executable file
@ -0,0 +1,36 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM golang:1.5
|
||||
MAINTAINER Quentin Machu <quentin.machu@coreos.com>
|
||||
|
||||
RUN apt-get update && apt-get install -y bzr rpm && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN mkdir /db
|
||||
VOLUME /db
|
||||
|
||||
EXPOSE 6060 6061
|
||||
|
||||
ADD . /go/src/github.com/coreos/clair/
|
||||
WORKDIR /go/src/github.com/coreos/clair/
|
||||
|
||||
ENV GO15VENDOREXPERIMENT 1
|
||||
RUN go install -v
|
||||
RUN go test $(go list ./... | grep -v /vendor/) # https://github.com/golang/go/issues/11659
|
||||
|
||||
ENTRYPOINT ["clair"]
|
94
Godeps/Godeps.json
generated
Normal file
94
Godeps/Godeps.json
generated
Normal file
@ -0,0 +1,94 @@
|
||||
{
|
||||
"ImportPath": "github.com/coreos/clair",
|
||||
"GoVersion": "go1.5.1",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/template",
|
||||
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/units",
|
||||
"Rev": "6b4e7dc5e3143b85ea77909c72caf89416fc2915"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/barakmich/glog",
|
||||
"Rev": "fafcb6128a8a2e6360ff034091434d547397d54a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/boltdb/bolt",
|
||||
"Comment": "v1.0-98-gafceb31",
|
||||
"Rev": "afceb316b96ea97cbac6d23afbdf69543d80748a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-systemd/journal",
|
||||
"Comment": "v3-15-gcfa48f3",
|
||||
"Rev": "cfa48f34d8dc4ff58f9b48725181a09f9092dc3c"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/capnslog",
|
||||
"Rev": "42a8c3b1a6f917bb8346ef738f32712a7ca0ede7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/timeutil",
|
||||
"Rev": "42a8c3b1a6f917bb8346ef738f32712a7ca0ede7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gogo/protobuf/proto",
|
||||
"Rev": "58bbd41c1a2d1b7154f5d99a8d0d839b3093301a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/cayley",
|
||||
"Comment": "v0.4.1-160-gcdf0154",
|
||||
"Rev": "cdf0154d1a34019651eb4f46ce666b31f4d8cae7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/julienschmidt/httprouter",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "8c199fb6259ffc1af525cc3ad52ee60ba8359669"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/lib/pq",
|
||||
"Comment": "go1.0-cutoff-56-gdc50b6a",
|
||||
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/pborman/uuid",
|
||||
"Rev": "ca53cad383cad2479bbba7f7a1a05797ec1386e4"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/stretchr/testify/assert",
|
||||
"Comment": "v1.0-17-g089c718",
|
||||
"Rev": "089c7181b8c728499929ff09b62d3fdd8df8adff"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
"Rev": "315fcfb05d4d46d4354b313d146ef688dda272a9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/gosnappy/snappy",
|
||||
"Rev": "156a073208e131d7d2e212cb749feae7c339e846"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/tylerb/graceful",
|
||||
"Comment": "v1.2.3",
|
||||
"Rev": "48afeb21e2fcbcff0f30bd5ad6b97747b0fae38e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/netutil",
|
||||
"Rev": "7654728e381988afd88e58cabfd6363a5ea91810"
|
||||
},
|
||||
{
|
||||
"ImportPath": "gopkg.in/alecthomas/kingpin.v2",
|
||||
"Comment": "v2.0.10",
|
||||
"Rev": "e1f37920c1d0ced4d1c92f9526a2a433183f02e9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "gopkg.in/mgo.v2",
|
||||
"Comment": "r2015.05.29",
|
||||
"Rev": "01ee097136da162d1dd3c9b44fbdf3abf4fd6552"
|
||||
}
|
||||
]
|
||||
}
|
5
Godeps/Readme
generated
Normal file
5
Godeps/Readme
generated
Normal file
@ -0,0 +1,5 @@
|
||||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
202
LICENSE
Executable file
202
LICENSE
Executable file
@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
5
NOTICE
Executable file
5
NOTICE
Executable file
@ -0,0 +1,5 @@
|
||||
CoreOS Project
|
||||
Copyright 2015 CoreOS, Inc
|
||||
|
||||
This product includes software developed at CoreOS, Inc.
|
||||
(http://www.coreos.com/).
|
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
||||
Clair
|
||||
=====
|
||||
|
||||
Clair is a container vulnerability analysis service. It provides the list of vulnerabilities that threaten each container and can sends notifications whenever new vulnerabilities that affect existing containers are released.
|
||||
|
||||
We named the project « Clair », which means in French *clear*, *bright*, *transparent* because we believe that it enables users to have a clear insight into the security of their container infrastructure.
|
||||
|
||||
## Why should I use Clair?
|
||||
|
||||
Clair is a single-binary server that exposes an JSON, HTTP API. It does not require any agent to sit on your containers neither does it need any specific container tweak to be done. It has been designed to perform massive analysis on the [Quay.io Container Registry](https://quay.io).
|
||||
|
||||
Whether you host a container registry, a continuous-integration system, or build dozens to thousands containers, you would benefit from Clair. More generally, if you consider that container security matters (and, honestly, you should), you should give it a shot.
|
||||
|
||||
## How Clair Detects Vulnerabilities
|
||||
|
||||
Clair has been designed to analyze a container layer only once, without running the container. The analysis has to extract all required data to detect the known vulnerabilities which may affect a layer but also any future vulnerabilities.
|
||||
|
||||
Detecting vulnerabilities can be achieved by several techniques. One possiblity is to compute hashes of binaries. These are presented on a layer and then compared with a database. However, building this database may become tricky considering the number of different packages and library versions.
|
||||
|
||||
To detect vulnerabilities Clair decided to take advantage of package managers, which quickly and comprehensively provide lists of installed binary and source packages. Package lists are extracted for each layer that composes of your container image, the difference between the layer’s package list, and its parent one is stored. Not only is this method storage-efficient, but it also enables us to scan a layer that may be used in many images only once. Coupled with vulnerability databases such as the Debian’s Security Bug Tracker, Clair is able to tell which vulnerabilities threaten a container, and which layer and package introduced them.
|
||||
|
||||
### Graph
|
||||
|
||||
Clair internally uses a graph, which has its model described in the [associated doc](docs/Model.md) to store and query data. Below is a non-exhaustive example graph that correspond to the following *Dockerfile*.
|
||||
|
||||
```
|
||||
1. MAINTAINER Quentin Machu <quentin.machu@coreos.com>
|
||||
2. FROM ubuntu:trusty
|
||||
3. RUN apt−get update && apt−get upgrade −y
|
||||
4. EXPOSE 22
|
||||
5. CMD ["/usr/sbin/sshd", "-D"]
|
||||
```
|
||||
|
||||
![Example graph](docs/Model.png)
|
||||
|
||||
The above image shows five layers represented by the purple nodes, associated with their ids and parents. Because the second layer imports *Ubuntu Trusty* in the container, Clair can detect the operating system and some packages, in green (we only show one here for the sake of simplicity). The third layer upgrades packages, so the graph reflects that this layer removes the previous version and installs the new one. Finally, the graph knows about a vulnerability, drawn in red, which is fixed by a particular package. Note that two synthetic package versions exist (0 and ∞): they ensure database consistency during parallel modification. ∞ also allows us to define very easily that a vulnerability is not yet fixed; thus, it affects every package version.
|
||||
|
||||
Querying this particular graph will tell us that our image is not vulnerable at all because none of the successor versions of its only package fix any vulnerability. However, an image based on the second layer could be vulnerable.
|
||||
|
||||
### Architecture
|
||||
|
||||
Clair is divided into X main modules (which represent Go packages):
|
||||
|
||||
- **api** defines how users interact with Clair and exposes a [documented HTTP API](docs/API.md).
|
||||
- **worker** extracts useful informations from layers and store everything in the database.
|
||||
- **updater** periodically updates Clair's vulnerability database from known vulnerability sources.
|
||||
- **notifier** dispatches [notifications](docs/Notifications.md) about vulnerable containers when vulnerabilities are released or updated.
|
||||
- **database** persists layers informations and vulnerabilities in [Cayley graph database](https://github.com/google/cayley).
|
||||
- **health** summarizes health checks of every Clair's services.
|
||||
|
||||
Multiple backend databases are supported, a testing deployment would use an in-memory storage while a production deployment should use [Bolt](https://github.com/boltdb/bolt) (single-instance deployment) or PostgreSQL (distributed deployment, probably behind a load-balancer). To learn more about how to run Clair, take a look at the [doc](docs/Run.md).
|
||||
|
||||
#### Detectors & Fetchers
|
||||
|
||||
Clair currently supports three operating systems and their package managers, which we believe are the most common ones: *Debian* (dpkg), *Ubuntu* (dpkg), *CentOS* (yum).
|
||||
|
||||
Supporting an operating system implies that we are able to extract the operating system's name and version from a layer and the list of package it has. This is done inside the *worker/detectors* package and extending that is straightforward.
|
||||
|
||||
All of this is useless if no vulnerability is known for any of these packages. The *updater/fetchers* package defines trusted sources of vulnerabilities, how to fetch them and parse them. For now, Clair uses three databases, one for each supported operating system:
|
||||
- [Ubuntu CVE Tracker](https://launchpad.net/ubuntu-cve-tracker)
|
||||
- [Debian Security Bug Tracker](https://security-tracker.debian.org/tracker/)
|
||||
- [Red Hat Security Data](https://www.redhat.com/security/data/metrics/)
|
||||
|
||||
Using these distro-specific sources gives us confidence that Clair can take into consideration *all* the different package implementations and backports without ever reporting anything possibly inaccurate.
|
||||
|
||||
# Coming Soon
|
||||
|
||||
- Improved performances.
|
||||
- Extended detection system
|
||||
- More package managers
|
||||
- Generic features such as detecting presence/absence of files
|
||||
- ...
|
||||
- Expose more informations about vulnerability
|
||||
- Access vector
|
||||
- Acess complexity
|
||||
- ...
|
||||
|
||||
# Related links
|
||||
|
||||
- Talk @ ContainerDays NYC 2015 [[Slides]](https://docs.google.com/presentation/d/1toUKgqLyy1b-pZlDgxONLduiLmt2yaLR0GliBB7b3L0/pub?start=false&loop=false&slide=id.p) [[Video]](https://www.youtube.com/watch?v=PA3oBAgjnkU)
|
||||
- [Quay](https://quay.io): First container registry using Clair.
|
126
api/api.go
Normal file
126
api/api.go
Normal file
@ -0,0 +1,126 @@
|
||||
// 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 api provides a RESTful HTTP API, enabling external apps to interact
|
||||
// with clair.
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/coreos/clair/utils"
|
||||
"github.com/tylerb/graceful"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "api")
|
||||
|
||||
// Config represents the configuration for the Main API.
|
||||
type Config struct {
|
||||
Port int
|
||||
TimeOut time.Duration
|
||||
CertFile, KeyFile, CAFile string
|
||||
}
|
||||
|
||||
// RunMain launches the main API, which exposes every possible interactions
|
||||
// with clair.
|
||||
func RunMain(conf *Config, st *utils.Stopper) {
|
||||
log.Infof("starting API on port %d.", conf.Port)
|
||||
defer func() {
|
||||
log.Info("API stopped")
|
||||
st.End()
|
||||
}()
|
||||
|
||||
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(conf.Port),
|
||||
TLSConfig: setupClientCert(conf.CAFile),
|
||||
Handler: NewVersionRouter(conf.TimeOut),
|
||||
},
|
||||
}
|
||||
listenAndServeWithStopper(srv, st, conf.CertFile, conf.KeyFile)
|
||||
}
|
||||
|
||||
// RunHealth launches the Health API, which only exposes a method to fetch
|
||||
// clair's health without any security or authentification mechanism.
|
||||
func RunHealth(port int, st *utils.Stopper) {
|
||||
log.Infof("starting Health API on port %d.", port)
|
||||
defer func() {
|
||||
log.Info("Health API stopped")
|
||||
st.End()
|
||||
}()
|
||||
|
||||
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(port),
|
||||
Handler: NewHealthRouter(),
|
||||
},
|
||||
}
|
||||
listenAndServeWithStopper(srv, st, "", "")
|
||||
}
|
||||
|
||||
// listenAndServeWithStopper wraps graceful.Server's
|
||||
// ListenAndServe/ListenAndServeTLS and adds the ability to interrupt them with
|
||||
// the provided utils.Stopper
|
||||
func listenAndServeWithStopper(srv *graceful.Server, st *utils.Stopper, certFile, keyFile string) {
|
||||
go func() {
|
||||
<-st.Chan()
|
||||
srv.Stop(0)
|
||||
}()
|
||||
|
||||
var err error
|
||||
if certFile != "" && keyFile != "" {
|
||||
log.Info("API: TLS Enabled")
|
||||
err = srv.ListenAndServeTLS(certFile, keyFile)
|
||||
} else {
|
||||
err = srv.ListenAndServe()
|
||||
}
|
||||
|
||||
if opErr, ok := err.(*net.OpError); !ok || (ok && opErr.Op != "accept") {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupClientCert creates a tls.Config instance using a CA file path
|
||||
// (if provided) and and calls log.Fatal if it does not exist.
|
||||
func setupClientCert(caFile string) *tls.Config {
|
||||
if len(caFile) > 0 {
|
||||
log.Info("API: Client Certificate Authentification Enabled")
|
||||
caCert, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
return &tls.Config{
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
ClientAuth: tls.NoClientCert,
|
||||
}
|
||||
}
|
78
api/jsonhttp/json.go
Normal file
78
api/jsonhttp/json.go
Normal file
@ -0,0 +1,78 @@
|
||||
// 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 jsonhttp provides helper functions to write JSON responses to
|
||||
// http.ResponseWriter and read JSON bodies from http.Request.
|
||||
package jsonhttp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
cerrors "github.com/coreos/clair/utils/errors"
|
||||
"github.com/coreos/clair/worker"
|
||||
)
|
||||
|
||||
// MaxPostSize is the maximum number of bytes that ParseBody reads from an
|
||||
// http.Request.Body.
|
||||
var MaxPostSize int64 = 1048576
|
||||
|
||||
// Render writes a JSON-encoded object to a http.ResponseWriter, as well as
|
||||
// a HTTP status code.
|
||||
func Render(w http.ResponseWriter, httpStatus int, v interface{}) {
|
||||
w.WriteHeader(httpStatus)
|
||||
if v != nil {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
result, _ := json.Marshal(v)
|
||||
w.Write(result)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderError writes an error, wrapped in the Message field of a JSON-encoded
|
||||
// object to a http.ResponseWriter, as well as a HTTP status code.
|
||||
// If the status code is 0, RenderError tries to guess the proper HTTP status
|
||||
// code from the error type.
|
||||
func RenderError(w http.ResponseWriter, httpStatus int, err error) {
|
||||
if httpStatus == 0 {
|
||||
httpStatus = http.StatusInternalServerError
|
||||
// Try to guess the http status code from the error type
|
||||
if _, isBadRequestError := err.(*cerrors.ErrBadRequest); isBadRequestError {
|
||||
httpStatus = http.StatusBadRequest
|
||||
} else {
|
||||
switch err {
|
||||
case cerrors.ErrNotFound:
|
||||
httpStatus = http.StatusNotFound
|
||||
case database.ErrTransaction, database.ErrBackendException:
|
||||
httpStatus = http.StatusServiceUnavailable
|
||||
case worker.ErrParentUnknown, worker.ErrUnsupported:
|
||||
httpStatus = http.StatusBadRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Render(w, httpStatus, struct{ Message string }{Message: err.Error()})
|
||||
}
|
||||
|
||||
// ParseBody reads a JSON-encoded body from a http.Request and unmarshals it
|
||||
// into the provided object.
|
||||
func ParseBody(r *http.Request, v interface{}) (int, error) {
|
||||
defer r.Body.Close()
|
||||
err := json.NewDecoder(io.LimitReader(r.Body, MaxPostSize)).Decode(v)
|
||||
if err != nil {
|
||||
return http.StatusUnsupportedMediaType, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
54
api/logic/general.go
Normal file
54
api/logic/general.go
Normal file
@ -0,0 +1,54 @@
|
||||
// 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 logic implements all the available API methods.
|
||||
// Every methods are documented in docs/API.md.
|
||||
package logic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/clair/api/jsonhttp"
|
||||
"github.com/coreos/clair/health"
|
||||
"github.com/coreos/clair/worker"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// Version is an integer representing the API version.
|
||||
const Version = 1
|
||||
|
||||
// GETVersions returns API and Engine versions.
|
||||
func GETVersions(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
jsonhttp.Render(w, http.StatusOK, struct {
|
||||
APIVersion string
|
||||
EngineVersion string
|
||||
}{
|
||||
APIVersion: strconv.Itoa(Version),
|
||||
EngineVersion: strconv.Itoa(worker.Version),
|
||||
})
|
||||
}
|
||||
|
||||
// GETHealth sums up the health of all the registered services.
|
||||
func GETHealth(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
globalHealth, statuses := health.Healthcheck()
|
||||
|
||||
httpStatus := http.StatusOK
|
||||
if !globalHealth {
|
||||
httpStatus = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, httpStatus, statuses)
|
||||
return
|
||||
}
|
365
api/logic/layers.go
Normal file
365
api/logic/layers.go
Normal file
@ -0,0 +1,365 @@
|
||||
// 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 logic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/clair/api/jsonhttp"
|
||||
"github.com/coreos/clair/database"
|
||||
cerrors "github.com/coreos/clair/utils/errors"
|
||||
"github.com/coreos/clair/utils/types"
|
||||
"github.com/coreos/clair/worker"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// POSTLayersParameters represents the expected parameters for POSTLayers.
|
||||
type POSTLayersParameters struct {
|
||||
ID, Path, ParentID string
|
||||
}
|
||||
|
||||
// POSTLayers analyzes a layer and returns the engine version that has been used
|
||||
// for the analysis.
|
||||
func POSTLayers(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var parameters POSTLayersParameters
|
||||
if s, err := jsonhttp.ParseBody(r, ¶meters); err != nil {
|
||||
jsonhttp.RenderError(w, s, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Process data.
|
||||
if err := worker.Process(parameters.ID, parameters.ParentID, parameters.Path); err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get engine version and return.
|
||||
jsonhttp.Render(w, http.StatusCreated, struct{ Version string }{Version: strconv.Itoa(worker.Version)})
|
||||
}
|
||||
|
||||
// GETLayersOS returns the operating system of a layer if it exists.
|
||||
// It uses not only the specified layer but also its parent layers if necessary.
|
||||
// An empty OS string is returned if no OS has been detected.
|
||||
func GETLayersOS(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Find layer.
|
||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent, database.FieldLayerOS})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get OS.
|
||||
os, err := layer.OperatingSystem()
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ OS string }{OS: os})
|
||||
}
|
||||
|
||||
// GETLayersParent returns the parent ID of a layer if it exists.
|
||||
// An empty ID string is returned if the layer has no parent.
|
||||
func GETLayersParent(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Find layer
|
||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get layer's parent.
|
||||
parent, err := layer.Parent([]string{database.FieldLayerID})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
ID := ""
|
||||
if parent != nil {
|
||||
ID = parent.ID
|
||||
}
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ ID string }{ID: ID})
|
||||
}
|
||||
|
||||
// GETLayersPackages returns the complete list of packages that a layer has
|
||||
// if it exists.
|
||||
func GETLayersPackages(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Find layer
|
||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent, database.FieldLayerPackages})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer's packages.
|
||||
packagesNodes, err := layer.AllPackages()
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
packages := []*database.Package{}
|
||||
if len(packagesNodes) > 0 {
|
||||
packages, err = database.FindAllPackagesByNodes(packagesNodes, []string{database.FieldPackageOS, database.FieldPackageName, database.FieldPackageVersion})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ Packages []*database.Package }{Packages: packages})
|
||||
}
|
||||
|
||||
// GETLayersPackagesDiff returns the list of packages that a layer installs and
|
||||
// removes if it exists.
|
||||
func GETLayersPackagesDiff(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Find layer.
|
||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerPackages})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer's packages.
|
||||
installedPackages, removedPackages := make([]*database.Package, 0), make([]*database.Package, 0)
|
||||
if len(layer.InstalledPackagesNodes) > 0 {
|
||||
installedPackages, err = database.FindAllPackagesByNodes(layer.InstalledPackagesNodes, []string{database.FieldPackageOS, database.FieldPackageName, database.FieldPackageVersion})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(layer.RemovedPackagesNodes) > 0 {
|
||||
removedPackages, err = database.FindAllPackagesByNodes(layer.RemovedPackagesNodes, []string{database.FieldPackageOS, database.FieldPackageName, database.FieldPackageVersion})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ InstalledPackages, RemovedPackages []*database.Package }{InstalledPackages: installedPackages, RemovedPackages: removedPackages})
|
||||
}
|
||||
|
||||
// GETLayersVulnerabilities returns the complete list of vulnerabilities that
|
||||
// a layer has if it exists.
|
||||
func GETLayersVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Get minumum priority parameter.
|
||||
minimumPriority := types.Priority(r.URL.Query().Get("minimumPriority"))
|
||||
if minimumPriority == "" {
|
||||
minimumPriority = "High" // Set default priority to High
|
||||
} else if !minimumPriority.IsValid() {
|
||||
jsonhttp.RenderError(w, 0, cerrors.NewBadRequestError("invalid priority"))
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer
|
||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent, database.FieldLayerPackages})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer's packages.
|
||||
packagesNodes, err := layer.AllPackages()
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find vulnerabilities.
|
||||
vulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(packagesNodes, minimumPriority, []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ Vulnerabilities []*database.Vulnerability }{Vulnerabilities: vulnerabilities})
|
||||
}
|
||||
|
||||
// GETLayersVulnerabilitiesDiff returns the list of vulnerabilities that a layer
|
||||
// adds and removes if it exists.
|
||||
func GETLayersVulnerabilitiesDiff(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Get minumum priority parameter.
|
||||
minimumPriority := types.Priority(r.URL.Query().Get("minimumPriority"))
|
||||
if minimumPriority == "" {
|
||||
minimumPriority = "High" // Set default priority to High
|
||||
} else if !minimumPriority.IsValid() {
|
||||
jsonhttp.RenderError(w, 0, cerrors.NewBadRequestError("invalid priority"))
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer.
|
||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerPackages})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Selected fields for vulnerabilities.
|
||||
selectedFields := []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription}
|
||||
|
||||
// Find vulnerabilities for installed packages.
|
||||
addedVulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(layer.InstalledPackagesNodes, minimumPriority, selectedFields)
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find vulnerabilities for removed packages.
|
||||
removedVulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(layer.RemovedPackagesNodes, minimumPriority, selectedFields)
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove vulnerabilities which appears both in added and removed lists (eg. case of updated packages but still vulnerable).
|
||||
for ia, a := range addedVulnerabilities {
|
||||
for ir, r := range removedVulnerabilities {
|
||||
if a.ID == r.ID {
|
||||
addedVulnerabilities = append(addedVulnerabilities[:ia], addedVulnerabilities[ia+1:]...)
|
||||
removedVulnerabilities = append(removedVulnerabilities[:ir], removedVulnerabilities[ir+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ Adds, Removes []*database.Vulnerability }{Adds: addedVulnerabilities, Removes: removedVulnerabilities})
|
||||
}
|
||||
|
||||
// POSTBatchLayersVulnerabilitiesParameters represents the expected parameters
|
||||
// for POSTBatchLayersVulnerabilities.
|
||||
type POSTBatchLayersVulnerabilitiesParameters struct {
|
||||
LayersIDs []string
|
||||
}
|
||||
|
||||
// POSTBatchLayersVulnerabilities returns the complete list of vulnerabilities
|
||||
// that the provided layers have, if they all exist.
|
||||
func POSTBatchLayersVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse body
|
||||
var parameters POSTBatchLayersVulnerabilitiesParameters
|
||||
if s, err := jsonhttp.ParseBody(r, ¶meters); err != nil {
|
||||
jsonhttp.RenderError(w, s, err)
|
||||
return
|
||||
}
|
||||
if len(parameters.LayersIDs) == 0 {
|
||||
jsonhttp.RenderError(w, http.StatusBadRequest, errors.New("at least one LayerID query parameter must be provided"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get minumum priority parameter.
|
||||
minimumPriority := types.Priority(r.URL.Query().Get("minimumPriority"))
|
||||
if minimumPriority == "" {
|
||||
minimumPriority = "High" // Set default priority to High
|
||||
} else if !minimumPriority.IsValid() {
|
||||
jsonhttp.RenderError(w, 0, cerrors.NewBadRequestError("invalid priority"))
|
||||
return
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
// For each LayerID parameter
|
||||
for _, layerID := range parameters.LayersIDs {
|
||||
// Find layer
|
||||
layer, err := database.FindOneLayerByID(layerID, []string{database.FieldLayerParent, database.FieldLayerPackages})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer's packages.
|
||||
packagesNodes, err := layer.AllPackages()
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find vulnerabilities.
|
||||
vulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(packagesNodes, minimumPriority, []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
response[layerID] = struct{ Vulnerabilities []*database.Vulnerability }{Vulnerabilities: vulnerabilities}
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// getSuccessorsFromPackagesNodes returns the node list of packages that have
|
||||
// versions following the versions of the provided packages.
|
||||
func getSuccessorsFromPackagesNodes(packagesNodes []string) ([]string, error) {
|
||||
if len(packagesNodes) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Get packages.
|
||||
packages, err := database.FindAllPackagesByNodes(packagesNodes, []string{database.FieldPackageNextVersion})
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
// Find all packages' successors.
|
||||
var packagesNextVersions []string
|
||||
for _, pkg := range packages {
|
||||
nextVersions, err := pkg.NextVersions([]string{})
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
for _, version := range nextVersions {
|
||||
packagesNextVersions = append(packagesNextVersions, version.Node)
|
||||
}
|
||||
}
|
||||
|
||||
return packagesNextVersions, nil
|
||||
}
|
||||
|
||||
// getVulnerabilitiesFromLayerPackagesNodes returns the list of vulnerabilities
|
||||
// affecting the provided package nodes, filtered by Priority.
|
||||
func getVulnerabilitiesFromLayerPackagesNodes(packagesNodes []string, minimumPriority types.Priority, selectedFields []string) ([]*database.Vulnerability, error) {
|
||||
if len(packagesNodes) == 0 {
|
||||
return []*database.Vulnerability{}, nil
|
||||
}
|
||||
|
||||
// Get successors of the packages.
|
||||
packagesNextVersions, err := getSuccessorsFromPackagesNodes(packagesNodes)
|
||||
if err != nil {
|
||||
return []*database.Vulnerability{}, err
|
||||
}
|
||||
if len(packagesNextVersions) == 0 {
|
||||
return []*database.Vulnerability{}, nil
|
||||
}
|
||||
|
||||
// Find vulnerabilities fixed in these successors.
|
||||
vulnerabilities, err := database.FindAllVulnerabilitiesByFixedIn(packagesNextVersions, selectedFields)
|
||||
if err != nil {
|
||||
return []*database.Vulnerability{}, err
|
||||
}
|
||||
|
||||
// Filter vulnerabilities depending on their priority and remove duplicates.
|
||||
filteredVulnerabilities := []*database.Vulnerability{}
|
||||
seen := map[string]struct{}{}
|
||||
for _, v := range vulnerabilities {
|
||||
if minimumPriority.Compare(v.Priority) <= 0 {
|
||||
if _, alreadySeen := seen[v.ID]; !alreadySeen {
|
||||
filteredVulnerabilities = append(filteredVulnerabilities, v)
|
||||
seen[v.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredVulnerabilities, nil
|
||||
}
|
247
api/logic/vulnerabilities.go
Normal file
247
api/logic/vulnerabilities.go
Normal file
@ -0,0 +1,247 @@
|
||||
// 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 logic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/clair/api/jsonhttp"
|
||||
"github.com/coreos/clair/database"
|
||||
cerrors "github.com/coreos/clair/utils/errors"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// GETVulnerabilities returns a vulnerability identified by an ID if it exists.
|
||||
func GETVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Find vulnerability.
|
||||
vulnerability, err := database.FindOneVulnerability(p.ByName("id"), []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription, database.FieldVulnerabilityFixedIn})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
abstractVulnerability, err := vulnerability.ToAbstractVulnerability()
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, abstractVulnerability)
|
||||
}
|
||||
|
||||
// POSTVulnerabilities manually inserts a vulnerability into the database if it
|
||||
// does not exist yet.
|
||||
func POSTVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
var parameters *database.AbstractVulnerability
|
||||
if s, err := jsonhttp.ParseBody(r, ¶meters); err != nil {
|
||||
jsonhttp.RenderError(w, s, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that the vulnerability does not exist.
|
||||
vulnerability, err := database.FindOneVulnerability(parameters.ID, []string{})
|
||||
if err != nil && err != cerrors.ErrNotFound {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
if vulnerability != nil {
|
||||
jsonhttp.RenderError(w, 0, cerrors.NewBadRequestError("vulnerability already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
// Insert packages.
|
||||
packages := database.AbstractPackagesToPackages(parameters.AffectedPackages)
|
||||
err = database.InsertPackages(packages)
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
var pkgNodes []string
|
||||
for _, p := range packages {
|
||||
pkgNodes = append(pkgNodes, p.Node)
|
||||
}
|
||||
|
||||
// Insert vulnerability.
|
||||
notifications, err := database.InsertVulnerabilities([]*database.Vulnerability{parameters.ToVulnerability(pkgNodes)})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert notifications.
|
||||
err = database.InsertNotifications(notifications, database.GetDefaultNotificationWrapper())
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusCreated, nil)
|
||||
}
|
||||
|
||||
// PUTVulnerabilities updates a vulnerability if it exists.
|
||||
func PUTVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
var parameters *database.AbstractVulnerability
|
||||
if s, err := jsonhttp.ParseBody(r, ¶meters); err != nil {
|
||||
jsonhttp.RenderError(w, s, err)
|
||||
return
|
||||
}
|
||||
parameters.ID = p.ByName("id")
|
||||
|
||||
// Ensure that the vulnerability exists.
|
||||
_, err := database.FindOneVulnerability(parameters.ID, []string{})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert packages.
|
||||
packages := database.AbstractPackagesToPackages(parameters.AffectedPackages)
|
||||
err = database.InsertPackages(packages)
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
var pkgNodes []string
|
||||
for _, p := range packages {
|
||||
pkgNodes = append(pkgNodes, p.Node)
|
||||
}
|
||||
|
||||
// Insert vulnerability.
|
||||
notifications, err := database.InsertVulnerabilities([]*database.Vulnerability{parameters.ToVulnerability(pkgNodes)})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert notifications.
|
||||
err = database.InsertNotifications(notifications, database.GetDefaultNotificationWrapper())
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusCreated, nil)
|
||||
}
|
||||
|
||||
// DELVulnerabilities deletes a vulnerability if it exists.
|
||||
func DELVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
err := database.DeleteVulnerability(p.ByName("id"))
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// GETVulnerabilitiesIntroducingLayers returns the list of layers that
|
||||
// introduces a given vulnerability, if it exists.
|
||||
// To clarify, it does not return the list of every layers that have
|
||||
// the vulnerability.
|
||||
func GETVulnerabilitiesIntroducingLayers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Find vulnerability to verify that it exists.
|
||||
_, err := database.FindOneVulnerability(p.ByName("id"), []string{})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
layers, err := database.FindAllLayersIntroducingVulnerability(p.ByName("id"), []string{database.FieldLayerID})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
layersIDs := []string{}
|
||||
for _, l := range layers {
|
||||
layersIDs = append(layersIDs, l.ID)
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, struct{ IntroducingLayersIDs []string }{IntroducingLayersIDs: layersIDs})
|
||||
}
|
||||
|
||||
// POSTVulnerabilitiesAffectedLayersParameters represents the expected
|
||||
// parameters for POSTVulnerabilitiesAffectedLayers.
|
||||
type POSTVulnerabilitiesAffectedLayersParameters struct {
|
||||
LayersIDs []string
|
||||
}
|
||||
|
||||
// POSTVulnerabilitiesAffectedLayers returns whether the specified layers
|
||||
// (by their IDs) are vulnerable to the given Vulnerability or not.
|
||||
func POSTVulnerabilitiesAffectedLayers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse body.
|
||||
var parameters POSTBatchLayersVulnerabilitiesParameters
|
||||
if s, err := jsonhttp.ParseBody(r, ¶meters); err != nil {
|
||||
jsonhttp.RenderError(w, s, err)
|
||||
return
|
||||
}
|
||||
if len(parameters.LayersIDs) == 0 {
|
||||
jsonhttp.RenderError(w, http.StatusBadRequest, errors.New("getting the entire list of affected layers is not supported yet: at least one LayerID query parameter must be provided"))
|
||||
return
|
||||
}
|
||||
|
||||
// Find vulnerability.
|
||||
vulnerability, err := database.FindOneVulnerability(p.ByName("id"), []string{database.FieldVulnerabilityFixedIn})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save the fixed in nodes into a map for fast check.
|
||||
fixedInPackagesMap := make(map[string]struct{})
|
||||
for _, fixedInNode := range vulnerability.FixedInNodes {
|
||||
fixedInPackagesMap[fixedInNode] = struct{}{}
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
// For each LayerID parameter.
|
||||
for _, layerID := range parameters.LayersIDs {
|
||||
// Find layer
|
||||
layer, err := database.FindOneLayerByID(layerID, []string{database.FieldLayerParent, database.FieldLayerPackages, database.FieldLayerPackages})
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find layer's packages.
|
||||
packagesNodes, err := layer.AllPackages()
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get successors packages of layer' packages.
|
||||
successors, err := getSuccessorsFromPackagesNodes(packagesNodes)
|
||||
if err != nil {
|
||||
jsonhttp.RenderError(w, 0, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if the layer is vulnerable by verifying if one of the successors
|
||||
// of its packages are fixed by the vulnerability.
|
||||
vulnerable := false
|
||||
for _, p := range successors {
|
||||
if _, fixed := fixedInPackagesMap[p]; fixed {
|
||||
vulnerable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
response[layerID] = struct{ Vulnerable bool }{Vulnerable: vulnerable}
|
||||
}
|
||||
|
||||
jsonhttp.Render(w, http.StatusOK, response)
|
||||
}
|
96
api/router.go
Normal file
96
api/router.go
Normal file
@ -0,0 +1,96 @@
|
||||
// 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 api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/clair/api/logic"
|
||||
"github.com/coreos/clair/api/wrappers"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// VersionRouter is an HTTP router that forwards requests to the appropriate
|
||||
// router depending on the API version specified in the requested URI.
|
||||
type VersionRouter map[string]*httprouter.Router
|
||||
|
||||
// NewVersionRouter instantiates a VersionRouter and every sub-routers that are
|
||||
// necessary to handle supported API versions.
|
||||
func NewVersionRouter(to time.Duration) *VersionRouter {
|
||||
return &VersionRouter{
|
||||
"/v1": NewRouterV1(to),
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP forwards requests to the appropriate router depending on the API
|
||||
// version specified in the requested URI and remove the version information
|
||||
// from the request URL.Path, without modifying the request uRequestURI.
|
||||
func (vs VersionRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
urlStr := r.URL.String()
|
||||
var version string
|
||||
if len(urlStr) >= 3 {
|
||||
version = urlStr[:3]
|
||||
}
|
||||
if router, _ := vs[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
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// NewRouterV1 creates a new router for the API (Version 1)
|
||||
func NewRouterV1(to time.Duration) *httprouter.Router {
|
||||
router := httprouter.New()
|
||||
wrap := func(fn httprouter.Handle) httprouter.Handle {
|
||||
return wrappers.Log(wrappers.TimeOut(to, fn))
|
||||
}
|
||||
|
||||
// General
|
||||
router.GET("/versions", wrap(logic.GETVersions))
|
||||
router.GET("/health", wrap(logic.GETHealth))
|
||||
|
||||
// Layers
|
||||
router.POST("/layers", wrap(logic.POSTLayers))
|
||||
router.GET("/layers/:id/os", wrap(logic.GETLayersOS))
|
||||
router.GET("/layers/:id/parent", wrap(logic.GETLayersParent))
|
||||
router.GET("/layers/:id/packages", wrap(logic.GETLayersPackages))
|
||||
router.GET("/layers/:id/packages/diff", wrap(logic.GETLayersPackagesDiff))
|
||||
router.GET("/layers/:id/vulnerabilities", wrap(logic.GETLayersVulnerabilities))
|
||||
router.GET("/layers/:id/vulnerabilities/diff", wrap(logic.GETLayersVulnerabilitiesDiff))
|
||||
// # Batch version of "/layers/:id/vulnerabilities"
|
||||
router.POST("/batch/layers/vulnerabilities", wrap(logic.POSTBatchLayersVulnerabilities))
|
||||
|
||||
// Vulnerabilities
|
||||
router.POST("/vulnerabilities", wrap(logic.POSTVulnerabilities))
|
||||
router.PUT("/vulnerabilities/:id", wrap(logic.PUTVulnerabilities))
|
||||
router.GET("/vulnerabilities/:id", wrap(logic.GETVulnerabilities))
|
||||
router.DELETE("/vulnerabilities/:id", wrap(logic.DELVulnerabilities))
|
||||
router.GET("/vulnerabilities/:id/introducing-layers", wrap(logic.GETVulnerabilitiesIntroducingLayers))
|
||||
router.POST("/vulnerabilities/:id/affected-layers", wrap(logic.POSTVulnerabilitiesAffectedLayers))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// NewHealthRouter creates a new router that only serve the Health function on /
|
||||
func NewHealthRouter() *httprouter.Router {
|
||||
router := httprouter.New()
|
||||
router.GET("/", logic.GETHealth)
|
||||
return router
|
||||
}
|
75
api/wrappers/log.go
Normal file
75
api/wrappers/log.go
Normal file
@ -0,0 +1,75 @@
|
||||
// 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 logic implements all the available API methods.
|
||||
// Every methods are documented in docs/API.md.
|
||||
|
||||
// Package wrappers contains httprouter.Handle wrappers that are used in the API.
|
||||
package wrappers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "api")
|
||||
|
||||
type logWriter struct {
|
||||
http.ResponseWriter
|
||||