copy hyperclair to clairctl and rename package

This commit is contained in:
jgsqware 2016-05-02 18:23:38 +02:00
parent 97347ec44d
commit 3083a891e0
63 changed files with 113109 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/.vscode

19
cmd/clairctl/.travis.yml Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
0.4.0

View 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
View 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")
}

View 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())
}
}

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

View File

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

View 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"))
}

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

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

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

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

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

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

View 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()
}

View File

@ -0,0 +1,8 @@
clair:
port: 6060
healthPort: 6061
uri: http://clair
priority: Low
report:
path: ./reports
format: html

View 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

View 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
```

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

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

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

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

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

View 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:

View 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

View 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{}
}

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

View 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,
}
}

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

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

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

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

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

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

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

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

View 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
View 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()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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,
}
}

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

View 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")
)

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

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