copy hyperclair to clairctl and rename package
This commit is contained in:
parent
97347ec44d
commit
3083a891e0
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/.vscode
|
19
cmd/clairctl/.travis.yml
Normal file
19
cmd/clairctl/.travis.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.5
|
||||||
|
install:
|
||||||
|
- go get -d -v ./...
|
||||||
|
- go build -ldflags "-X github.com/coreos/clair/cmd/clairctl/cmd.version=$(cat VERSION)" -v
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: SPeZzw212p/0iYPLVWUkLq226j19oWHBogHIlbKzkp8zNk7tdJEHMthAhka1zxS5afr+JWu3BBdduPHbnvvpXtv2axsrCXVsW7jPEcxXgfM3m/YSxQwjsGojMG318nX/E5ApY8eDhiZuHoTAP67DJZEoLteV5GKvNk7np74bMNexBxCPDIbXepBbjrEqxrUG/uVDFrmFf0LlCKVGxALK42uriaDM1tPGDy0+7zX7a3RqG9J4ROmZCQNzd9+rcur4DndrxYPCvJK3awUwXL5XzdRed24m5S8IG6Q/gMMUhUVECMNZ6/Z+KZ4CKqcQ9e9NvOtHYvO0OtPNrd4/HajZpUpfO008imSj6/g9NxdC6hJYvyK/HuRv5DsLgZukyvwroVSM5rC77FJeOmoLXfEBHo7E16I5XKGcp31NoCv5kdQuaHLxkBZk43CcnT/rraQ8cjCQAERQEh6u0fidyOvZ4vkTW30/c6H25zDejCzDcx1oHL4O8QwJwMfRSifSA8vk81saOurmQeviZK1UkpeWLxMYLpFZ8vN9kK/2Nn24ud9DN7N8bqGob13I8lJ69j9xcXuq/86vITziESZCHaYs2Hw1vSHD8vpkQxu8aSYqPXgwAKDwV9JkGV1/kcUdwpOlD/xkKunluu8Mf5K2JSm9G8xWr6foOibKsuHDn0nH+jY=
|
||||||
|
file: hyperclair
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
repo: wemanity-belgium/hyperclair
|
||||||
|
tags: true
|
||||||
|
branch: master
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
secure: QpHC0r/d3sUNZ4Tisu7IjRnE+exzUUdTRlWUvvepWA2/wdsvsh9IFKmPgHvmt/V46RtByc52HVwE/uE6gF8eVhRs4L4DP/gSi0TxmfNi2m+6EdL1MmFvdIVGfPZDDbFnNNXpWcNJGv5/UDJ8eke003Mtm7/qQfJqH3UzxoWnTrvsA7IHbhxomgsHOFqHU+e6fmaQ3q7neUJEci2HSPLehl11yBwMcsYyFjeW+33GVQeI0m2j1TrcldG3KN63tszIx7tHXTUrIK369nrndLjYWnc7Hi+k6Uc8vPh111/hLBG4UtxffP+/DTkhEtzxDNn3hn4zSKTMIyeKA/UhnySYchzDYOdVZoM0oVxAkbTcdTPWCxS75W1u3AW3QAGxScNEqmy5enrW+kGyRpd1zIRJ1qS2tHAVmos8N6bl5Rp9UJF5UITr9/DyQS93O9DOSY5Z6UVGI5J+MlG89Nr4XJ5NW4ak5yC5fc+9IwM58VMk8ULS3I0g/YBWsWRWHGZrHurnvcGbiXHk5S+anQsiYUV8n3OUa67ai7Duil3qiPklEhHeFZzmXrJMvLdeVxywfvIfKzSEGqDrUYw3hO9pkMzLxDQfvaNzAHWZGJ970N7apX2pnAUgPDsNb8cOnbDSjX36FICAiUZlrkpsz6O6h3Fm/2t/HDCyYH7gf6eJXypUOYc=
|
21
cmd/clairctl/LICENSE
Normal file
21
cmd/clairctl/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 wemanity-belgium
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
85
cmd/clairctl/README.md
Normal file
85
cmd/clairctl/README.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
|
||||||
|
# hyperclair
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/wemanity-belgium/hyperclair.svg?branch=develop)](https://travis-ci.org/wemanity-belgium/hyperclair) [![Join the chat at https://gitter.im/wemanity-belgium/hyperclair](https://badges.gitter.im/wemanity-belgium/hyperclair.svg)](https://gitter.im/wemanity-belgium/hyperclair?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
|
> Tracking container vulnerabilities, that's should be *Hyperclair*
|
||||||
|
|
||||||
|
Tracking vulnerabilities in your container images, it's easy with CoreOS Clair.
|
||||||
|
Integrate it inside your CI/CD pipeline is easier with Hyperclair.
|
||||||
|
|
||||||
|
Hyperclair is a lightweight command-line tool doing the bridge between Registries as Docker Hub, Docker Registry or Quay.io, and the CoreOS vulnerability tracker, Clair.
|
||||||
|
It's easily integrated in your CI/CD pipeline and Hyperclair will play as reverse proxy for authentication.
|
||||||
|
|
||||||
|
|
||||||
|
> The Registry is a stateless, highly scalable server side application that stores and lets you distribute Docker images. The Registry is open-source, under the permissive Apache license.
|
||||||
|
>
|
||||||
|
>*From https://docs.docker.com/registry/*
|
||||||
|
|
||||||
|
> Clair is a container vulnerability analysis service. It provides a list of vulnerabilities that threaten a container, and can notify users when new vulnerabilities that affect existing containers become known.
|
||||||
|
>
|
||||||
|
>*From https://github.com/coreos/clair*
|
||||||
|
|
||||||
|
hyperclair is tool to make the link between the Docker Registry and the CoreOS Clair tool.
|
||||||
|
|
||||||
|
![hyperclair](https://cloud.githubusercontent.com/assets/3304363/14174675/348bc190-f746-11e5-9edd-9e736ec38b0e.png)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
[![asciicast](https://asciinema.org/a/41461.png)](https://asciinema.org/a/41461)
|
||||||
|
|
||||||
|
# Notification
|
||||||
|
2. On-Demand: the CLI tool is used to pull image from Registry then push it to Clair
|
||||||
|
|
||||||
|
# Reporting
|
||||||
|
|
||||||
|
**hyperclair** get vulnerabilities report from Clair and generate HTML report
|
||||||
|
|
||||||
|
hyperclair can be used for Docker Hub and self-hosted Registry
|
||||||
|
|
||||||
|
# Command
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyse your docker image with Clair, directly from your registry.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
hyperclair [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
analyse Analyse Docker image
|
||||||
|
health Get Health of Hyperclair and underlying services
|
||||||
|
login Log in to a Docker registry
|
||||||
|
logout Log out from a Docker registry
|
||||||
|
pull Pull Docker image information
|
||||||
|
push Push Docker image to Clair
|
||||||
|
report Generate Docker Image vulnerabilities report
|
||||||
|
version Get Versions of Hyperclair and underlying services
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--config string config file (default is ./.hyperclair.yml)
|
||||||
|
--log-level string log level [Panic,Fatal,Error,Warn,Info,Debug]
|
||||||
|
|
||||||
|
Use "hyperclair [command] --help" for more information about a command.
|
||||||
|
```
|
||||||
|
|
||||||
|
# Optional Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
clair:
|
||||||
|
port: 6060
|
||||||
|
healthPort: 6061
|
||||||
|
uri: http://clair
|
||||||
|
priority: Low
|
||||||
|
report:
|
||||||
|
path: ./reports
|
||||||
|
format: html
|
||||||
|
```
|
||||||
|
|
||||||
|
# Remarks
|
||||||
|
|
||||||
|
1. Analyzing Official Docker image is disallowed. You cannot pull layers from image you don't own.
|
||||||
|
|
||||||
|
# Contribution and Test
|
||||||
|
|
||||||
|
Go to /contrib folder
|
1
cmd/clairctl/VERSION
Normal file
1
cmd/clairctl/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.4.0
|
39
cmd/clairctl/clair/analyse.go
Normal file
39
cmd/clairctl/clair/analyse.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Analyse get Analysis os specified layer
|
||||||
|
func Analyse(id string) (v1.LayerEnvelope, error) {
|
||||||
|
|
||||||
|
lURI := fmt.Sprintf("%v/layers/%v?vulnerabilities", uri, id)
|
||||||
|
// lURI := fmt.Sprintf("%v/layers/%v/vulnerabilities?minimumPriority=%v", uri, id, priority)
|
||||||
|
response, err := http.Get(lURI)
|
||||||
|
if err != nil {
|
||||||
|
return v1.LayerEnvelope{}, fmt.Errorf("analysing layer %v: %v", id, err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return v1.LayerEnvelope{}, fmt.Errorf("reading layer analysis: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return v1.LayerEnvelope{}, fmt.Errorf("%d - %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var analysis v1.LayerEnvelope
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &analysis)
|
||||||
|
if err != nil {
|
||||||
|
return v1.LayerEnvelope{}, fmt.Errorf("unmarshalling layer analysis: %v", err)
|
||||||
|
}
|
||||||
|
return analysis, nil
|
||||||
|
}
|
289
cmd/clairctl/clair/clair.go
Normal file
289
cmd/clairctl/clair/clair.go
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var uri string
|
||||||
|
var priority string
|
||||||
|
var healthPort int
|
||||||
|
|
||||||
|
//Report Reporting Config value
|
||||||
|
var Report ReportConfig
|
||||||
|
|
||||||
|
//VulnerabiliesCounts Total count of vulnerabilities
|
||||||
|
type VulnerabiliesCounts struct {
|
||||||
|
Total int
|
||||||
|
High int
|
||||||
|
Medium int
|
||||||
|
Low int
|
||||||
|
Negligible int
|
||||||
|
}
|
||||||
|
|
||||||
|
//RelativeCount get the percentage of vulnerabilities of a severity
|
||||||
|
func (vulnerabilityCount VulnerabiliesCounts) RelativeCount(severity string) float64 {
|
||||||
|
var count int
|
||||||
|
|
||||||
|
switch severity {
|
||||||
|
case "High":
|
||||||
|
count = vulnerabilityCount.High
|
||||||
|
case "Medium":
|
||||||
|
count = vulnerabilityCount.Medium
|
||||||
|
case "Low":
|
||||||
|
count = vulnerabilityCount.Low
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Ceil(float64(count) / float64(vulnerabilityCount.Total) * 100 * 100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
//ImageAnalysis Full image analysis
|
||||||
|
type ImageAnalysis struct {
|
||||||
|
Registry string
|
||||||
|
ImageName string
|
||||||
|
Tag string
|
||||||
|
Layers []v1.LayerEnvelope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageAnalysis ImageAnalysis) String() string {
|
||||||
|
return imageAnalysis.Registry + "/" + imageAnalysis.ImageName + ":" + imageAnalysis.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageAnalysis ImageAnalysis) ShortName(l v1.Layer) string {
|
||||||
|
return xstrings.Substr(l.Name, 0, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountVulnerabilities counts all image vulnerability
|
||||||
|
func (imageAnalysis ImageAnalysis) CountVulnerabilities(l v1.Layer) int {
|
||||||
|
count := 0
|
||||||
|
for _, f := range l.Features {
|
||||||
|
count += len(f.Vulnerabilities)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountAllVulnerabilities Total count of vulnerabilities
|
||||||
|
func (imageAnalysis ImageAnalysis) CountAllVulnerabilities() VulnerabiliesCounts {
|
||||||
|
var result VulnerabiliesCounts;
|
||||||
|
result.Total = 0
|
||||||
|
result.High = 0
|
||||||
|
result.Medium = 0
|
||||||
|
result.Low = 0
|
||||||
|
result.Negligible = 0
|
||||||
|
|
||||||
|
for _, l := range imageAnalysis.Layers {
|
||||||
|
for _, f := range l.Layer.Features {
|
||||||
|
result.Total += len(f.Vulnerabilities)
|
||||||
|
for _, v := range f.Vulnerabilities {
|
||||||
|
switch v.Severity {
|
||||||
|
case "High":
|
||||||
|
result.High++
|
||||||
|
case "Medium":
|
||||||
|
result.Medium++
|
||||||
|
case "Low":
|
||||||
|
result.Low++
|
||||||
|
case "Negligible":
|
||||||
|
result.Negligible++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vulnerability : A vulnerability inteface
|
||||||
|
type Vulnerability struct {
|
||||||
|
Name, Severity, IntroduceBy, Description, Link, Layer string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight get the weight of the vulnerability according to its Severity
|
||||||
|
func (v Vulnerability) Weight() int {
|
||||||
|
weight := 0
|
||||||
|
|
||||||
|
switch v.Severity {
|
||||||
|
case "High":
|
||||||
|
weight = 4
|
||||||
|
case "Medium":
|
||||||
|
weight = 3
|
||||||
|
case "Low":
|
||||||
|
weight = 2
|
||||||
|
case "Negligible":
|
||||||
|
weight = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return weight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer : A layer inteface
|
||||||
|
type Layer struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Namespace string
|
||||||
|
Features []Feature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature : A feature inteface
|
||||||
|
type Feature struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Vulnerabilities []Vulnerability
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status give the healthy / unhealthy statut of a feature
|
||||||
|
func (feature Feature) Status() bool {
|
||||||
|
return len(feature.Vulnerabilities) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight git the weight of a featrure according to its vulnerabilities
|
||||||
|
func (feature Feature) Weight() int {
|
||||||
|
weight := 0
|
||||||
|
|
||||||
|
for _, v := range feature.Vulnerabilities {
|
||||||
|
weight += v.Weight()
|
||||||
|
}
|
||||||
|
|
||||||
|
return weight
|
||||||
|
}
|
||||||
|
|
||||||
|
// VulnerabilitiesBySeverity sorting vulnerabilities by severity
|
||||||
|
type VulnerabilitiesBySeverity []Vulnerability
|
||||||
|
|
||||||
|
func (a VulnerabilitiesBySeverity) Len() int { return len(a) }
|
||||||
|
func (a VulnerabilitiesBySeverity) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a VulnerabilitiesBySeverity) Less(i, j int) bool {
|
||||||
|
return a[i].Weight() > a[j].Weight()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayerByVulnerabilities sorting of layers by global vulnerability
|
||||||
|
type LayerByVulnerabilities []Layer
|
||||||
|
|
||||||
|
func (a LayerByVulnerabilities) Len() int { return len(a) }
|
||||||
|
func (a LayerByVulnerabilities) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a LayerByVulnerabilities) Less(i, j int) bool {
|
||||||
|
firstVulnerabilities := 0
|
||||||
|
secondVulnerabilities := 0
|
||||||
|
|
||||||
|
for _, l := range a[i].Features {
|
||||||
|
firstVulnerabilities = firstVulnerabilities + l.Weight()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ , l := range a[j].Features {
|
||||||
|
secondVulnerabilities = secondVulnerabilities + l.Weight()
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstVulnerabilities > secondVulnerabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeatureByVulnerabilities sorting off features by vulnerabilities
|
||||||
|
type FeatureByVulnerabilities []Feature
|
||||||
|
|
||||||
|
func (a FeatureByVulnerabilities) Len() int { return len(a) }
|
||||||
|
func (a FeatureByVulnerabilities) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
|
func (a FeatureByVulnerabilities) Less(i, j int) bool {
|
||||||
|
return a[i].Weight() > a[j].Weight()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortLayers give layers ordered by vulnerability algorithm
|
||||||
|
func (imageAnalysis ImageAnalysis) SortLayers() []Layer {
|
||||||
|
layers := []Layer{}
|
||||||
|
|
||||||
|
for _, l := range imageAnalysis.Layers {
|
||||||
|
features := []Feature{}
|
||||||
|
|
||||||
|
for _, f := range l.Layer.Features {
|
||||||
|
vulnerabilities := []Vulnerability{}
|
||||||
|
|
||||||
|
for _, v := range f.Vulnerabilities {
|
||||||
|
nv := Vulnerability{
|
||||||
|
Name: v.Name,
|
||||||
|
Severity: v.Severity,
|
||||||
|
IntroduceBy: f.Name + ":" + f.Version,
|
||||||
|
Description: v.Description,
|
||||||
|
Layer: l.Layer.Name,
|
||||||
|
Link: v.Link,
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnerabilities = append(vulnerabilities, nv);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(VulnerabilitiesBySeverity(vulnerabilities))
|
||||||
|
|
||||||
|
nf := Feature{
|
||||||
|
Name: f.Name,
|
||||||
|
Version: f.Version,
|
||||||
|
Vulnerabilities: vulnerabilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
features = append(features, nf);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(FeatureByVulnerabilities(features))
|
||||||
|
|
||||||
|
nl := Layer{
|
||||||
|
Name: l.Layer.Name,
|
||||||
|
Path: l.Layer.Path,
|
||||||
|
Features: features,
|
||||||
|
}
|
||||||
|
layers = append(layers, nl);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(LayerByVulnerabilities(layers));
|
||||||
|
|
||||||
|
return layers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortVulnerabilities get all vulnerabilities sorted by Severity
|
||||||
|
func (imageAnalysis ImageAnalysis) SortVulnerabilities() []Vulnerability {
|
||||||
|
vulnerabilities := []Vulnerability{}
|
||||||
|
|
||||||
|
// there should be a better method, but I don't know how to easlily concert []v1.Vulnerability to [Vulnerability]
|
||||||
|
for _, l := range imageAnalysis.Layers {
|
||||||
|
for _, f := range l.Layer.Features {
|
||||||
|
for _, v := range f.Vulnerabilities {
|
||||||
|
nv := Vulnerability{
|
||||||
|
Name: v.Name,
|
||||||
|
Severity: v.Severity,
|
||||||
|
IntroduceBy: f.Name + ":" + f.Version,
|
||||||
|
Description: v.Description,
|
||||||
|
Layer: l.Layer.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnerabilities = append(vulnerabilities, nv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(VulnerabilitiesBySeverity(vulnerabilities));
|
||||||
|
|
||||||
|
return vulnerabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtURI(u string, port int) {
|
||||||
|
uri = u
|
||||||
|
if port != 0 {
|
||||||
|
uri += ":" + strconv.Itoa(port)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(uri, "/v1") {
|
||||||
|
uri += "/v1"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://") {
|
||||||
|
uri = "http://" + uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Config configure Clair from configFile
|
||||||
|
func Config() {
|
||||||
|
fmtURI(viper.GetString("clair.uri"), viper.GetInt("clair.port"))
|
||||||
|
priority = viper.GetString("clair.priority")
|
||||||
|
healthPort = viper.GetInt("clair.healthPort")
|
||||||
|
Report.Path = viper.GetString("clair.report.path")
|
||||||
|
Report.Format = viper.GetString("clair.report.format")
|
||||||
|
}
|
129
cmd/clairctl/clair/clair_test.go
Normal file
129
cmd/clairctl/clair/clair_test.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSampleAnalysis() []byte {
|
||||||
|
file, err := ioutil.ReadFile("./samples/clair_report.json")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("File error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(httpStatus int) *httptest.Server {
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(httpStatus)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsHealthy(t *testing.T) {
|
||||||
|
server := newServer(http.StatusOK)
|
||||||
|
defer server.Close()
|
||||||
|
uri = server.URL
|
||||||
|
if h := IsHealthy(); !h {
|
||||||
|
t.Errorf("IsHealthy() => %v, want %v", h, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNotHealthy(t *testing.T) {
|
||||||
|
server := newServer(http.StatusInternalServerError)
|
||||||
|
defer server.Close()
|
||||||
|
uri = server.URL
|
||||||
|
if h := IsHealthy(); h {
|
||||||
|
t.Errorf("IsHealthy() => %v, want %v", h, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountAllVulnerabilities(t *testing.T) {
|
||||||
|
|
||||||
|
var analysis ImageAnalysis
|
||||||
|
err := json.Unmarshal([]byte(getSampleAnalysis()), &analysis)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failing with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnerabilitiesCount := analysis.CountAllVulnerabilities()
|
||||||
|
|
||||||
|
if vulnerabilitiesCount.Total != 77 {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().Total => %v, want 77", vulnerabilitiesCount.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vulnerabilitiesCount.High != 1 {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().High => %v, want 1", vulnerabilitiesCount.High)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vulnerabilitiesCount.Medium != 18 {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().Medium => %v, want 18", vulnerabilitiesCount.Medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vulnerabilitiesCount.Low != 57 {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().Low => %v, want 57", vulnerabilitiesCount.Low)
|
||||||
|
}
|
||||||
|
if vulnerabilitiesCount.Negligible != 1 {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().Negligible => %v, want 1", vulnerabilitiesCount.Negligible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelativeCount(t *testing.T) {
|
||||||
|
var analysis ImageAnalysis
|
||||||
|
err := json.Unmarshal([]byte(getSampleAnalysis()), &analysis)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failing with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnerabilitiesCount := analysis.CountAllVulnerabilities()
|
||||||
|
|
||||||
|
if (vulnerabilitiesCount.RelativeCount("High") != 1.3) {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().RelativeCount(\"High\") => %v, want 1.3", vulnerabilitiesCount.RelativeCount("High"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vulnerabilitiesCount.RelativeCount("Medium") != 23.38) {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().RelativeCount(\"Medium\") => %v, want 23.38", vulnerabilitiesCount.RelativeCount("Medium"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vulnerabilitiesCount.RelativeCount("Low") != 74.03) {
|
||||||
|
t.Errorf("analysis.CountAllVulnerabilities().RelativeCount(\"Low\") => %v, want 74.03", vulnerabilitiesCount.RelativeCount("Low"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeatureWeight(t *testing.T) {
|
||||||
|
feature := Feature{
|
||||||
|
Vulnerabilities: []Vulnerability{},
|
||||||
|
}
|
||||||
|
|
||||||
|
v1 := Vulnerability{
|
||||||
|
Severity: "High",
|
||||||
|
}
|
||||||
|
|
||||||
|
v2 := Vulnerability{
|
||||||
|
Severity: "Medium",
|
||||||
|
}
|
||||||
|
|
||||||
|
v3 := Vulnerability{
|
||||||
|
Severity: "Low",
|
||||||
|
}
|
||||||
|
|
||||||
|
v4 := Vulnerability{
|
||||||
|
Severity: "Negligible",
|
||||||
|
}
|
||||||
|
|
||||||
|
feature.Vulnerabilities = append(feature.Vulnerabilities, v1)
|
||||||
|
feature.Vulnerabilities = append(feature.Vulnerabilities, v2)
|
||||||
|
feature.Vulnerabilities = append(feature.Vulnerabilities, v3)
|
||||||
|
feature.Vulnerabilities = append(feature.Vulnerabilities, v4)
|
||||||
|
|
||||||
|
if (feature.Weight() != 10) {
|
||||||
|
t.Errorf("feature.Weigh => %v, want 6", feature.Weight())
|
||||||
|
}
|
||||||
|
}
|
13
cmd/clairctl/clair/errors.go
Normal file
13
cmd/clairctl/clair/errors.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
const oSNotSupportedValue = "worker: OS and/or package manager are not supported"
|
||||||
|
|
||||||
|
var (
|
||||||
|
OSNotSupported = errors.New(oSNotSupportedValue)
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayerError struct {
|
||||||
|
Message string
|
||||||
|
}
|
0
cmd/clairctl/clair/go-bindata
Normal file
0
cmd/clairctl/clair/go-bindata
Normal file
26
cmd/clairctl/clair/health.go
Normal file
26
cmd/clairctl/clair/health.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsHealthy() bool {
|
||||||
|
healthURI := strings.Replace(uri, "6060/v1", strconv.Itoa(healthPort), 1) + "/health"
|
||||||
|
response, err := http.Get(healthURI)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "requesting Clair health: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
52
cmd/clairctl/clair/push.go
Normal file
52
cmd/clairctl/clair/push.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Push send a layer to Clair for analysis
|
||||||
|
func Push(layer v1.LayerEnvelope) error {
|
||||||
|
lJSON, err := json.Marshal(layer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshalling layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lURI := fmt.Sprintf("%v/layers", uri)
|
||||||
|
request, err := http.NewRequest("POST", lURI, bytes.NewBuffer(lJSON))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating 'add layer' request: %v", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
response, err := (&http.Client{}).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pushing layer to clair: %v", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != 201 {
|
||||||
|
if response.StatusCode == 422 {
|
||||||
|
return OSNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading 'add layer' response : %v", err)
|
||||||
|
}
|
||||||
|
var lErr LayerError
|
||||||
|
err = json.Unmarshal(body, &lErr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshalling 'add layer' error message: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%d - %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
32
cmd/clairctl/clair/report.go
Normal file
32
cmd/clairctl/clair/report.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go-bindata -pkg clair -o templates.go templates/...
|
||||||
|
|
||||||
|
//ReportConfig Reporting configuration
|
||||||
|
type ReportConfig struct {
|
||||||
|
Path string
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
|
||||||
|
//ReportAsHTML report analysis as HTML
|
||||||
|
func ReportAsHTML(analyses ImageAnalysis) (string, error) {
|
||||||
|
asset, err := Asset("templates/analysis-template.html")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("accessing template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
templte := template.Must(template.New("analysis-template").Parse(string(asset)))
|
||||||
|
|
||||||
|
var doc bytes.Buffer
|
||||||
|
err = templte.Execute(&doc, analyses)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("rendering HTML report: %v", err)
|
||||||
|
}
|
||||||
|
return doc.String(), nil
|
||||||
|
}
|
31
cmd/clairctl/clair/report_test.go
Normal file
31
cmd/clairctl/clair/report_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReportAsHtml(t *testing.T) {
|
||||||
|
var analysis ImageAnalysis
|
||||||
|
err := json.Unmarshal([]byte(getSampleAnalysis()), &analysis)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failing with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := ReportAsHTML(analysis)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(os.TempDir()+"/hyperclair-html-report.html")
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(os.TempDir()+"/hyperclair-html-report.html", []byte(html), 0700)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
1580
cmd/clairctl/clair/samples/clair_report.json
Normal file
1580
cmd/clairctl/clair/samples/clair_report.json
Normal file
File diff suppressed because it is too large
Load Diff
260
cmd/clairctl/clair/templates.go
Normal file
260
cmd/clairctl/clair/templates.go
Normal file
File diff suppressed because one or more lines are too long
100
cmd/clairctl/clair/templates/analysis-template.1.html
Normal file
100
cmd/clairctl/clair/templates/analysis-template.1.html
Normal file
File diff suppressed because one or more lines are too long
420
cmd/clairctl/clair/templates/analysis-template.html
Normal file
420
cmd/clairctl/clair/templates/analysis-template.html
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hyperclair report : {{.ImageName}}</title>
|
||||||
|
|
||||||
|
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,600italic,400italic,300italic,300' rel='stylesheet' type='text/css'>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: ghostwhite;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* global layout */
|
||||||
|
.container {
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
clear:both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: 0 -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row:after {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="col-"] {
|
||||||
|
padding: 0 20px;
|
||||||
|
float: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-6 {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
/* padding: 1em; */
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0px 1px 2px #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-bottom: .2em;
|
||||||
|
border-bottom: solid 1px gainsboro;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0px 0px 0px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
box-shadow: 0 -2px 16px #263238;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: lighter;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-intro {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #263238;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-bottom: solid 1px #ECEFF1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-intro h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
line-height: .6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* report */
|
||||||
|
|
||||||
|
.report {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 960px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style of the graph */
|
||||||
|
.graph .node {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
.graph .node .dot {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
background: gray;
|
||||||
|
|
||||||
|
/* box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
|
||||||
|
border: solid 1px rgba(255, 255, 255, .2); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node.High .dot {
|
||||||
|
background: #E91E63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node.Medium .dot {
|
||||||
|
background: #FFA726;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node.Low .dot {
|
||||||
|
background: #8BC34A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node .popup {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-left: -150px;
|
||||||
|
left: 2px;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0px 1px 2px rgba(0, 0, 0, .2);
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* border: solid 1px #e2e2e2; */
|
||||||
|
text-shadow: 0 0 0 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node .popup:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-width: 10px 10px 0 10px;
|
||||||
|
border-color: white transparent transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node .popup:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-width: 11px 11px 0 10px;
|
||||||
|
border-color: rgba(0, 0, 0, .2) transparent transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node .popup > div {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node:hover .dot {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph .node:hover .popup {
|
||||||
|
display: block;
|
||||||
|
max-height: 180px;
|
||||||
|
color: dimgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bars */
|
||||||
|
.bar-bg {
|
||||||
|
display: inline-block;
|
||||||
|
width: 240px;
|
||||||
|
height: 6px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-bar {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #E91E63;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-bar.High {
|
||||||
|
background: #E91E63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-bar.Medium {
|
||||||
|
background: #FFA726;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-bar.Low {
|
||||||
|
background: #8BC34A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vulnerabilities */
|
||||||
|
.report {
|
||||||
|
margin: 18px auto;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report .vulnerabilities,
|
||||||
|
.report .features > ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report .features > ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
border-bottom: solid 1px #ECEFF1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature:last-child {
|
||||||
|
border-color: #CFD8DC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature__title {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities {
|
||||||
|
padding-left: 2.6em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerability {
|
||||||
|
padding-bottom: .8em;
|
||||||
|
padding-right: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities .High .name {
|
||||||
|
color: #E91E63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities .Medium .name {
|
||||||
|
color: #FFA726;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities .Low .name {
|
||||||
|
color: #8BC34A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* layers */
|
||||||
|
|
||||||
|
.layer .layer__title {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1em;
|
||||||
|
border-bottom: solid 1px #CFD8DC;
|
||||||
|
color: #37474F;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer .layer__title:hover {
|
||||||
|
background: #ECEFF1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.closed .features {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="app-header">
|
||||||
|
<h1>Hyperclair report</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="app-intro clearfix">
|
||||||
|
<h2>Image: {{.ImageName}}</h2>
|
||||||
|
|
||||||
|
<section class="summary">
|
||||||
|
<div>
|
||||||
|
{{with $vulnerabilitiesCount := .CountAllVulnerabilities}}
|
||||||
|
<p><span class="lead"><strong>Total : {{$vulnerabilitiesCount.Total}}</strong></span></p>
|
||||||
|
<p>
|
||||||
|
<span style="display: inline-block; width: 120px;">Critical : <strong>{{$vulnerabilitiesCount.High}}</strong></span>
|
||||||
|
<!--<span class="bar-bg">
|
||||||
|
<span class="bar-bar High" style="width: {{$vulnerabilitiesCount.RelativeCount "High"}}%"></span>
|
||||||
|
</span>-->
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span style="display: inline-block; width: 120px;">Medium : <strong>{{$vulnerabilitiesCount.Medium}}</strong></span>
|
||||||
|
<!--<span class="bar-bg">
|
||||||
|
<span class="bar-bar Medium" style="width: {{$vulnerabilitiesCount.RelativeCount "Medium"}}%"></span>
|
||||||
|
</span>-->
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span style="display: inline-block; width: 120px;">Low : <strong>{{$vulnerabilitiesCount.Low}}</strong></span>
|
||||||
|
<!--<span class="bar-bg">
|
||||||
|
<span class="bar-bar Low" style="width: {{$vulnerabilitiesCount.RelativeCount "Low"}}%"></span>
|
||||||
|
</span>-->
|
||||||
|
</p>
|
||||||
|
<span style="display: inline-block; width: 120px;">Negligible : <strong>{{$vulnerabilitiesCount.Negligible}}</strong></span>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="graph">
|
||||||
|
{{range .SortVulnerabilities}}
|
||||||
|
<a class="node {{.Severity}}" href="#{{ .Name }}">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="popup">
|
||||||
|
<div><strong>{{.Name}}</strong></div>
|
||||||
|
<div>{{.Severity}}</div>
|
||||||
|
<!--<div>{{.IntroduceBy}}</div>-->
|
||||||
|
<!--<div>{{.Description}}</div>-->
|
||||||
|
<div>{{.Layer}}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="report">
|
||||||
|
<div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="layers">
|
||||||
|
{{range .SortLayers}}
|
||||||
|
<div id="{{.Name}}" class="layer">
|
||||||
|
<h3 class="layer__title" data-toggle-layer="{{.Name}}">{{.Name}}</h3>
|
||||||
|
<div>{{.Path}}</div>
|
||||||
|
<div class="features">
|
||||||
|
<ul>
|
||||||
|
{{range .Features}}
|
||||||
|
<li class="feature">
|
||||||
|
<div class="feature__title">
|
||||||
|
<strong>{{ .Name }}</strong> <span>{{ .Version }}</span> - <span class="fa fa-{{if .Status}}check-circle{{else}}exclamation-triangle{{end}}" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<ul class="vulnerabilities">
|
||||||
|
{{range .Vulnerabilities}}
|
||||||
|
<li class="vulnerability {{ .Severity }}">
|
||||||
|
<a class="vulnerability__title" name="{{ .Name }}"></a>
|
||||||
|
<strong class="name">{{ .Name }}</strong>
|
||||||
|
<div>{{ .Description }}</div>
|
||||||
|
<a href="{{ .Link }}" target="blank">Link</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const togglers = document.querySelectorAll('[data-toggle-layer]');
|
||||||
|
console.log(togglers);
|
||||||
|
|
||||||
|
for (var i = togglers.length - 1; i >= 0; i--) {
|
||||||
|
togglers[i].onclick = function (e) {
|
||||||
|
e.target.parentNode.classList.toggle('closed');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
32
cmd/clairctl/clair/versions.go
Normal file
32
cmd/clairctl/clair/versions.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Versions() (interface{}, error) {
|
||||||
|
Config()
|
||||||
|
response, err := http.Get(uri + "/versions")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("requesting Clair version: %v", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading Clair version body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionBody interface{}
|
||||||
|
err = json.Unmarshal(body, &versionBody)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshalling Clair version body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionBody, nil
|
||||||
|
}
|
82
cmd/clairctl/cmd/analyse.go
Normal file
82
cmd/clairctl/cmd/analyse.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const analyseTplt = `
|
||||||
|
Image: {{.String}}
|
||||||
|
{{.Layers | len}} layers found
|
||||||
|
{{$ia := .}}
|
||||||
|
{{range .Layers}} ➜ {{with .Layer}}Analysis [{{.|$ia.ShortName}}] found {{.|$ia.CountVulnerabilities}} vulnerabilities.{{end}}
|
||||||
|
{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
var analyseCmd = &cobra.Command{
|
||||||
|
Use: "analyse IMAGE",
|
||||||
|
Short: "Analyse Docker image",
|
||||||
|
Long: `Analyse a Docker image with Clair, against Ubuntu, Red hat and Debian vulnerabilities databases`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Printf("hyperclair: \"analyse\" requires a minimum of 1 argument")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ia := analyse(args[0])
|
||||||
|
|
||||||
|
err := template.Must(template.New("analysis").Parse(analyseTplt)).Execute(os.Stdout, ia)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("rendering analysis: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyse(imageName string) clair.ImageAnalysis {
|
||||||
|
var err error
|
||||||
|
var image docker.Image
|
||||||
|
|
||||||
|
if !docker.IsLocal {
|
||||||
|
image, err = docker.Pull(imageName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == xerrors.NotFound {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("pulling image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
image, err = docker.Parse(imageName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("parsing local image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
docker.FromHistory(&image)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("getting local image %q from history: %v", imageName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return docker.Analyse(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(analyseCmd)
|
||||||
|
analyseCmd.Flags().BoolVarP(&docker.IsLocal, "local", "l", false, "Use local images")
|
||||||
|
analyseCmd.Flags().StringP("priority", "p", "Low", "Vulnerabilities priority [Low, Medium, High, Critical]")
|
||||||
|
viper.BindPFlag("clair.priority", analyseCmd.Flags().Lookup("priority"))
|
||||||
|
}
|
39
cmd/clairctl/cmd/health.go
Normal file
39
cmd/clairctl/cmd/health.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const healthTplt = `
|
||||||
|
Clair: {{if .}}✔{{else}}✘{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
type health struct {
|
||||||
|
Clair interface{} `json:"clair"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var healthCmd = &cobra.Command{
|
||||||
|
Use: "health",
|
||||||
|
Short: "Get Health of Hyperclair and underlying services",
|
||||||
|
Long: `Get Health of Hyperclair and underlying services`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
ok := clair.IsHealthy()
|
||||||
|
err := template.Must(template.New("health").Parse(healthTplt)).Execute(os.Stdout, ok)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("rendering the health: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(healthCmd)
|
||||||
|
}
|
124
cmd/clairctl/cmd/login.go
Normal file
124
cmd/clairctl/cmd/login.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type userMapping map[string]user
|
||||||
|
|
||||||
|
var loginCmd = &cobra.Command{
|
||||||
|
Use: "login",
|
||||||
|
Short: "Log in to a Docker registry",
|
||||||
|
Long: `Log in to a Docker registry`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
fmt.Println("Only one argument is allowed")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var users userMapping
|
||||||
|
|
||||||
|
if err := readConfigFile(&users, config.HyperclairConfig()); err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("reading hyperclair file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reg string = docker.DockerHub
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
reg = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var usr user
|
||||||
|
if err := askForUser(&usr); err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("encrypting password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
users[reg] = usr
|
||||||
|
|
||||||
|
if err := writeConfigFile(users, config.HyperclairConfig()); err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("indenting login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logged, err := docker.Login(reg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("log in: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logged {
|
||||||
|
fmt.Println("Unauthorized: Wrong login/password, please try again")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Login Successful")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigFile(users *userMapping, configFile string) error {
|
||||||
|
if _, err := os.Stat(configFile); err == nil {
|
||||||
|
f, err := ioutil.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(f, &users); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*users = userMapping{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func askForUser(usr *user) error {
|
||||||
|
fmt.Print("Username: ")
|
||||||
|
fmt.Scan(&usr.Username)
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
pwd, err := terminal.ReadPassword(1)
|
||||||
|
fmt.Println(" ")
|
||||||
|
encryptedPwd, err := bcrypt.GenerateFromPassword(pwd, 5)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
usr.Password = string(encryptedPwd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfigFile(users userMapping, configFile string) error {
|
||||||
|
s, err := xstrings.ToIndentJSON(users)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(configFile, s, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(loginCmd)
|
||||||
|
}
|
59
cmd/clairctl/cmd/login_test.go
Normal file
59
cmd/clairctl/cmd/login_test.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var loginData = []struct {
|
||||||
|
in string
|
||||||
|
out int
|
||||||
|
}{
|
||||||
|
{"", 0},
|
||||||
|
{`{
|
||||||
|
"docker.io": {
|
||||||
|
"Username": "johndoe",
|
||||||
|
"Password": "$2a$05$Qe4TTO8HMmOht"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfigFile(t *testing.T) {
|
||||||
|
for _, ld := range loginData {
|
||||||
|
|
||||||
|
tmpfile := test.CreateTmpConfigFile(ld.in)
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
|
||||||
|
var users userMapping
|
||||||
|
if err := readConfigFile(&users, tmpfile); err != nil {
|
||||||
|
t.Errorf("readConfigFile(&users,%q) failed => %v", tmpfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(users); l != ld.out {
|
||||||
|
t.Errorf("readConfigFile(&users,%q) => %v users, want %v", tmpfile, l, ld.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteConfigFile(t *testing.T) {
|
||||||
|
users := userMapping{}
|
||||||
|
users["docker.io"] = user{Username: "johndoe", Password: "$2a$05$Qe4TTO8HMmOht"}
|
||||||
|
tmpfile := test.CreateTmpConfigFile("")
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
|
||||||
|
if err := writeConfigFile(users, tmpfile); err != nil {
|
||||||
|
t.Errorf("writeConfigFile(users,%q) failed => %v", tmpfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
users = userMapping{}
|
||||||
|
if err := readConfigFile(&users, tmpfile); err != nil {
|
||||||
|
t.Errorf("after writing: readConfigFile(&users,%q) failed => %v", tmpfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(users); l != 1 {
|
||||||
|
t.Errorf("after writing: readConfigFile(&users,%q) => %v users, want %v", tmpfile, l, 1)
|
||||||
|
}
|
||||||
|
}
|
55
cmd/clairctl/cmd/logout.go
Normal file
55
cmd/clairctl/cmd/logout.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logoutCmd = &cobra.Command{
|
||||||
|
Use: "logout",
|
||||||
|
Short: "Log out from a Docker registry",
|
||||||
|
Long: `Log out from a Docker registry`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
fmt.Println("Only one argument is allowed")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var reg string = docker.DockerHub
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
reg = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.HyperclairConfig()); err == nil {
|
||||||
|
var users userMapping
|
||||||
|
|
||||||
|
if err := readConfigFile(&users, config.HyperclairConfig()); err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("reading hyperclair file: %v", err)
|
||||||
|
}
|
||||||
|
if _, present := users[reg]; present {
|
||||||
|
delete(users, reg)
|
||||||
|
|
||||||
|
if err := writeConfigFile(users, config.HyperclairConfig()); err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("indenting login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Log out successful")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("You are not logged in")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(logoutCmd)
|
||||||
|
}
|
63
cmd/clairctl/cmd/pull.go
Normal file
63
cmd/clairctl/cmd/pull.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
|
||||||
|
//
|
||||||
|
// 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pullTplt = `
|
||||||
|
Image: {{.String}}
|
||||||
|
{{.FsLayers | len}} layers found
|
||||||
|
{{range .FsLayers}} ➜ {{.BlobSum}}
|
||||||
|
{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// pingCmd represents the ping command
|
||||||
|
var pullCmd = &cobra.Command{
|
||||||
|
Use: "pull IMAGE",
|
||||||
|
Short: "Pull Docker image information",
|
||||||
|
Long: `Pull image information from Docker Hub or Registry`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
//TODO how to use args with viper
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Printf("hyperclair: \"pull\" requires a minimum of 1 argument\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
im := args[0]
|
||||||
|
image, err := docker.Pull(im)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.ServiceUnavailable)
|
||||||
|
logrus.Fatalf("pulling image %v: %v", args[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = template.Must(template.New("pull").Parse(pullTplt)).Execute(os.Stdout, image)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("rendering image: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(pullCmd)
|
||||||
|
}
|
87
cmd/clairctl/cmd/push.go
Normal file
87
cmd/clairctl/cmd/push.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/server"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pushCmd = &cobra.Command{
|
||||||
|
Use: "push IMAGE",
|
||||||
|
Short: "Push Docker image to Clair",
|
||||||
|
Long: `Upload a Docker image to Clair for further analysis`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Printf("hyperclair: \"push\" requires a minimum of 1 argument\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
startLocalServer()
|
||||||
|
|
||||||
|
imageName := args[0]
|
||||||
|
|
||||||
|
var image docker.Image
|
||||||
|
if !docker.IsLocal {
|
||||||
|
var err error
|
||||||
|
image, err = docker.Pull(imageName)
|
||||||
|
if err != nil {
|
||||||
|
if err == xerrors.NotFound {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("pulling image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
image, err = docker.Parse(imageName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("parsing local image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
err = docker.Prepare(&image)
|
||||||
|
logrus.Debugf("prepared image layers: %d", len(image.FsLayers))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("preparing local image %q from history: %v", imageName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("Pushing Image")
|
||||||
|
if err := docker.Push(image); err != nil {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("pushing image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%v has been pushed to Clair\n", imageName)
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(pushCmd)
|
||||||
|
pushCmd.Flags().BoolVarP(&docker.IsLocal, "local", "l", false, "Use local images")
|
||||||
|
}
|
||||||
|
|
||||||
|
//StartLocalServer start the hyperclair local server needed for reverse proxy and file server
|
||||||
|
func startLocalServer() {
|
||||||
|
sURL, err := config.LocalServerIP()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("retrieving internal server IP: %v", err)
|
||||||
|
}
|
||||||
|
err = server.Serve(sURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("starting local server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
88
cmd/clairctl/cmd/report.go
Normal file
88
cmd/clairctl/cmd/report.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reportCmd = &cobra.Command{
|
||||||
|
Use: "report IMAGE",
|
||||||
|
Short: "Generate Docker Image vulnerabilities report",
|
||||||
|
Long: `Generate Docker Image vulnerabilities report as HTML or JSON`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Printf("hyperclair: \"report\" requires a minimum of 1 argument")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
analyses := analyse(args[0])
|
||||||
|
imageName := strings.Replace(analyses.ImageName, "/", "-", -1) + "-" + analyses.Tag
|
||||||
|
switch clair.Report.Format {
|
||||||
|
case "html":
|
||||||
|
html, err := clair.ReportAsHTML(analyses)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("generating HTML report: %v", err)
|
||||||
|
}
|
||||||
|
err = saveReport(imageName, string(html))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("saving HTML report: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "json":
|
||||||
|
json, err := xstrings.ToIndentJSON(analyses)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("indenting JSON: %v", err)
|
||||||
|
}
|
||||||
|
err = saveReport(imageName, string(json))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("saving JSON report: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unsupported Report format: %v", clair.Report.Format)
|
||||||
|
logrus.Fatalf("Unsupported Report format: %v", clair.Report.Format)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveReport(name string, content string) error {
|
||||||
|
path := viper.GetString("clair.report.path") + "/" + clair.Report.Format
|
||||||
|
if err := os.MkdirAll(path, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reportsName := fmt.Sprintf("%v/analysis-%v.%v", path, name, strings.ToLower(clair.Report.Format))
|
||||||
|
f, err := os.Create(reportsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating report file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString(content)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing report file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v report at %v\n", strings.ToUpper(clair.Report.Format), reportsName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(reportCmd)
|
||||||
|
reportCmd.Flags().BoolVarP(&docker.IsLocal, "local", "l", false, "Use local images")
|
||||||
|
reportCmd.Flags().StringP("format", "f", "html", "Format for Report [html,json]")
|
||||||
|
viper.BindPFlag("clair.report.format", reportCmd.Flags().Lookup("format"))
|
||||||
|
}
|
56
cmd/clairctl/cmd/root.go
Normal file
56
cmd/clairctl/cmd/root.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
|
||||||
|
//
|
||||||
|
// 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cfgFile string
|
||||||
|
var logLevel string
|
||||||
|
|
||||||
|
// RootCmd represents the base command when called without any subcommands
|
||||||
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "hyperclair",
|
||||||
|
Short: "Analyse your docker image with Clair, directly from your registry.",
|
||||||
|
Long: ``,
|
||||||
|
// Uncomment the following line if your bare application
|
||||||
|
// has an action associated with it:
|
||||||
|
// Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
if err := RootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.hyperclair.yml)")
|
||||||
|
RootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "log level [Panic,Fatal,Error,Warn,Info,Debug]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
config.Init(cfgFile, logLevel)
|
||||||
|
}
|
37
cmd/clairctl/cmd/version.go
Normal file
37
cmd/clairctl/cmd/version.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const versionTplt = `
|
||||||
|
Hyperclair version {{.}}
|
||||||
|
`
|
||||||
|
|
||||||
|
var version string
|
||||||
|
|
||||||
|
var templ = template.Must(template.New("versions").Parse(versionTplt))
|
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Get Versions of Hyperclair and underlying services",
|
||||||
|
Long: `Get Versions of Hyperclair and underlying services`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
err := templ.Execute(os.Stdout, version)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("rendering the version: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(versionCmd)
|
||||||
|
}
|
216
cmd/clairctl/config/config.go
Normal file
216
cmd/clairctl/config/config.go
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type r struct {
|
||||||
|
Path, Format string
|
||||||
|
}
|
||||||
|
type c struct {
|
||||||
|
URI, Priority string
|
||||||
|
Port, HealthPort int
|
||||||
|
Report r
|
||||||
|
}
|
||||||
|
type a struct {
|
||||||
|
InsecureSkipVerify bool
|
||||||
|
}
|
||||||
|
type h struct {
|
||||||
|
IP, TempFolder string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Clair c
|
||||||
|
Auth a
|
||||||
|
Hyperclair h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init reads in config file and ENV variables if set.
|
||||||
|
func Init(cfgFile string, logLevel string) {
|
||||||
|
lvl := logrus.WarnLevel
|
||||||
|
if logLevel != "" {
|
||||||
|
var err error
|
||||||
|
lvl, err = logrus.ParseLevel(logLevel)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warningf("Wrong Log level %v, defaults to [Warning]", logLevel)
|
||||||
|
lvl = logrus.WarnLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logrus.SetLevel(lvl)
|
||||||
|
|
||||||
|
viper.SetEnvPrefix("hyperclair")
|
||||||
|
viper.SetConfigName("hyperclair") // name of config file (without extension)
|
||||||
|
viper.AddConfigPath("$HOME/.hyperclair") // adding home directory as first search path
|
||||||
|
viper.AddConfigPath(".") // adding home directory as first search path
|
||||||
|
viper.AutomaticEnv() // read in environment variables that match
|
||||||
|
if cfgFile != "" {
|
||||||
|
viper.SetConfigFile(cfgFile)
|
||||||
|
}
|
||||||
|
err := viper.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("No config file used")
|
||||||
|
} else {
|
||||||
|
logrus.Debugf("Using config file: %v", viper.ConfigFileUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.Get("clair.uri") == nil {
|
||||||
|
viper.Set("clair.uri", "http://localhost")
|
||||||
|
}
|
||||||
|
if viper.Get("clair.port") == nil {
|
||||||
|
viper.Set("clair.port", "6060")
|
||||||
|
}
|
||||||
|
if viper.Get("clair.healthPort") == nil {
|
||||||
|
viper.Set("clair.healthPort", "6061")
|
||||||
|
}
|
||||||
|
if viper.Get("clair.priority") == nil {
|
||||||
|
viper.Set("clair.priority", "Low")
|
||||||
|
}
|
||||||
|
if viper.Get("clair.report.path") == nil {
|
||||||
|
viper.Set("clair.report.path", "reports")
|
||||||
|
}
|
||||||
|
if viper.Get("clair.report.format") == nil {
|
||||||
|
viper.Set("clair.report.format", "html")
|
||||||
|
}
|
||||||
|
if viper.Get("auth.insecureSkipVerify") == nil {
|
||||||
|
viper.Set("auth.insecureSkipVerify", "true")
|
||||||
|
}
|
||||||
|
if viper.Get("hyperclair.ip") == nil {
|
||||||
|
viper.Set("hyperclair.ip", "")
|
||||||
|
}
|
||||||
|
if viper.Get("hyperclair.port") == nil {
|
||||||
|
viper.Set("hyperclair.port", 0)
|
||||||
|
}
|
||||||
|
if viper.Get("hyperclair.tempFolder") == nil {
|
||||||
|
viper.Set("hyperclair.tempFolder", "/tmp/hyperclair")
|
||||||
|
}
|
||||||
|
clair.Config()
|
||||||
|
}
|
||||||
|
|
||||||
|
func values() config {
|
||||||
|
return config{
|
||||||
|
Clair: c{
|
||||||
|
URI: viper.GetString("clair.uri"),
|
||||||
|
Port: viper.GetInt("clair.port"),
|
||||||
|
HealthPort: viper.GetInt("clair.healthPort"),
|
||||||
|
Priority: viper.GetString("clair.priority"),
|
||||||
|
Report: r{
|
||||||
|
Path: viper.GetString("clair.report.path"),
|
||||||
|
Format: viper.GetString("clair.report.format"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Auth: a{
|
||||||
|
InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify"),
|
||||||
|
},
|
||||||
|
Hyperclair: h{
|
||||||
|
IP: viper.GetString("hyperclair.ip"),
|
||||||
|
Port: viper.GetInt("hyperclair.port"),
|
||||||
|
TempFolder: viper.GetString("hyperclair.tempFolder"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Print() {
|
||||||
|
cfg := values()
|
||||||
|
cfgBytes, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("marshalling configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Configuration")
|
||||||
|
fmt.Printf("%v", string(cfgBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func HyperclairHome() string {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(xerrors.InternalError)
|
||||||
|
logrus.Fatalf("retrieving user: %v", err)
|
||||||
|
}
|
||||||
|
p := usr.HomeDir + "/.hyperclair"
|
||||||
|
|
||||||
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(p, 0700)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func HyperclairConfig() string {
|
||||||
|
return HyperclairHome() + "/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
//LocalServerIP return the local hyperclair server IP
|
||||||
|
func LocalServerIP() (string, error) {
|
||||||
|
localPort := viper.GetString("hyperclair.port")
|
||||||
|
localIP := viper.GetString("hyperclair.ip")
|
||||||
|
if localIP == "" {
|
||||||
|
logrus.Infoln("retrieving docker0 interface as local IP")
|
||||||
|
var err error
|
||||||
|
localIP, err = Docker0InterfaceIP()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("retrieving docker0 interface ip: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(localIP) + ":" + localPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Docker0InterfaceIP return the docker0 interface ip by running `ip route show | grep docker0 | awk {print $9}`
|
||||||
|
func Docker0InterfaceIP() (string, error) {
|
||||||
|
var localIP bytes.Buffer
|
||||||
|
|
||||||
|
ip := exec.Command("ip", "route", "show")
|
||||||
|
rGrep, wIP := io.Pipe()
|
||||||
|
grep := exec.Command("grep", "docker0")
|
||||||
|
ip.Stdout = wIP
|
||||||
|
grep.Stdin = rGrep
|
||||||
|
awk := exec.Command("awk", "{print $9}")
|
||||||
|
rAwk, wGrep := io.Pipe()
|
||||||
|
grep.Stdout = wGrep
|
||||||
|
awk.Stdin = rAwk
|
||||||
|
awk.Stdout = &localIP
|
||||||
|
err := ip.Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = grep.Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = awk.Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = ip.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = wIP.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = grep.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = wGrep.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = awk.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return localIP.String(), nil
|
||||||
|
}
|
123
cmd/clairctl/config/config_test.go
Normal file
123
cmd/clairctl/config/config_test.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/test"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultValues = `
|
||||||
|
clair:
|
||||||
|
uri: http://localhost
|
||||||
|
priority: Low
|
||||||
|
port: 6060
|
||||||
|
healthport: 6061
|
||||||
|
report:
|
||||||
|
path: reports
|
||||||
|
format: html
|
||||||
|
auth:
|
||||||
|
insecureskipverify: true
|
||||||
|
hyperclair:
|
||||||
|
ip: ""
|
||||||
|
tempfolder: /tmp/hyperclair
|
||||||
|
port: 0
|
||||||
|
`
|
||||||
|
|
||||||
|
const customValues = `
|
||||||
|
clair:
|
||||||
|
uri: http://clair
|
||||||
|
priority: High
|
||||||
|
port: 6061
|
||||||
|
healthport: 6062
|
||||||
|
report:
|
||||||
|
path: reports/test
|
||||||
|
format: json
|
||||||
|
auth:
|
||||||
|
insecureskipverify: false
|
||||||
|
hyperclair:
|
||||||
|
ip: "localhost"
|
||||||
|
tempfolder: /tmp/hyperclair/test
|
||||||
|
port: 64157
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestInitDefault(t *testing.T) {
|
||||||
|
Init("", "INFO")
|
||||||
|
|
||||||
|
cfg := values()
|
||||||
|
|
||||||
|
var expected config
|
||||||
|
err := yaml.Unmarshal([]byte(defaultValues), &expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != expected {
|
||||||
|
t.Error("Default values are not correct")
|
||||||
|
}
|
||||||
|
viper.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitCustomLocal(t *testing.T) {
|
||||||
|
tmpfile := test.CreateConfigFile(customValues, "hyperclair.yml", ".")
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
fmt.Println(tmpfile)
|
||||||
|
Init("", "INFO")
|
||||||
|
|
||||||
|
cfg := values()
|
||||||
|
|
||||||
|
var expected config
|
||||||
|
err := yaml.Unmarshal([]byte(customValues), &expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != expected {
|
||||||
|
t.Error("values are not correct")
|
||||||
|
}
|
||||||
|
viper.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitCustomHome(t *testing.T) {
|
||||||
|
tmpfile := test.CreateConfigFile(customValues, "hyperclair.yml", HyperclairHome())
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
fmt.Println(tmpfile)
|
||||||
|
Init("", "INFO")
|
||||||
|
|
||||||
|
cfg := values()
|
||||||
|
|
||||||
|
var expected config
|
||||||
|
err := yaml.Unmarshal([]byte(customValues), &expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != expected {
|
||||||
|
t.Error("values are not correct")
|
||||||
|
}
|
||||||
|
viper.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitCustom(t *testing.T) {
|
||||||
|
tmpfile := test.CreateConfigFile(customValues, "hyperclair.yml", "/tmp")
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
fmt.Println(tmpfile)
|
||||||
|
Init(tmpfile, "INFO")
|
||||||
|
|
||||||
|
cfg := values()
|
||||||
|
|
||||||
|
var expected config
|
||||||
|
err := yaml.Unmarshal([]byte(customValues), &expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != expected {
|
||||||
|
t.Error("values are not correct")
|
||||||
|
}
|
||||||
|
viper.Reset()
|
||||||
|
}
|
8
cmd/clairctl/contrib/.hyperclair.yml
Normal file
8
cmd/clairctl/contrib/.hyperclair.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
clair:
|
||||||
|
port: 6060
|
||||||
|
healthPort: 6061
|
||||||
|
uri: http://clair
|
||||||
|
priority: Low
|
||||||
|
report:
|
||||||
|
path: ./reports
|
||||||
|
format: html
|
5
cmd/clairctl/contrib/Dockerfile
Normal file
5
cmd/clairctl/contrib/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM golang:latest
|
||||||
|
WORKDIR /go/src/github.com/coreos/clair/cmd/clairctl
|
||||||
|
EXPOSE 9999
|
||||||
|
RUN mkdir -p /data
|
||||||
|
ENTRYPOINT bash
|
17
cmd/clairctl/contrib/README.md
Normal file
17
cmd/clairctl/contrib/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CONTRIBUTION
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
# Running full dev environnement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Running Authentication server, Registry, Clair , Hyperclair-server and Hyperclair-DEV-BOX
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Enter the hyperclair dev box
|
||||||
|
docker exec -ti hyperclair_dev bash
|
||||||
|
|
||||||
|
# Run Any command ex:
|
||||||
|
go run main.go help
|
||||||
|
# Or
|
||||||
|
go run main.go pull registry:5000/wemanity-belgium/ubuntu-git
|
||||||
|
```
|
29
cmd/clairctl/contrib/auth_server/config/auth_config.yml
Normal file
29
cmd/clairctl/contrib/auth_server/config/auth_config.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
server:
|
||||||
|
addr: :5001
|
||||||
|
certificate: /ssl/server.pem
|
||||||
|
key: /ssl/server.key
|
||||||
|
token:
|
||||||
|
issuer: auth_service
|
||||||
|
expiration: 900
|
||||||
|
users:
|
||||||
|
"": {}
|
||||||
|
jgsqware:
|
||||||
|
password: $2y$05$oGKwJ8QJDLBOoTBmC/EQiefIMV1N9Yt9jpX3SqMoRqZRRql6q7yym
|
||||||
|
acl:
|
||||||
|
- match:
|
||||||
|
account: jgsqware
|
||||||
|
actions: ['*']
|
||||||
|
- match:
|
||||||
|
account: /.+/
|
||||||
|
name: ${account}/*
|
||||||
|
actions: ['*']
|
||||||
|
- match:
|
||||||
|
type: registry
|
||||||
|
name: catalog
|
||||||
|
actions: ['*']
|
||||||
|
comment: Anonymous users can get catalog.
|
||||||
|
- match:
|
||||||
|
account: ""
|
||||||
|
name: /.*/
|
||||||
|
actions: [pull]
|
||||||
|
comment: Anonymous users can pull everything.
|
28
cmd/clairctl/contrib/auth_server/ssl/old/server.key
Normal file
28
cmd/clairctl/contrib/auth_server/ssl/old/server.key
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClbJbUWDCY31g5
|
||||||
|
hyFlhCpgXOntcqR72vVRew6vw9ooN7LKjC/ev1AbBD3JYca1R9vd/7D6wI5rYNR0
|
||||||
|
ZtCMxaQLEgvDf166VdrZewkq8UmbVSFwqtBvcZCG7mTapzgap6jD5H/fFoFoSkFa
|
||||||
|
V3dGWlEaS5emtk7fLjXTKO0OpByuO2mmRqAoTsnN4O3tYfrW7ReBHueibVIpjZo9
|
||||||
|
0qyFK3sZ2ewKG98m8OGh1jzOhxM8PZDKs9KjkZ/SJiGwxIQ/Ny+rXci7q3cshQna
|
||||||
|
n7ac2Wf0nG21j7eNk0lSKOkT3unb2+1B2+59mEtLauoYs1VcEmM55RnVdawvCmIy
|
||||||
|
LRowuLlDAgMBAAECggEAP3ELz1gbGyXcwgNPDY3Iarh3hncHGfD5UExvb30fN3lU
|
||||||
|
+lUVLsoUQKg5wffbqz5p1hPvndsnQ4sZL6MWrEZICW7cUBeTDsdKbUnAVFXBMu9N
|
||||||
|
KdZ4paTaFsVqrGihHafbE3WYjMgmzQZdVfZhafvNStZezLLyQKmKPvddItZRoYfN
|
||||||
|
sc+iFpT94hPp9Hjs9ClLQv/w9Xt8lVgD1FUh6yAlLUAn77HzbZuyC2nF4gbD2LiS
|
||||||
|
4G+xHcH77FyAU5W6BRv1DqNsuu0ksX/93GiYx0EebzT/IXa7xc0mYE0758EXk72y
|
||||||
|
yoznglkPkSOyyhcuI75FKMyYdQGKpyvw+y4aEv5JwQKBgQDTAaQ827Tpn/aMhP7L
|
||||||
|
jngFgTdfeq/7Q3eZjGgtr5RFnen6YS6WzWigvh5/70ASDziFd4fyd0P41/MjPkO6
|
||||||
|
FTFWisRCpW14+mSTUSDmgTQfsONy1Xr2ib4v4CX2NEy+nUsvpdl72dwZAG/fSu3K
|
||||||
|
MfkVksd5Z56WJ4wxKrB4riHukQKBgQDIsren8ljtxrLepMHvaNLx5Gl1EtrgX3gy
|
||||||
|
zTuUM2CSQktwBYNsp68AloOi6/tuH8t1xcBc8ywgCZ2WXzYdRL/koXsd2IpOTsLd
|
||||||
|
m/zGILgRPVow70yoxKxqxW8YYuQ1gLeAOshj8IHGGfnXTvvpNQNvrnja0NzavjFU
|
||||||
|
tR3aZQb8kwKBgQCOqNx2vQCKt7kEdmKiE1e4OQ3MAvH6SjoRWWmSAdSYYNSxkITk
|
||||||
|
NkpX61JJouNJknrfWdpTJymQk8hx+oXlyLBL15Qrjxb9pSTcqQw6a/5msryEhisV
|
||||||
|
hjlMuxpPZDrC4SvVMidhYgE58h6w9ELi4niKimtM/K6uzFwvXbJkVS7h0QKBgErT
|
||||||
|
Zum0zzcHZ9TedHfACzWoRTEi8HvK3FOEdPwSE6U0FlATniY6dmKvuzBY7wrly8OD
|
||||||
|
EO8WspLXQuu3X8OVyD2DfxVnkFkVwE1DRQDRXg7/YsrvzRL3EJlWNs9Ov2q7LK8g
|
||||||
|
O2oXVyr2sFF33y/ZVgijceeTC2R6mIXOaOzt0acFAoGASB7aF8PTT7YzFCGK/x2O
|
||||||
|
kg4GLJJSlDyhAZzQqe5LBZB+RhkoHZjdQHcMW84iHp8CsFqb3/D8X+5FsDkwBSMP
|
||||||
|
bN1fCFE03BsqubtKhI9kMz5hP1OhxlMZdMxRscbdRZqo57f3imtXg6laOktYyPOy
|
||||||
|
uOzr/Cxm5YUQqyAJ/S4zVuc=
|
||||||
|
-----END PRIVATE KEY-----
|
21
cmd/clairctl/contrib/auth_server/ssl/old/server.pem
Normal file
21
cmd/clairctl/contrib/auth_server/ssl/old/server.pem
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDXTCCAkWgAwIBAgIJAOMN706JOuJOMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||||
|
aWRnaXRzIFB0eSBMdGQwHhcNMTYwMTI4MTcwNjE4WhcNMTcwMTI3MTcwNjE4WjBF
|
||||||
|
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||||
|
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||||
|
CgKCAQEApWyW1FgwmN9YOYchZYQqYFzp7XKke9r1UXsOr8PaKDeyyowv3r9QGwQ9
|
||||||
|
yWHGtUfb3f+w+sCOa2DUdGbQjMWkCxILw39eulXa2XsJKvFJm1UhcKrQb3GQhu5k
|
||||||
|
2qc4Gqeow+R/3xaBaEpBWld3RlpRGkuXprZO3y410yjtDqQcrjtppkagKE7JzeDt
|
||||||
|
7WH61u0XgR7nom1SKY2aPdKshSt7GdnsChvfJvDhodY8zocTPD2QyrPSo5Gf0iYh
|
||||||
|
sMSEPzcvq13Iu6t3LIUJ2p+2nNln9JxttY+3jZNJUijpE97p29vtQdvufZhLS2rq
|
||||||
|
GLNVXBJjOeUZ1XWsLwpiMi0aMLi5QwIDAQABo1AwTjAdBgNVHQ4EFgQUWCDpNrvl
|
||||||
|
IPntyV7Y4uyoAq+aPiQwHwYDVR0jBBgwFoAUWCDpNrvlIPntyV7Y4uyoAq+aPiQw
|
||||||
|
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAROOTzaxkx9YdvnAEj8Pt
|
||||||
|
Ej1FgiN1ogxrL4RMwyXs5cAdHi0G71En9onB/8CaWXVrrJbu6DosNn2RQmNapxPG
|
||||||
|
CkT7QfuYVyZF5jtigsxc+W7aLzASLZHHRA0FcgxUAlEUVaxD3xs6U2jMMntp53ij
|
||||||
|
kOWmalMi5qOBps8PCD9sd9MDejLFihPAIz15l3TgVkbRvtcUlfmMio5AJYzjbm4/
|
||||||
|
0c8brR9tOp3qapeT78AhOmsF7zOVygd/BRIBG+Ynzo2DudBUs/j/4VOt9D9XO4I7
|
||||||
|
e3UaqN2OMcL5RYZ5cHemAAy9jjq9/NAYUyLLP0DiCe6OY7SKsDlGfkYVLpZMbUth
|
||||||
|
9w==
|
||||||
|
-----END CERTIFICATE-----
|
28
cmd/clairctl/contrib/auth_server/ssl/server.key
Normal file
28
cmd/clairctl/contrib/auth_server/ssl/server.key
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP8wLYKQgqxNRw
|
||||||
|
Liy4ZuYGHvDfK/NT0CIlvMJuobPt6NIGi6NRISLyR7eZvo6oHTN3i0ElpblzL0Hq
|
||||||
|
JmF87d7o7tqVxO2Fq+faslXrKTgMpDiPj+WxUR3igTy2+DZ4ZP00Y4jwaPYtvwSi
|
||||||
|
sOP2YeM6r+3sSETj5XonIr+/U3mQYkU9GgFQIbPmJnyPb+WaS0fBZ686zeIKvY3Y
|
||||||
|
+enSCKOjss1QFyTb0TmmwQUcTBCiEXV10FSgzV383ghj+Fq9+nKo3cwooHNYwU+a
|
||||||
|
/6gpI1GGWR074J7PGeBId4DNItcpR1x58BGmgaVHabsEW+9RdMNnh17QFk3Wt2eT
|
||||||
|
S5Knl4UTAgMBAAECggEAcmSoZ+kKiRyGEMAV8csJNszGjL5MuQqB/mh8PQfPR00Q
|
||||||
|
XHFsgjDMXKN/KKBfMbP+oACG8gLcpbSVeg1rC6J/QXxD2qfeUe5jOTdpdFfUcX/V
|
||||||
|
bYQnQwfwfK3DjJO2wzwq3irzJe1Xn4q5LhZJETyAF8S4CYcn/oY6UFUZTlLJSNcH
|
||||||
|
chQOFWvjk13DBjGAmZmjwWKxHoZsKs0ioHtShgONpPM8TZU6SmtJxdFD2pBNp+ba
|
||||||
|
Lj5IQUYWrfCudBlzqvpXmqBlZe1J1MG+FafvAKx2CFbYkVObjRa/5DtQs99qac8y
|
||||||
|
rhn8uloK9gljiszwwUVq/ImrUICP+20rHW+kLfHeYQKBgQD9U2XqXz0d2asD72wS
|
||||||
|
+6yhxY4KZ3TD6W3GgfADC/kTfY+pME7KAXr//7paJJP/GtOxsLGRDHV347c3o3js
|
||||||
|
OGlFWuUSsuJxGq4SwKuo9eRVbOMEXiVlgCuUL5HAk2co1MbKVhSJ5RGbrp6785JO
|
||||||
|
JJcuNUTlaUsgQExEsIFJmZpbdQKBgQDSJPwl3uZIg0GC4QbQTAG1ueiQ9oPJ9kyz
|
||||||
|
cjT31ar9L6VrLwY/MMHYKgBD5JLxkq8qL1h9ii/LJKjX7rX3yttu/qtTMO4Y82CP
|
||||||
|
XnmR5kbODUUfiirQjTQFS3YP390nAewLwRgYPcvpyNIWA6Im6UdFJECLOTUBeiYg
|
||||||
|
VumEhSe1ZwKBgAEj6faHHThQLYPkBQGE3n8P65bCZnUnTNYy6Yip+iILU6U4UXJ5
|
||||||
|
VTtnxEf5mCzyyvcmy3XSr4itnrqCYt31Vwv338YYxgoqS5RMB7nH+ZIk3lS7s8Fk
|
||||||
|
NU4CdM6AG1vEsWxhvM/uFwkzXQWNkCAH7CJKHRhHRA5OG8nHXZ2eMmKtAoGBAJ0J
|
||||||
|
1IA8fVys8bTrkprwYcq6/ifugHfZnmHvM9QNEXWZOIXLo2BvgDyYzo/t7T2nv0zI
|
||||||
|
Ctnt/V9SqvaKxeNB7g+ZMtC9XQC6R2t8T18PddQfqIs0RmCJVNmsFbMxOOQglJQI
|
||||||
|
HYhoDc1MLGsVFgT8CS2LNMyV2J2c+YbrTCCjHRR7AoGAICzoSClfvjmRg+4TP9/d
|
||||||
|
rixJF1UX77TnEhcHaFNBDnmSEX0K4rUr1o6GVZCwI+urL7ZmziDdmTDdbXWjqviJ
|
||||||
|
73COPw798Ox50VoVWssMGZQkXfbkk2yilLbok08ohlvVhzpiyecgbxAe4C6KRWWg
|
||||||
|
WEALyN3lILlyj1cYknRJ7gk=
|
||||||
|
-----END PRIVATE KEY-----
|
21
cmd/clairctl/contrib/auth_server/ssl/server.pem
Normal file
21
cmd/clairctl/contrib/auth_server/ssl/server.pem
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDezCCAmOgAwIBAgIJAJXlshcLjIlpMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||||
|
aWRnaXRzIFB0eSBMdGQxDTALBgNVBAMMBGF1dGgwHhcNMTYwMTI5MDcyMTE3WhcN
|
||||||
|
MTcwMTI4MDcyMTE3WjBUMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0
|
||||||
|
ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ0wCwYDVQQDDARh
|
||||||
|
dXRoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz/MC2CkIKsTUcC4s
|
||||||
|
uGbmBh7w3yvzU9AiJbzCbqGz7ejSBoujUSEi8ke3mb6OqB0zd4tBJaW5cy9B6iZh
|
||||||
|
fO3e6O7alcTthavn2rJV6yk4DKQ4j4/lsVEd4oE8tvg2eGT9NGOI8Gj2Lb8EorDj
|
||||||
|
9mHjOq/t7EhE4+V6JyK/v1N5kGJFPRoBUCGz5iZ8j2/lmktHwWevOs3iCr2N2Pnp
|
||||||
|
0gijo7LNUBck29E5psEFHEwQohF1ddBUoM1d/N4IY/havfpyqN3MKKBzWMFPmv+o
|
||||||
|
KSNRhlkdO+CezxngSHeAzSLXKUdcefARpoGlR2m7BFvvUXTDZ4de0BZN1rdnk0uS
|
||||||
|
p5eFEwIDAQABo1AwTjAdBgNVHQ4EFgQUcCD00y15Rdvwe8VnwoZee+J+6ucwHwYD
|
||||||
|
VR0jBBgwFoAUcCD00y15Rdvwe8VnwoZee+J+6ucwDAYDVR0TBAUwAwEB/zANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAQEAvmlCA49FGGOZS5CWl/NzH3es3N1Gr8MihdAK0vYLxbOM
|
||||||
|
8qA2PirEjJ6sWSeB0ZthVpk/dcod68r4dpFh7hpypvaEerFbpr+eWa9nf/KVJ/ft
|
||||||
|
ClLw+iWZpjEjmtSbSg/XIfraWfvwQp9XNMcmIeHvovHd4HyyU1Ulx6aE31wnZ6SJ
|
||||||
|
UKhTPgft0DRsmvFMc683jjeUg/Ik/XknnCiSyfVvwv7UEUs7sH85mE0p4giJxhEv
|
||||||
|
7MdGlQkob+58BpzsErjoj+RpZSljna98NpwBZUfbxkYE2KzU0oqPC0zQ8KawPtw1
|
||||||
|
OB9O45KN2mJ9dPIAbezQHolrTQ7V+49/nhTghS/T3Q==
|
||||||
|
-----END CERTIFICATE-----
|
73
cmd/clairctl/contrib/config/clair.yml
Normal file
73
cmd/clairctl/contrib/config/clair.yml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# 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.
|
||||||
|
---
|
||||||
|
database:
|
||||||
|
# PostgreSQL Connection string
|
||||||
|
# http://www.postgresql.org/docs/9.4/static/libpq-connect.html
|
||||||
|
source: postgresql://postgres:5432?sslmode=disable&user=postgres&password=root
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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/coreos/etcd-ca
|
||||||
|
# https://github.com/cloudflare/cfssl
|
||||||
|
servername:
|
||||||
|
cafile:
|
||||||
|
keyfile:
|
||||||
|
certfile:
|
89
cmd/clairctl/contrib/docker-compose.yml
Normal file
89
cmd/clairctl/contrib/docker-compose.yml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
auth:
|
||||||
|
image: cesanta/docker_auth:stable
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
volumes:
|
||||||
|
- ./auth_server/config:/config:ro
|
||||||
|
- ./auth_server/ssl:/ssl
|
||||||
|
command: --v=2 --alsologtostderr /config/auth_config.yml
|
||||||
|
container_name: "auth"
|
||||||
|
|
||||||
|
registry:
|
||||||
|
image: registry:2.2.1
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
volumes:
|
||||||
|
- ./auth_server/ssl:/ssl
|
||||||
|
- registry-data:/var/lib/registry
|
||||||
|
container_name: "registry"
|
||||||
|
environment:
|
||||||
|
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
|
||||||
|
- REGISTRY_AUTH=token
|
||||||
|
- REGISTRY_AUTH_TOKEN_REALM=https://auth:5001/auth
|
||||||
|
- REGISTRY_AUTH_TOKEN_SERVICE="registry"
|
||||||
|
- REGISTRY_AUTH_TOKEN_ISSUER="auth_service"
|
||||||
|
- REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/ssl/server.pem
|
||||||
|
|
||||||
|
# Uncomment to deploy an insecure registry
|
||||||
|
# registry_insecure:
|
||||||
|
# image: registry:2.2.1
|
||||||
|
# ports:
|
||||||
|
# - 5002:5000
|
||||||
|
# container_name: "registry_insecure"
|
||||||
|
|
||||||
|
|
||||||
|
clair:
|
||||||
|
image: quay.io/coreos/clair:v1.0.0-rc1
|
||||||
|
volumes:
|
||||||
|
- /tmp:/tmp
|
||||||
|
- ./config:/config
|
||||||
|
- clair-data:/var/local
|
||||||
|
ports:
|
||||||
|
- 6060:6060
|
||||||
|
- 6061:6061
|
||||||
|
container_name: "clair"
|
||||||
|
command: --log-level=debug --config=/config/clair.yml
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
container_name: "postgres"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=root
|
||||||
|
|
||||||
|
hyperclair:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- $GOPATH:/go
|
||||||
|
- hyperclair-data:/data
|
||||||
|
ports:
|
||||||
|
- 9999:9999
|
||||||
|
tty: true
|
||||||
|
container_name: "hyperclair"
|
||||||
|
entrypoint: ["go","run","main.go","--log-level","debug","serve"]
|
||||||
|
|
||||||
|
ui:
|
||||||
|
image: jgsqware/registry-ui
|
||||||
|
ports:
|
||||||
|
- 5080:8080
|
||||||
|
environment:
|
||||||
|
- REGISTRYUI_HUB_URI=registry:5000
|
||||||
|
- REGISTRYUI_ACCOUNT_MGMT_ENABLED=true
|
||||||
|
- REGISTRYUI_ACCOUNT_MGMT_CONFIG=./config/auth_config.yml
|
||||||
|
- REGISTRYUI_HYPERCLAIR_REPORT_ENABLED=true
|
||||||
|
# hyperclair_dev:
|
||||||
|
# build: .
|
||||||
|
# volumes:
|
||||||
|
# - $GOPATH:/go
|
||||||
|
# tty: true
|
||||||
|
# container_name: "hyperclair_dev"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
clair-data:
|
||||||
|
driver: local
|
||||||
|
hyperclair-data:
|
||||||
|
driver: local
|
||||||
|
registry-data:
|
||||||
|
driver: local
|
28
cmd/clairctl/database/database.go
Normal file
28
cmd/clairctl/database/database.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var registryMapping map[string]string
|
||||||
|
|
||||||
|
//InsertRegistryMapping insert the pair layerID,RegistryURI
|
||||||
|
func InsertRegistryMapping(layerDigest string, registryURI string) {
|
||||||
|
logrus.Debugf("Saving %s[%s]", layerDigest, registryURI)
|
||||||
|
registryMapping[layerDigest] = registryURI
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetRegistryMapping return the registryURI corresponding to the layerID passed as parameter
|
||||||
|
func GetRegistryMapping(layerDigest string) (string, error) {
|
||||||
|
registryURI, present := registryMapping[layerDigest]
|
||||||
|
if !present {
|
||||||
|
return "", fmt.Errorf("%v mapping not found", layerDigest)
|
||||||
|
}
|
||||||
|
return registryURI, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registryMapping = map[string]string{}
|
||||||
|
}
|
28
cmd/clairctl/database/database_test.go
Normal file
28
cmd/clairctl/database/database_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestInsertRegistryMapping(t *testing.T) {
|
||||||
|
layerID := "sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e"
|
||||||
|
registryURI := "registry:5000"
|
||||||
|
InsertRegistryMapping(layerID, registryURI)
|
||||||
|
|
||||||
|
if r := registryMapping[layerID]; r != registryURI {
|
||||||
|
t.Errorf("InsertRegistryMapping(%q,%q) => %q, want %q", layerID, registryURI, r, registryURI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRegistryMapping(t *testing.T) {
|
||||||
|
layerID := "sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e"
|
||||||
|
registryURI := "registry:5000"
|
||||||
|
InsertRegistryMapping(layerID, registryURI)
|
||||||
|
|
||||||
|
if r, err := GetRegistryMapping(layerID); r != registryURI {
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("InsertRegistryMapping(%q,%q) failed => %v", layerID, registryURI, err)
|
||||||
|
} else {
|
||||||
|
t.Errorf("InsertRegistryMapping(%q,%q) => %q, want %q", layerID, registryURI, r, registryURI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
cmd/clairctl/docker/analyse.go
Normal file
32
cmd/clairctl/docker/analyse.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Analyse return Clair Image analysis
|
||||||
|
func Analyse(image Image) clair.ImageAnalysis {
|
||||||
|
c := len(image.FsLayers)
|
||||||
|
res := []v1.LayerEnvelope{}
|
||||||
|
|
||||||
|
for i := range image.FsLayers {
|
||||||
|
l := image.FsLayers[c-i-1].BlobSum
|
||||||
|
lShort := xstrings.Substr(l, 0, 12)
|
||||||
|
|
||||||
|
if a, err := clair.Analyse(l); err != nil {
|
||||||
|
logrus.Infof("analysing layer [%v] %d/%d: %v", lShort, i+1, c, err)
|
||||||
|
} else {
|
||||||
|
logrus.Infof("analysing layer [%v] %d/%d", lShort, i+1, c)
|
||||||
|
res = append(res, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clair.ImageAnalysis{
|
||||||
|
Registry: xstrings.TrimPrefixSuffix(image.Registry, "http://", "/v2"),
|
||||||
|
ImageName: image.Name,
|
||||||
|
Tag: image.Tag,
|
||||||
|
Layers: res,
|
||||||
|
}
|
||||||
|
}
|
89
cmd/clairctl/docker/auth.go
Normal file
89
cmd/clairctl/docker/auth.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker/httpclient"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authentication struct {
|
||||||
|
Username, Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
var User Authentication
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
Value string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tok token) String() string {
|
||||||
|
return tok.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
//BearerAuthParams parse Bearer Token on Www-Authenticate header
|
||||||
|
func BearerAuthParams(r *http.Response) map[string]string {
|
||||||
|
s := strings.Fields(r.Header.Get("Www-Authenticate"))
|
||||||
|
if len(s) != 2 || s[0] != "Bearer" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := map[string]string{}
|
||||||
|
|
||||||
|
for _, kv := range strings.Split(s[1], ",") {
|
||||||
|
parts := strings.Split(kv, "=")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthenticateResponse(dockerResponse *http.Response, request *http.Request) error {
|
||||||
|
bearerToken := BearerAuthParams(dockerResponse)
|
||||||
|
url := bearerToken["realm"] + "?service=" + bearerToken["service"]
|
||||||
|
if bearerToken["scope"] != "" {
|
||||||
|
url += "&scope=" + bearerToken["scope"]
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(User.Username, User.Password)
|
||||||
|
|
||||||
|
response, err := httpclient.Get().Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode == http.StatusUnauthorized {
|
||||||
|
return xerrors.Unauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("authentication server response: %v - %v", response.StatusCode, response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tok token
|
||||||
|
err = json.Unmarshal(body, &tok)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Authorization", "Bearer "+tok.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
24
cmd/clairctl/docker/httpclient/httpclient.go
Normal file
24
cmd/clairctl/docker/httpclient/httpclient.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client *http.Client
|
||||||
|
|
||||||
|
//Get create a http.Client with Transport configuration
|
||||||
|
func Get() *http.Client {
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify")},
|
||||||
|
DisableCompression: true,
|
||||||
|
}
|
||||||
|
client = &http.Client{Transport: tr}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
108
cmd/clairctl/docker/image.go
Normal file
108
cmd/clairctl/docker/image.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Image represent Image Manifest from Docker image, including the registry URL
|
||||||
|
type Image struct {
|
||||||
|
Name string
|
||||||
|
Tag string
|
||||||
|
Registry string
|
||||||
|
FsLayers []Layer
|
||||||
|
}
|
||||||
|
|
||||||
|
//Layer represent the digest of a image layer
|
||||||
|
type Layer struct {
|
||||||
|
BlobSum string
|
||||||
|
History string
|
||||||
|
}
|
||||||
|
|
||||||
|
const dockerImageRegex = "^(?:([^/]+)/)?(?:([^/]+)/)?([^@:/]+)(?:[@:](.+))?"
|
||||||
|
const DockerHub = "registry-1.docker.io"
|
||||||
|
const hubURI = "https://" + DockerHub + "/v2"
|
||||||
|
|
||||||
|
var IsLocal = false
|
||||||
|
|
||||||
|
func TmpLocal() string {
|
||||||
|
return viper.GetString("hyperclair.tempFolder")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse is used to parse a docker image command
|
||||||
|
//
|
||||||
|
//Example:
|
||||||
|
//"register.com:5080/wemanity-belgium/alpine"
|
||||||
|
//"register.com:5080/wemanity-belgium/alpine:latest"
|
||||||
|
//"register.com:5080/alpine"
|
||||||
|
//"register.com/wemanity-belgium/alpine"
|
||||||
|
//"register.com/alpine"
|
||||||
|
//"register.com/wemanity-belgium/alpine:latest"
|
||||||
|
//"alpine"
|
||||||
|
//"wemanity-belgium/alpine"
|
||||||
|
//"wemanity-belgium/alpine:latest"
|
||||||
|
func Parse(image string) (Image, error) {
|
||||||
|
imageRegex := regexp.MustCompile(dockerImageRegex)
|
||||||
|
|
||||||
|
if imageRegex.MatchString(image) == false {
|
||||||
|
return Image{}, fmt.Errorf("cannot parse image name: %v", image)
|
||||||
|
}
|
||||||
|
groups := imageRegex.FindStringSubmatch(image)
|
||||||
|
|
||||||
|
registry, repository, name, tag := groups[1], groups[2], groups[3], groups[4]
|
||||||
|
|
||||||
|
if tag == "" {
|
||||||
|
tag = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
if repository == "" && !strings.ContainsAny(registry, ":.") {
|
||||||
|
repository, registry = registry, hubURI //Regex problem, if no registry in url, regex parse repository as registry, so need to invert it
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//FIXME We need to move to https. <error: tls: oversized record received with length 20527>
|
||||||
|
//Maybe using a `insecure-registry` flag in configuration
|
||||||
|
if strings.Contains(registry, "docker") {
|
||||||
|
registry = "https://" + registry + "/v2"
|
||||||
|
|
||||||
|
} else {
|
||||||
|
registry = "http://" + registry + "/v2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if repository != "" {
|
||||||
|
name = repository + "/" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(registry, "docker.io") && repository == "" {
|
||||||
|
return Image{}, xerrors.ErrDisallowed
|
||||||
|
}
|
||||||
|
|
||||||
|
return Image{
|
||||||
|
Registry: registry,
|
||||||
|
Name: name,
|
||||||
|
Tag: tag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlobsURI run Blobs URI as <registry>/<imageName>/blobs/<digest>
|
||||||
|
// eg: "http://registry:5000/v2/jgsqware/ubuntu-git/blobs/sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e"
|
||||||
|
func (image Image) BlobsURI(digest string) string {
|
||||||
|
return strings.Join([]string{image.Registry, image.Name, "blobs", digest}, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image Image) String() string {
|
||||||
|
return image.Registry + "/" + image.Name + ":" + image.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image Image) AsJSON() (string, error) {
|
||||||
|
b, err := json.Marshal(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot marshal image: %v", err)
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
78
cmd/clairctl/docker/image_test.go
Normal file
78
cmd/clairctl/docker/image_test.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var imageNameTests = []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"jgsqware/ubuntu-git", hubURI + "/jgsqware/ubuntu-git:latest"},
|
||||||
|
{"wemanity-belgium/registry-backup", hubURI + "/wemanity-belgium/registry-backup:latest"},
|
||||||
|
{"wemanity-belgium/alpine:latest", hubURI + "/wemanity-belgium/alpine:latest"},
|
||||||
|
{"register.com/alpine", "http://register.com/v2/alpine:latest"},
|
||||||
|
{"register.com/wemanity-belgium/alpine", "http://register.com/v2/wemanity-belgium/alpine:latest"},
|
||||||
|
{"register.com/wemanity-belgium/alpine:latest", "http://register.com/v2/wemanity-belgium/alpine:latest"},
|
||||||
|
{"register.com:5080/alpine", "http://register.com:5080/v2/alpine:latest"},
|
||||||
|
{"register.com:5080/wemanity-belgium/alpine", "http://register.com:5080/v2/wemanity-belgium/alpine:latest"},
|
||||||
|
{"register.com:5080/wemanity-belgium/alpine:latest", "http://register.com:5080/v2/wemanity-belgium/alpine:latest"},
|
||||||
|
{"registry:5000/google/cadvisor", "http://registry:5000/v2/google/cadvisor:latest"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidImageNameTests = []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"alpine", hubURI + "/alpine:latest"},
|
||||||
|
{"docker.io/golang", hubURI + "/golang:latest"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
for _, imageName := range imageNameTests {
|
||||||
|
image, err := Parse(imageName.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Parse(\"%s\") should be valid: %v", imageName.in, err)
|
||||||
|
}
|
||||||
|
if image.String() != imageName.out {
|
||||||
|
t.Errorf("Parse(\"%s\") => %v, want %v", imageName.in, image, imageName.out)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDisallowed(t *testing.T) {
|
||||||
|
for _, imageName := range invalidImageNameTests {
|
||||||
|
_, err := Parse(imageName.in)
|
||||||
|
if err != xerrors.ErrDisallowed {
|
||||||
|
t.Errorf("Parse(\"%s\") should failed with err \"%v\": %v", imageName.in, xerrors.ErrDisallowed, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMBlobstURI(t *testing.T) {
|
||||||
|
image, err := Parse("localhost:5000/alpine")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := image.BlobsURI("sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e")
|
||||||
|
if result != "http://localhost:5000/v2/alpine/blobs/sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e" {
|
||||||
|
t.Errorf("Is %s, should be http://localhost:5000/v2/alpine/blobs/sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueLayer(t *testing.T) {
|
||||||
|
image := Image{
|
||||||
|
FsLayers: []Layer{Layer{BlobSum: "test1"}, Layer{BlobSum: "test1"}, Layer{BlobSum: "test2"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
image.uniqueLayers()
|
||||||
|
|
||||||
|
if len(image.FsLayers) > 2 {
|
||||||
|
t.Errorf("Layers must be unique: %v", image.FsLayers)
|
||||||
|
}
|
||||||
|
}
|
171
cmd/clairctl/docker/local.go
Normal file
171
cmd/clairctl/docker/local.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Prepare populate image.FSLayers with the layer from manifest coming from `docker save` command. Layer.History will be populated with `docker history` command
|
||||||
|
func Prepare(im *Image) error {
|
||||||
|
imageName := im.Name + ":" + im.Tag
|
||||||
|
logrus.Debugf("preparing %v", imageName)
|
||||||
|
|
||||||
|
path, err := save(imageName)
|
||||||
|
// defer os.RemoveAll(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not save image: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve history.
|
||||||
|
logrus.Infoln("Getting image's history")
|
||||||
|
manifestLayerIDs, err := historyFromManifest(path)
|
||||||
|
|
||||||
|
historyLayerIDs, err := historyFromCommand(imageName)
|
||||||
|
|
||||||
|
if err != nil || (len(manifestLayerIDs) == 0 && len(historyLayerIDs) == 0) {
|
||||||
|
return fmt.Errorf("Could not get image's history: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, l := range manifestLayerIDs {
|
||||||
|
im.FsLayers = append(im.FsLayers, Layer{BlobSum: l, History: historyLayerIDs[i]})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//FromHistory populate image.FSLayers with the layer from `docker history` command
|
||||||
|
func FromHistory(im *Image) error {
|
||||||
|
imageName := im.Name + ":" + im.Tag
|
||||||
|
layerIDs, err := historyFromCommand(imageName)
|
||||||
|
|
||||||
|
if err != nil || len(layerIDs) == 0 {
|
||||||
|
return fmt.Errorf("Could not get image's history: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range layerIDs {
|
||||||
|
im.FsLayers = append(im.FsLayers, Layer{BlobSum: l})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanLocal() error {
|
||||||
|
logrus.Debugln("cleaning temporary local repository")
|
||||||
|
err := os.RemoveAll(TmpLocal())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cleaning temporary local repository: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(imageName string) (string, error) {
|
||||||
|
path := TmpLocal() + "/" + strings.Split(imageName, ":")[0] + "/blobs"
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsExist(err) {
|
||||||
|
err := os.RemoveAll(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.MkdirAll(path, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
logrus.Debugln("docker image to save: ", imageName)
|
||||||
|
logrus.Debugln("saving in: ", path)
|
||||||
|
save := exec.Command("docker", "save", imageName)
|
||||||
|
save.Stderr = &stderr
|
||||||
|
extract := exec.Command("tar", "xf", "-", "-C"+path)
|
||||||
|
extract.Stderr = &stderr
|
||||||
|
pipe, err := extract.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
save.Stdout = pipe
|
||||||
|
|
||||||
|
err = extract.Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New(stderr.String())
|
||||||
|
}
|
||||||
|
err = save.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New(stderr.String())
|
||||||
|
}
|
||||||
|
err = pipe.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = extract.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New(stderr.String())
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func historyFromManifest(path string) ([]string, error) {
|
||||||
|
mf, err := os.Open(path + "/manifest.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer mf.Close()
|
||||||
|
|
||||||
|
// https://github.com/docker/docker/blob/master/image/tarexport/tarexport.go#L17
|
||||||
|
type manifestItem struct {
|
||||||
|
Config string
|
||||||
|
RepoTags []string
|
||||||
|
Layers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest []manifestItem
|
||||||
|
if err = json.NewDecoder(mf).Decode(&manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(manifest) != 1 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var layers []string
|
||||||
|
for _, layer := range manifest[0].Layers {
|
||||||
|
layers = append(layers, strings.TrimSuffix(layer, "/layer.tar"))
|
||||||
|
}
|
||||||
|
return layers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func historyFromCommand(imageName string) ([]string, error) {
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd := exec.Command("docker", "history", "-q", "--no-trunc", imageName)
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, errors.New(stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var layers []string
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
layers = append(layers, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(layers)/2 - 1; i >= 0; i-- {
|
||||||
|
opp := len(layers) - 1 - i
|
||||||
|
layers[i], layers[opp] = layers[opp], layers[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return layers, nil
|
||||||
|
}
|
47
cmd/clairctl/docker/login.go
Normal file
47
cmd/clairctl/docker/login.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker/httpclient"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Pull Image from Registry or Hub depending on image name
|
||||||
|
func Login(registry string) (bool, error) {
|
||||||
|
|
||||||
|
logrus.Info("log in: ", registry)
|
||||||
|
|
||||||
|
if strings.Contains(registry, "docker") {
|
||||||
|
registry = "https://" + registry + "/v2"
|
||||||
|
|
||||||
|
} else {
|
||||||
|
registry = "http://" + registry + "/v2"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := httpclient.Get()
|
||||||
|
request, err := http.NewRequest("GET", registry, nil)
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("log in %v: %v", registry, err)
|
||||||
|
}
|
||||||
|
authorized := response.StatusCode != http.StatusUnauthorized
|
||||||
|
if !authorized {
|
||||||
|
logrus.Info("Unauthorized access")
|
||||||
|
err := AuthenticateResponse(response, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == xerrors.Unauthorized {
|
||||||
|
authorized = false
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
authorized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorized, nil
|
||||||
|
}
|
88
cmd/clairctl/docker/pull.go
Normal file
88
cmd/clairctl/docker/pull.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker/httpclient"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Pull Image from Registry or Hub depending on image name
|
||||||
|
func Pull(imageName string) (Image, error) {
|
||||||
|
image, err := Parse(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return Image{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("pulling image: ", image)
|
||||||
|
|
||||||
|
mURI := fmt.Sprintf("%v/%v/manifests/%v", image.Registry, image.Name, image.Tag)
|
||||||
|
client := httpclient.Get()
|
||||||
|
request, err := http.NewRequest("GET", mURI, nil)
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return Image{}, fmt.Errorf("retrieving manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode == http.StatusUnauthorized {
|
||||||
|
logrus.Info("Pull is Unauthorized")
|
||||||
|
err := AuthenticateResponse(response, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Image{}, fmt.Errorf("authenticating: %v", err)
|
||||||
|
}
|
||||||
|
response, err = client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return Image{}, fmt.Errorf("retrieving manifest: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return Image{}, fmt.Errorf("reading manifest body: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
switch response.StatusCode {
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
return Image{}, xerrors.Unauthorized
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return Image{}, xerrors.NotFound
|
||||||
|
default:
|
||||||
|
return Image{}, fmt.Errorf("%d - %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := image.parseManifest(body); err != nil {
|
||||||
|
return Image{}, fmt.Errorf("parsing manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *Image) parseManifest(body []byte) error {
|
||||||
|
|
||||||
|
err := json.Unmarshal(body, &image)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshalling manifest body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
image.uniqueLayers()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *Image) uniqueLayers() {
|
||||||
|
encountered := map[Layer]bool{}
|
||||||
|
result := []Layer{}
|
||||||
|
|
||||||
|
for index := range image.FsLayers {
|
||||||
|
if encountered[image.FsLayers[index]] != true {
|
||||||
|
encountered[image.FsLayers[index]] = true
|
||||||
|
result = append(result, image.FsLayers[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image.FsLayers = result
|
||||||
|
}
|
68
cmd/clairctl/docker/push.go
Normal file
68
cmd/clairctl/docker/push.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/database"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Push image to Clair for analysis
|
||||||
|
func Push(image Image) error {
|
||||||
|
layerCount := len(image.FsLayers)
|
||||||
|
|
||||||
|
parentID := ""
|
||||||
|
|
||||||
|
if layerCount == 0 {
|
||||||
|
logrus.Warningln("there is no layer to push")
|
||||||
|
}
|
||||||
|
localIP, err := config.LocalServerIP()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hURL := fmt.Sprintf("http://%v/v2", localIP)
|
||||||
|
if IsLocal {
|
||||||
|
hURL += "/local"
|
||||||
|
logrus.Infof("using %v as local url", hURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, layer := range image.FsLayers {
|
||||||
|
lUID := xstrings.Substr(layer.BlobSum, 0, 12)
|
||||||
|
logrus.Infof("Pushing Layer %d/%d [%v]", index+1, layerCount, lUID)
|
||||||
|
|
||||||
|
database.InsertRegistryMapping(layer.BlobSum, image.Registry)
|
||||||
|
payload := v1.LayerEnvelope{Layer: &v1.Layer{
|
||||||
|
Name: layer.BlobSum,
|
||||||
|
Path: image.BlobsURI(layer.BlobSum),
|
||||||
|
ParentName: parentID,
|
||||||
|
Format: "Docker",
|
||||||
|
}}
|
||||||
|
|
||||||
|
//FIXME Update to TLS
|
||||||
|
if IsLocal {
|
||||||
|
payload.Layer.Name = layer.History
|
||||||
|
payload.Layer.Path += "/layer.tar"
|
||||||
|
}
|
||||||
|
payload.Layer.Path = strings.Replace(payload.Layer.Path, image.Registry, hURL, 1)
|
||||||
|
if err := clair.Push(payload); err != nil {
|
||||||
|
logrus.Infof("adding layer %d/%d [%v]: %v", index+1, layerCount, lUID, err)
|
||||||
|
if err != clair.OSNotSupported {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parentID = ""
|
||||||
|
} else {
|
||||||
|
parentID = payload.Layer.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if IsLocal {
|
||||||
|
if err := cleanLocal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
8
cmd/clairctl/hyperclair.yml.default
Normal file
8
cmd/clairctl/hyperclair.yml.default
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
clair:
|
||||||
|
port: 6060
|
||||||
|
healthPort: 6061
|
||||||
|
uri: http://clair
|
||||||
|
priority: Low
|
||||||
|
report:
|
||||||
|
path: ./reports
|
||||||
|
format: html
|
21
cmd/clairctl/main.go
Normal file
21
cmd/clairctl/main.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
|
||||||
|
//
|
||||||
|
// 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 main
|
||||||
|
|
||||||
|
import "github.com/coreos/clair/cmd/clairctl/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
102919
cmd/clairctl/reports/html/analysis-jgsqware-clair-latest.html
Normal file
102919
cmd/clairctl/reports/html/analysis-jgsqware-clair-latest.html
Normal file
File diff suppressed because it is too large
Load Diff
2925
cmd/clairctl/reports/html/report-sample.html
Normal file
2925
cmd/clairctl/reports/html/report-sample.html
Normal file
File diff suppressed because it is too large
Load Diff
1580
cmd/clairctl/reports/json/analysis-jgsqware-ubuntu-git-latest.json
Normal file
1580
cmd/clairctl/reports/json/analysis-jgsqware-ubuntu-git-latest.json
Normal file
File diff suppressed because it is too large
Load Diff
245
cmd/clairctl/server/reverseProxy/reverseProxy.go
Normal file
245
cmd/clairctl/server/reverseProxy/reverseProxy.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
package reverseProxy
|
||||||
|
|
||||||
|
// Modified version of the original golang HTTP reverse proxy handler
|
||||||
|
// And Vars in Gorilla/mux
|
||||||
|
// Added support for Filter functions
|
||||||
|
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/gorilla/context"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/database"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker/httpclient"
|
||||||
|
"github.com/wunderlist/moxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterFunc is a function that is called to process a proxy response
|
||||||
|
// Since it has handle to the response object, it can manipulate the content
|
||||||
|
type FilterFunc func(*http.Request, *http.Response)
|
||||||
|
|
||||||
|
// onExitFlushLoop is a callback set by tests to detect the state of the
|
||||||
|
// flushLoop() goroutine.
|
||||||
|
var onExitFlushLoop func()
|
||||||
|
|
||||||
|
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||||
|
// sends it to another server, proxying the response back to the
|
||||||
|
// client.
|
||||||
|
type ReverseProxy struct {
|
||||||
|
// Director must be a function which modifies
|
||||||
|
// the request into a new request to be sent
|
||||||
|
// using Transport. Its response is then copied
|
||||||
|
// back to the original client unmodified.
|
||||||
|
Director func(*http.Request)
|
||||||
|
|
||||||
|
// Filters must be an array of functions which modify
|
||||||
|
// the response before the body is written
|
||||||
|
Filters []FilterFunc
|
||||||
|
|
||||||
|
// The transport used to perform proxy requests.
|
||||||
|
// If nil, http.DefaultTransport is used.
|
||||||
|
Transport http.RoundTripper
|
||||||
|
|
||||||
|
// FlushInterval specifies the flush interval
|
||||||
|
// to flush to the client while copying the
|
||||||
|
// response body.
|
||||||
|
// If zero, no periodic flushing is done.
|
||||||
|
FlushInterval time.Duration
|
||||||
|
|
||||||
|
// ErrorLog specifies an optional logger for errors
|
||||||
|
// that occur when attempting to proxy the request.
|
||||||
|
// If nil, logging goes to os.Stderr via the log package's
|
||||||
|
// standard logger.
|
||||||
|
ErrorLog *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyHeader(dst, src http.Header) {
|
||||||
|
for k, vv := range src {
|
||||||
|
for _, v := range vv {
|
||||||
|
dst.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||||
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
|
||||||
|
var hopHeaders = []string{
|
||||||
|
"Connection",
|
||||||
|
"Keep-Alive",
|
||||||
|
"Proxy-Authenticate",
|
||||||
|
"Proxy-Authorization",
|
||||||
|
"Te", // canonicalized version of "TE"
|
||||||
|
"Trailers",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"Upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
|
transport := p.Transport
|
||||||
|
if transport == nil {
|
||||||
|
transport = http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
outreq := new(http.Request)
|
||||||
|
*outreq = *req // includes shallow copies of maps, but okay
|
||||||
|
|
||||||
|
context.Set(outreq, "in_req", req)
|
||||||
|
p.Director(outreq)
|
||||||
|
outreq.Proto = "HTTP/1.1"
|
||||||
|
outreq.ProtoMajor = 1
|
||||||
|
outreq.ProtoMinor = 1
|
||||||
|
outreq.Close = false
|
||||||
|
// Remove hop-by-hop headers to the backend. Especially
|
||||||
|
// important is "Connection" because we want a persistent
|
||||||
|
// connection, regardless of what the client sent to us. This
|
||||||
|
// is modifying the same underlying map from req (shallow
|
||||||
|
// copied above) so we only copy it if necessary.
|
||||||
|
copiedHeaders := false
|
||||||
|
for _, h := range hopHeaders {
|
||||||
|
if outreq.Header.Get(h) != "" {
|
||||||
|
if !copiedHeaders {
|
||||||
|
outreq.Header = make(http.Header)
|
||||||
|
copyHeader(outreq.Header, req.Header)
|
||||||
|
copiedHeaders = true
|
||||||
|
}
|
||||||
|
outreq.Header.Del(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||||
|
// If we aren't the first proxy retain prior
|
||||||
|
// X-Forwarded-For information as a comma+space
|
||||||
|
// separated list and fold multiple headers into one.
|
||||||
|
if prior, ok := outreq.Header["X-Forwarded-For"]; ok {
|
||||||
|
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||||
|
}
|
||||||
|
outreq.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := transport.RoundTrip(outreq)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("http: proxy error: %v", err)
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
for _, filterFn := range p.Filters {
|
||||||
|
filterFn(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range hopHeaders {
|
||||||
|
res.Header.Del(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
copyHeader(rw.Header(), res.Header)
|
||||||
|
|
||||||
|
rw.WriteHeader(res.StatusCode)
|
||||||
|
p.copyResponse(rw, res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
|
||||||
|
if p.FlushInterval != 0 {
|
||||||
|
if wf, ok := dst.(writeFlusher); ok {
|
||||||
|
mlw := &maxLatencyWriter{
|
||||||
|
dst: wf,
|
||||||
|
latency: p.FlushInterval,
|
||||||
|
done: make(chan bool),
|
||||||
|
}
|
||||||
|
go mlw.flushLoop()
|
||||||
|
defer mlw.stop()
|
||||||
|
dst = mlw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
io.Copy(dst, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeFlusher interface {
|
||||||
|
io.Writer
|
||||||
|
http.Flusher
|
||||||
|
}
|
||||||
|
|
||||||
|
type maxLatencyWriter struct {
|
||||||
|
dst writeFlusher
|
||||||
|
latency time.Duration
|
||||||
|
|
||||||
|
lk sync.Mutex // protects Write + Flush
|
||||||
|
done chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maxLatencyWriter) Write(p []byte) (int, error) {
|
||||||
|
m.lk.Lock()
|
||||||
|
defer m.lk.Unlock()
|
||||||
|
return m.dst.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maxLatencyWriter) flushLoop() {
|
||||||
|
t := time.NewTicker(m.latency)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.done:
|
||||||
|
if onExitFlushLoop != nil {
|
||||||
|
onExitFlushLoop()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
m.lk.Lock()
|
||||||
|
m.dst.Flush()
|
||||||
|
m.lk.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maxLatencyWriter) stop() { m.done <- true }
|
||||||
|
|
||||||
|
// NewReverseProxy returns a new ReverseProxy that load-balances the proxy requests between multiple hosts defined by the RegistryMapping in the database
|
||||||
|
// It also allows to define a chain of filter functions to process the outgoing response(s)
|
||||||
|
func NewReverseProxy(filters []FilterFunc) *ReverseProxy {
|
||||||
|
director := func(request *http.Request) {
|
||||||
|
|
||||||
|
inr := context.Get(request, "in_req").(*http.Request)
|
||||||
|
host, _ := database.GetRegistryMapping(mux.Vars(inr)["digest"])
|
||||||
|
out, _ := url.Parse(host)
|
||||||
|
request.URL.Scheme = out.Scheme
|
||||||
|
request.URL.Host = out.Host
|
||||||
|
client := httpclient.Get()
|
||||||
|
req, _ := http.NewRequest("HEAD", request.URL.String(), nil)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("response error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
logrus.Info("pull from clair is unauthorized")
|
||||||
|
docker.AuthenticateResponse(resp, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("GET", request.URL.String(), nil)
|
||||||
|
r.Header.Set("Authorization", request.Header.Get("Authorization"))
|
||||||
|
r.Header.Set("Accept-Encoding", request.Header.Get("Accept-Encoding"))
|
||||||
|
*request = *r
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReverseProxy{
|
||||||
|
Transport: moxy.NewTransport(),
|
||||||
|
Director: director,
|
||||||
|
Filters: filters,
|
||||||
|
}
|
||||||
|
}
|
69
cmd/clairctl/server/server.go
Normal file
69
cmd/clairctl/server/server.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairctl/server/reverseProxy"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler func(rw http.ResponseWriter, req *http.Request) error
|
||||||
|
|
||||||
|
var router *mux.Router
|
||||||
|
|
||||||
|
//Serve run a local server with the fileserver and the reverse proxy
|
||||||
|
func Serve(sURL string) error {
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
restrictedFileServer := func(path string) http.Handler {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(path, 0777)
|
||||||
|
}
|
||||||
|
|
||||||
|
fc := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.FileServer(http.Dir(path)).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fc)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.PathPrefix("/v2/local").Handler(http.StripPrefix("/v2/local", restrictedFileServer(docker.TmpLocal()))).Methods("GET")
|
||||||
|
listener, err := net.Listen("tcp", sURL)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("cannot instanciate listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.GetInt("hyperclair.port") == 0 {
|
||||||
|
port := strings.Split(listener.Addr().String(), ":")[1]
|
||||||
|
logrus.Debugf("Update local server port from %q to %q", "0", port)
|
||||||
|
viper.Set("hyperclair.port", port)
|
||||||
|
}
|
||||||
|
logrus.Info("Starting Server on ", listener.Addr())
|
||||||
|
|
||||||
|
if err := http.Serve(listener, nil); err != nil {
|
||||||
|
logrus.Fatalf("local server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
//sleep needed to wait the server start. Maybe use a channel for that
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverseRegistryHandler() http.HandlerFunc {
|
||||||
|
filters := []reverseProxy.FilterFunc{}
|
||||||
|
proxy := reverseProxy.NewReverseProxy(filters)
|
||||||
|
return proxy.ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
router = mux.NewRouter()
|
||||||
|
router.PathPrefix("/v2").Path("/{repository}/{name}/blobs/{digest}").HandlerFunc(reverseRegistryHandler())
|
||||||
|
http.Handle("/", router)
|
||||||
|
}
|
36
cmd/clairctl/test/test.go
Normal file
36
cmd/clairctl/test/test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateTmpConfigFile(content string) string {
|
||||||
|
|
||||||
|
c := []byte(content)
|
||||||
|
tmpfile, err := ioutil.TempFile("", "test-hyperclair")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if content != "" {
|
||||||
|
if _, err := tmpfile.Write(c); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tmpfile.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(tmpfile.Name()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tmpfile.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateConfigFile(content string, name string, path string) string {
|
||||||
|
if err := ioutil.WriteFile(path+"/"+name, []byte(content), 0600); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return path + "/" + name
|
||||||
|
}
|
11
cmd/clairctl/xerrors/xerrors.go
Normal file
11
cmd/clairctl/xerrors/xerrors.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package xerrors
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ServiceUnavailable = errors.New("service is unavailable")
|
||||||
|
Unauthorized = errors.New("unauthorized access")
|
||||||
|
NotFound = errors.New("image not found")
|
||||||
|
InternalError = errors.New("client quit unexpectedly")
|
||||||
|
ErrDisallowed = errors.New("analysing official images is not allowed")
|
||||||
|
)
|
29
cmd/clairctl/xstrings/xstrings.go
Normal file
29
cmd/clairctl/xstrings/xstrings.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package xstrings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Substr extract string of length in s starting at pos
|
||||||
|
func Substr(s string, pos, length int) string {
|
||||||
|
runes := []rune(s)
|
||||||
|
l := pos + length
|
||||||
|
if l > len(runes) {
|
||||||
|
l = len(runes)
|
||||||
|
}
|
||||||
|
return string(runes[pos:l])
|
||||||
|
}
|
||||||
|
|
||||||
|
//TrimPrefixSuffix combine TrimPrefix and TrimSuffix
|
||||||
|
func TrimPrefixSuffix(s string, prefix string, suffix string) string {
|
||||||
|
return strings.TrimSuffix(strings.TrimPrefix(s, prefix), suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToIndentJSON(v interface{}) ([]byte, error) {
|
||||||
|
b, err := json.MarshalIndent(v, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
27
cmd/clairctl/xstrings/xstrings_test.go
Normal file
27
cmd/clairctl/xstrings/xstrings_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package xstrings
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSubstrFromBeginning(t *testing.T) {
|
||||||
|
commitID := "e3ff9321271b0a5cec45ca6e0cdc72b2f376afd2"
|
||||||
|
expected := "e3ff9"
|
||||||
|
if s := Substr(commitID, 0, 5); s != expected {
|
||||||
|
t.Errorf("is %v, expect %v", s, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstrFromCharFive(t *testing.T) {
|
||||||
|
commitID := "e3ff9321271b0a5cec45ca6e0cdc72b2f376afd2"
|
||||||
|
expected := "32127"
|
||||||
|
if s := Substr(commitID, 5, 5); s != expected {
|
||||||
|
t.Errorf("is %v, expect %v", s, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimPrefixSuffix(t *testing.T) {
|
||||||
|
v := "http://registry:5555/v2"
|
||||||
|
e := "registry:5555"
|
||||||
|
if s := TrimPrefixSuffix(v, "http://", "/v2"); s != e {
|
||||||
|
t.Errorf("is %v, expect %v", s, e)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user