migrate hyperclair into cmd/clairclt package
This commit is contained in:
parent
458d59df46
commit
1f3601c6da
21
cmd/clairclt/LICENSE
Normal file
21
cmd/clairclt/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.
|
1
cmd/clairclt/VERSION
Normal file
1
cmd/clairclt/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.4.0
|
39
cmd/clairclt/clair/analyse.go
Normal file
39
cmd/clairclt/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
|
||||||
|
}
|
103
cmd/clairclt/clair/clair.go
Normal file
103
cmd/clairclt/clair/clair.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/xstrings"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var uri string
|
||||||
|
var priority string
|
||||||
|
var healthPort int
|
||||||
|
|
||||||
|
//Report Reporting Config value
|
||||||
|
var Report ReportConfig
|
||||||
|
|
||||||
|
//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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageAnalysis ImageAnalysis) CountVulnerabilities(l v1.Layer) int {
|
||||||
|
count := 0
|
||||||
|
for _, f := range l.Features {
|
||||||
|
count += len(f.Vulnerabilities)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vulnerability struct {
|
||||||
|
Name, Severity, IntroduceBy, Description, Layer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageAnalysis ImageAnalysis) SortVulnerabilities() []Vulnerability {
|
||||||
|
low := []Vulnerability{}
|
||||||
|
medium := []Vulnerability{}
|
||||||
|
high := []Vulnerability{}
|
||||||
|
critical := []Vulnerability{}
|
||||||
|
defcon1 := []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,
|
||||||
|
}
|
||||||
|
switch strings.ToLower(v.Severity) {
|
||||||
|
case "low":
|
||||||
|
low = append(low, nv)
|
||||||
|
case "medium":
|
||||||
|
medium = append(medium, nv)
|
||||||
|
case "high":
|
||||||
|
high = append(high, nv)
|
||||||
|
case "critical":
|
||||||
|
critical = append(critical, nv)
|
||||||
|
case "defcon1":
|
||||||
|
defcon1 = append(defcon1, nv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(defcon1, append(critical, append(high, append(medium, low...)...)...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
31
cmd/clairclt/clair/clair_test.go
Normal file
31
cmd/clairclt/clair/clair_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
26
cmd/clairclt/clair/health.go
Normal file
26
cmd/clairclt/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
|
||||||
|
}
|
59
cmd/clairclt/clair/push.go
Normal file
59
cmd/clairclt/clair/push.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ErrOSNotSupported is returned when Clair received a layer which on os not supported
|
||||||
|
var ErrOSNotSupported = errors.New("worker: OS and/or package manager are not supported")
|
||||||
|
|
||||||
|
//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 ErrOSNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading 'add layer' response : %v", err)
|
||||||
|
}
|
||||||
|
type layerError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
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/clairclt/clair/report.go
Normal file
32
cmd/clairclt/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
|
||||||
|
}
|
1611
cmd/clairclt/clair/report_test.go
Normal file
1611
cmd/clairclt/clair/report_test.go
Normal file
File diff suppressed because it is too large
Load Diff
237
cmd/clairclt/clair/templates.go
Normal file
237
cmd/clairclt/clair/templates.go
Normal file
File diff suppressed because one or more lines are too long
98
cmd/clairclt/clair/templates/analysis-template.html
Normal file
98
cmd/clairclt/clair/templates/analysis-template.html
Normal file
File diff suppressed because one or more lines are too long
32
cmd/clairclt/clair/versions.go
Normal file
32
cmd/clairclt/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
|
||||||
|
}
|
BIN
cmd/clairclt/clairclt
Executable file
BIN
cmd/clairclt/clairclt
Executable file
Binary file not shown.
81
cmd/clairclt/cmd/analyse.go
Normal file
81
cmd/clairclt/cmd/analyse.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(errInternalError)
|
||||||
|
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 == docker.ErrNotFound {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("pulling image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
image, err = docker.Parse(imageName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("parsing local image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
docker.FromHistory(&image)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
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"))
|
||||||
|
}
|
38
cmd/clairclt/cmd/health.go
Normal file
38
cmd/clairclt/cmd/health.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/clair"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(errInternalError)
|
||||||
|
logrus.Fatalf("rendering the health: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(healthCmd)
|
||||||
|
}
|
76
cmd/clairclt/cmd/login.go
Normal file
76
cmd/clairclt/cmd/login.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 reg string = docker.DockerHub
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
reg = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var login config.Login
|
||||||
|
if err := askForLogin(&login); err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("encrypting password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AddLogin(reg, login)
|
||||||
|
|
||||||
|
logged, err := docker.Login(reg)
|
||||||
|
|
||||||
|
if err != nil && err != docker.ErrUnauthorized {
|
||||||
|
config.RemoveLogin(reg)
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("log in: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logged {
|
||||||
|
config.RemoveLogin(reg)
|
||||||
|
fmt.Println("Unauthorized: Wrong login/password, please try again")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Login Successful")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func askForLogin(login *config.Login) error {
|
||||||
|
fmt.Print("Username: ")
|
||||||
|
fmt.Scan(&login.Username)
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
pwd, err := terminal.ReadPassword(1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(" ")
|
||||||
|
|
||||||
|
encryptedPwd := base64.StdEncoding.EncodeToString(pwd)
|
||||||
|
login.Password = string(encryptedPwd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(loginCmd)
|
||||||
|
}
|
44
cmd/clairclt/cmd/logout.go
Normal file
44
cmd/clairclt/cmd/logout.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
ok, err := config.RemoveLogin(reg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("log out: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
fmt.Println("Log out successful")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("You are not logged in")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(logoutCmd)
|
||||||
|
}
|
62
cmd/clairclt/cmd/pull.go
Normal file
62
cmd/clairclt/cmd/pull.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// 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/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(errInternalError)
|
||||||
|
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(errInternalError)
|
||||||
|
logrus.Fatalf("rendering image: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(pullCmd)
|
||||||
|
}
|
86
cmd/clairclt/cmd/push.go
Normal file
86
cmd/clairclt/cmd/push.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/server"
|
||||||
|
"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 == docker.ErrNotFound {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("pulling image %q: %v", imageName, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
image, err = docker.Parse(imageName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
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(errInternalError)
|
||||||
|
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(errInternalError)
|
||||||
|
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(errInternalError)
|
||||||
|
logrus.Fatalf("retrieving internal server IP: %v", err)
|
||||||
|
}
|
||||||
|
err = server.Serve(sURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("starting local server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
87
cmd/clairclt/cmd/report.go
Normal file
87
cmd/clairclt/cmd/report.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/xstrings"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(errInternalError)
|
||||||
|
logrus.Fatalf("generating HTML report: %v", err)
|
||||||
|
}
|
||||||
|
err = saveReport(imageName, string(html))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("saving HTML report: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "json":
|
||||||
|
json, err := xstrings.ToIndentJSON(analyses)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
logrus.Fatalf("indenting JSON: %v", err)
|
||||||
|
}
|
||||||
|
err = saveReport(imageName, string(json))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errInternalError)
|
||||||
|
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"))
|
||||||
|
}
|
61
cmd/clairclt/cmd/root.go
Normal file
61
cmd/clairclt/cmd/root.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInternalError = errors.New("client quit unexpectedly")
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
36
cmd/clairclt/cmd/version.go
Normal file
36
cmd/clairclt/cmd/version.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(errInternalError)
|
||||||
|
logrus.Fatalf("rendering the version: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(versionCmd)
|
||||||
|
}
|
312
cmd/clairclt/config/config.go
Normal file
312
cmd/clairclt/config/config.go
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/xstrings"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrLoginNotFound = errors.New("user is not log in")
|
||||||
|
|
||||||
|
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 {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
p := usr.HomeDir + "/.hyperclair"
|
||||||
|
|
||||||
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(p, 0700)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
type Login struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginMapping map[string]Login
|
||||||
|
|
||||||
|
func HyperclairConfig() string {
|
||||||
|
return HyperclairHome() + "/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddLogin(registry string, login Login) error {
|
||||||
|
var logins loginMapping
|
||||||
|
|
||||||
|
if err := readConfigFile(&logins, HyperclairConfig()); err != nil {
|
||||||
|
return fmt.Errorf("reading hyperclair file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logins[registry] = login
|
||||||
|
|
||||||
|
if err := writeConfigFile(logins, HyperclairConfig()); err != nil {
|
||||||
|
return fmt.Errorf("indenting login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func GetLogin(registry string) (Login, error) {
|
||||||
|
if _, err := os.Stat(HyperclairConfig()); err == nil {
|
||||||
|
var logins loginMapping
|
||||||
|
|
||||||
|
if err := readConfigFile(&logins, HyperclairConfig()); err != nil {
|
||||||
|
return Login{}, fmt.Errorf("reading hyperclair file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if login, present := logins[registry]; present {
|
||||||
|
d, err := base64.StdEncoding.DecodeString(login.Password)
|
||||||
|
if err != nil {
|
||||||
|
return Login{}, fmt.Errorf("decoding password: %v", err)
|
||||||
|
}
|
||||||
|
login.Password = string(d)
|
||||||
|
return login, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Login{}, ErrLoginNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveLogin(registry string) (bool, error) {
|
||||||
|
if _, err := os.Stat(HyperclairConfig()); err == nil {
|
||||||
|
var logins loginMapping
|
||||||
|
|
||||||
|
if err := readConfigFile(&logins, HyperclairConfig()); err != nil {
|
||||||
|
return false, fmt.Errorf("reading hyperclair file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, present := logins[registry]; present {
|
||||||
|
delete(logins, registry)
|
||||||
|
|
||||||
|
if err := writeConfigFile(logins, HyperclairConfig()); err != nil {
|
||||||
|
return false, fmt.Errorf("indenting login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigFile(logins *loginMapping, file string) error {
|
||||||
|
if _, err := os.Stat(file); err == nil {
|
||||||
|
f, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(f, &logins); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*logins = loginMapping{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfigFile(logins loginMapping, file string) error {
|
||||||
|
s, err := xstrings.ToIndentJSON(logins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(file, s, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//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
|
||||||
|
}
|
204
cmd/clairclt/config/config_test.go
Normal file
204
cmd/clairclt/config/config_test.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var loginData = []struct {
|
||||||
|
in string
|
||||||
|
out int
|
||||||
|
}{
|
||||||
|
{"", 0},
|
||||||
|
{`{
|
||||||
|
"docker.io": {
|
||||||
|
"Username": "johndoe",
|
||||||
|
"Password": "$2a$05$Qe4TTO8HMmOht"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := 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 := 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 := 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfigFile(t *testing.T) {
|
||||||
|
for _, ld := range loginData {
|
||||||
|
|
||||||
|
tmpfile := CreateTmpConfigFile(ld.in)
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
|
||||||
|
var logins loginMapping
|
||||||
|
if err := readConfigFile(&logins, tmpfile); err != nil {
|
||||||
|
t.Errorf("readConfigFile(&logins,%q) failed => %v", tmpfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(logins); l != ld.out {
|
||||||
|
t.Errorf("readConfigFile(&logins,%q) => %v logins, want %v", tmpfile, l, ld.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteConfigFile(t *testing.T) {
|
||||||
|
logins := loginMapping{}
|
||||||
|
logins["docker.io"] = Login{Username: "johndoe", Password: "$2a$05$Qe4TTO8HMmOht"}
|
||||||
|
tmpfile := CreateTmpConfigFile("")
|
||||||
|
defer os.Remove(tmpfile) // clean up
|
||||||
|
|
||||||
|
if err := writeConfigFile(logins, tmpfile); err != nil {
|
||||||
|
t.Errorf("writeConfigFile(logins,%q) failed => %v", tmpfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logins = loginMapping{}
|
||||||
|
if err := readConfigFile(&logins, tmpfile); err != nil {
|
||||||
|
t.Errorf("after writing: readConfigFile(&logins,%q) failed => %v", tmpfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(logins); l != 1 {
|
||||||
|
t.Errorf("after writing: readConfigFile(&logins,%q) => %v logins, want %v", tmpfile, l, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
8
cmd/clairclt/contrib/.hyperclair.yml
Normal file
8
cmd/clairclt/contrib/.hyperclair.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
clair:
|
||||||
|
port: 6060
|
||||||
|
healthPort: 6061
|
||||||
|
uri: http://clair
|
||||||
|
priority: Low
|
||||||
|
report:
|
||||||
|
path: ./reports
|
||||||
|
format: html
|
11
cmd/clairclt/contrib/README.md
Normal file
11
cmd/clairclt/contrib/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CONTRIBUTION
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
# Running full dev environnement
|
||||||
|
|
||||||
|
Update the configuration file `hyperclair.yml` with your clair container IP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Running Authentication server, Registry, Clair & Postgres Clair db
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
29
cmd/clairclt/contrib/auth_server/config/auth_config.yml
Normal file
29
cmd/clairclt/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/clairclt/contrib/auth_server/ssl/old/server.key
Normal file
28
cmd/clairclt/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/clairclt/contrib/auth_server/ssl/old/server.pem
Normal file
21
cmd/clairclt/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/clairclt/contrib/auth_server/ssl/server.key
Normal file
28
cmd/clairclt/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/clairclt/contrib/auth_server/ssl/server.pem
Normal file
21
cmd/clairclt/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-----
|
77
cmd/clairclt/contrib/config/clair.yml
Normal file
77
cmd/clairclt/contrib/config/clair.yml
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Copyright 2015 clair authors
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined.
|
||||||
|
clair:
|
||||||
|
database:
|
||||||
|
# PostgreSQL Connection string
|
||||||
|
# http://www.postgresql.org/docs/9.4/static/libpq-connect.html
|
||||||
|
source: postgresql://postgres:root@postgres:5432?sslmode=disable
|
||||||
|
|
||||||
|
# Number of elements kept in the cache
|
||||||
|
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
|
||||||
|
cacheSize: 16384
|
||||||
|
|
||||||
|
api:
|
||||||
|
# API server port
|
||||||
|
port: 6060
|
||||||
|
|
||||||
|
# Health server port
|
||||||
|
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
|
||||||
|
healthport: 6061
|
||||||
|
|
||||||
|
# Deadline before an API request will respond with a 503
|
||||||
|
timeout: 900s
|
||||||
|
|
||||||
|
# 32-bit URL-safe base64 key used to encrypt pagination tokens
|
||||||
|
# If one is not provided, it will be generated.
|
||||||
|
# Multiple clair instances in the same cluster need the same value.
|
||||||
|
paginationKey:
|
||||||
|
|
||||||
|
# Optional PKI configuration
|
||||||
|
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||||
|
# https://github.com/coreos/etcd-ca
|
||||||
|
# https://github.com/cloudflare/cfssl
|
||||||
|
servername:
|
||||||
|
cafile:
|
||||||
|
keyfile:
|
||||||
|
certfile:
|
||||||
|
|
||||||
|
updater:
|
||||||
|
# Frequency the database will be updated with vulnerabilities from the default data sources
|
||||||
|
# The value 0 disables the updater entirely.
|
||||||
|
interval: 0
|
||||||
|
|
||||||
|
notifier:
|
||||||
|
# Number of attempts before the notification is marked as failed to be sent
|
||||||
|
attempts: 3
|
||||||
|
|
||||||
|
# Duration before a failed notification is retried
|
||||||
|
renotifyInterval: 2h
|
||||||
|
|
||||||
|
http:
|
||||||
|
# Optional endpoint that will receive notifications via POST requests
|
||||||
|
endpoint:
|
||||||
|
|
||||||
|
# Optional PKI configuration
|
||||||
|
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||||
|
# https://github.com/cloudflare/cfssl
|
||||||
|
# https://github.com/coreos/etcd-ca
|
||||||
|
servername:
|
||||||
|
cafile:
|
||||||
|
keyfile:
|
||||||
|
certfile:
|
||||||
|
|
||||||
|
# Optional HTTP Proxy: must be a valid URL (including the scheme).
|
||||||
|
proxy:
|
54
cmd/clairclt/contrib/docker-compose.yml
Normal file
54
cmd/clairclt/contrib/docker-compose.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
clair:
|
||||||
|
image: quay.io/coreos/clair
|
||||||
|
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
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
clair-data:
|
||||||
|
driver: local
|
||||||
|
hyperclair-data:
|
||||||
|
driver: local
|
||||||
|
registry-data:
|
||||||
|
driver: local
|
32
cmd/clairclt/docker/analyse.go
Normal file
32
cmd/clairclt/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/clairclt/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/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,
|
||||||
|
}
|
||||||
|
}
|
90
cmd/clairclt/docker/auth.go
Normal file
90
cmd/clairclt/docker/auth.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUnauthorized = errors.New("unauthorized access")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
l, err := config.GetLogin(request.URL.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(l.Username, l.Password)
|
||||||
|
|
||||||
|
response, err := httpclient.Get().Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode == http.StatusUnauthorized {
|
||||||
|
return ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
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/clairclt/docker/httpclient/httpclient.go
Normal file
24
cmd/clairclt/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
|
||||||
|
}
|
110
cmd/clairclt/docker/image.go
Normal file
110
cmd/clairclt/docker/image.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDisallowed = errors.New("analysing official images is not allowed")
|
||||||
|
|
||||||
|
//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{}, 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
|
||||||
|
}
|
74
cmd/clairclt/docker/image_test.go
Normal file
74
cmd/clairclt/docker/image_test.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
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 != ErrDisallowed {
|
||||||
|
t.Errorf("Parse(\"%s\") should failed with err \"%v\": %v", imageName.in, 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/clairclt/docker/local.go
Normal file
171
cmd/clairclt/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
|
||||||
|
}
|
46
cmd/clairclt/docker/login.go
Normal file
46
cmd/clairclt/docker/login.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
//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 == ErrUnauthorized {
|
||||||
|
authorized = false
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
authorized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorized, nil
|
||||||
|
}
|
90
cmd/clairclt/docker/pull.go
Normal file
90
cmd/clairclt/docker/pull.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("image not found")
|
||||||
|
|
||||||
|
//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{}, ErrUnauthorized
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return Image{}, ErrNotFound
|
||||||
|
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
|
||||||
|
}
|
87
cmd/clairclt/docker/push.go
Normal file
87
cmd/clairclt/docker/push.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/api/v1"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/clair"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/config"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/xstrings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var registryMapping map[string]string
|
||||||
|
|
||||||
|
//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 = strings.Replace(hURL, "/v2", "/local", -1)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.ErrOSNotSupported {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parentID = ""
|
||||||
|
} else {
|
||||||
|
parentID = payload.Layer.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if IsLocal {
|
||||||
|
if err := cleanLocal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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/clairclt/docker/push_test.go
Normal file
28
cmd/clairclt/docker/push_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
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("GetRegistryMapping(%q) failed => %v", layerID, err)
|
||||||
|
} else {
|
||||||
|
t.Errorf("GetRegistryMapping(%q) => %q, want %q", layerID, registryURI, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
cmd/clairclt/hyperclair.yml
Normal file
10
cmd/clairclt/hyperclair.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
clair:
|
||||||
|
port: 6060
|
||||||
|
healthPort: 6061
|
||||||
|
uri: http://clair
|
||||||
|
priority: Low
|
||||||
|
report:
|
||||||
|
path: ./reports
|
||||||
|
format: html
|
||||||
|
hyperclair:
|
||||||
|
interface: virtualbox
|
8
cmd/clairclt/hyperclair.yml.default
Normal file
8
cmd/clairclt/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/clairclt/main.go
Normal file
21
cmd/clairclt/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/clairclt/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
15538
cmd/clairclt/reports/html/analysis-jgsqware-clair-latest.html
Normal file
15538
cmd/clairclt/reports/html/analysis-jgsqware-clair-latest.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1580
cmd/clairclt/reports/json/analysis-jgsqware-ubuntu-git-latest.json
Normal file
1580
cmd/clairclt/reports/json/analysis-jgsqware-ubuntu-git-latest.json
Normal file
File diff suppressed because it is too large
Load Diff
99
cmd/clairclt/server/server.go
Normal file
99
cmd/clairclt/server/server.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker"
|
||||||
|
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Serve run a local server with the fileserver and the reverse proxy
|
||||||
|
func Serve(sURL string) error {
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
http.Handle("/v2/", newSingleHostReverseProxy())
|
||||||
|
http.Handle("/local/", http.StripPrefix("/local", restrictedFileServer(docker.TmpLocal())))
|
||||||
|
|
||||||
|
listener := tcpListener(sURL)
|
||||||
|
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 tcpListener(sURL string) (listener net.Listener) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func restrictedFileServer(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSingleHostReverseProxy() *httputil.ReverseProxy {
|
||||||
|
director := func(request *http.Request) {
|
||||||
|
|
||||||
|
var validID = regexp.MustCompile(`.*/blobs/(.*)$`)
|
||||||
|
u := request.URL.Path
|
||||||
|
logrus.Debugf("request url: %v", u)
|
||||||
|
if !validID.MatchString(u) {
|
||||||
|
logrus.Errorf("cannot parse url: %v", u)
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _ := docker.GetRegistryMapping(validID.FindStringSubmatch(u)[1])
|
||||||
|
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 &httputil.ReverseProxy{
|
||||||
|
Director: director,
|
||||||
|
}
|
||||||
|
}
|
30
cmd/clairclt/xstrings/xstrings.go
Normal file
30
cmd/clairclt/xstrings/xstrings.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
//ToIndentJSON apply json.MarshalIndent with tabulation on an interface and return the formatted string
|
||||||
|
func ToIndentJSON(v interface{}) ([]byte, error) {
|
||||||
|
b, err := json.MarshalIndent(v, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
27
cmd/clairclt/xstrings/xstrings_test.go
Normal file
27
cmd/clairclt/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