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