mirror of
https://github.com/genuinetools/reg.git
synced 2024-09-28 11:46:20 -04:00
Merge branch 'majst01-dynamic-frontend'
* majst01-dynamic-frontend: Remove code duplicates Add git to be able to do go get Make tests work again Make sample shell script more robust load BadVulns count with AJAX calls Satisfy golint Vendor all dependencies Always point to the vulnerabilities cleanup and reusage of error messages Remove now unneeded templates Show vulnerabilities Fix searching for new dynamic frontend Refactor NewClairLayer to a single place First draft of a dynamic frontend, display of vulnerabilities is still not implemented.
This commit is contained in:
commit
510b24edff
16 changed files with 696 additions and 304 deletions
|
@ -15,6 +15,7 @@
|
||||||
install:
|
install:
|
||||||
- go get github.com/golang/lint/golint
|
- go get github.com/golang/lint/golint
|
||||||
script:
|
script:
|
||||||
|
- go get ./...
|
||||||
- go build -v
|
- go build -v
|
||||||
- go vet $(go list ./... | grep -v vendor)
|
- go vet $(go list ./... | grep -v vendor)
|
||||||
- test -z "$(golint ./... | grep -v vendor | tee /dev/stderr)"
|
- test -z "$(golint ./... | grep -v vendor | tee /dev/stderr)"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
FROM golang:alpine
|
FROM golang:alpine
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
build-base
|
build-base \
|
||||||
|
git
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -25,6 +25,8 @@ lint:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@echo "+ $@"
|
@echo "+ $@"
|
||||||
|
@go get github.com/labstack/echo
|
||||||
|
@go get github.com/labstack/echo/middleware
|
||||||
@go test -v -tags "$(BUILDTAGS) cgo" $(shell go list ./... | grep -v vendor)
|
@go test -v -tags "$(BUILDTAGS) cgo" $(shell go list ./... | grep -v vendor)
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
|
|
|
@ -56,6 +56,16 @@ type Vulnerability struct {
|
||||||
FixedIn []feature `json:"FixedIn,omitempty"`
|
FixedIn []feature `json:"FixedIn,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VulnerabilityReport represents the result of a vulnerability scan of a repo.
|
||||||
|
type VulnerabilityReport struct {
|
||||||
|
RegistryURL string
|
||||||
|
Repo string
|
||||||
|
Tag string
|
||||||
|
Date string
|
||||||
|
Vulns []Vulnerability
|
||||||
|
VulnsBySeverity map[string][]Vulnerability
|
||||||
|
BadVulns int
|
||||||
|
}
|
||||||
type feature struct {
|
type feature struct {
|
||||||
Name string `json:"Name,omitempty"`
|
Name string `json:"Name,omitempty"`
|
||||||
NamespaceName string `json:"NamespaceName,omitempty"`
|
NamespaceName string `json:"NamespaceName,omitempty"`
|
||||||
|
|
111
clair/vulns.go
Normal file
111
clair/vulns.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package clair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
|
"github.com/jessfraz/reg/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vulnerabilities scans the given repo and tag
|
||||||
|
func (c *Clair) Vulnerabilities(r *registry.Registry, repo, tag string, m schema1.SignedManifest) (VulnerabilityReport, error) {
|
||||||
|
report := VulnerabilityReport{
|
||||||
|
RegistryURL: r.Domain,
|
||||||
|
Repo: repo,
|
||||||
|
Tag: tag,
|
||||||
|
Date: time.Now().Local().Format(time.RFC1123),
|
||||||
|
VulnsBySeverity: make(map[string][]Vulnerability),
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out the empty layers
|
||||||
|
var filteredLayers []schema1.FSLayer
|
||||||
|
for _, layer := range m.FSLayers {
|
||||||
|
if layer.BlobSum != EmptyLayerBlobSum {
|
||||||
|
filteredLayers = append(filteredLayers, layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.FSLayers = filteredLayers
|
||||||
|
if len(m.FSLayers) == 0 {
|
||||||
|
fmt.Printf("No need to analyse image %s:%s as there is no non-emtpy layer", repo, tag)
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
||||||
|
// form the clair layer
|
||||||
|
l, err := c.NewClairLayer(r, repo, m.FSLayers, i)
|
||||||
|
if err != nil {
|
||||||
|
return report, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// post the layer
|
||||||
|
if _, err := c.PostLayer(l); err != nil {
|
||||||
|
return report, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vl, err := c.GetLayer(m.FSLayers[0].BlobSum.String(), false, true)
|
||||||
|
if err != nil {
|
||||||
|
return report, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the vulns
|
||||||
|
for _, f := range vl.Features {
|
||||||
|
for _, v := range f.Vulnerabilities {
|
||||||
|
report.Vulns = append(report.Vulns, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnsBy := func(sev string, store map[string][]Vulnerability) []Vulnerability {
|
||||||
|
items, found := store[sev]
|
||||||
|
if !found {
|
||||||
|
items = make([]Vulnerability, 0)
|
||||||
|
store[sev] = items
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// group by severity
|
||||||
|
for _, v := range report.Vulns {
|
||||||
|
sevRow := vulnsBy(v.Severity, report.VulnsBySeverity)
|
||||||
|
report.VulnsBySeverity[v.Severity] = append(sevRow, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate number of bad vulns
|
||||||
|
report.BadVulns = len(report.VulnsBySeverity["High"]) + len(report.VulnsBySeverity["Critical"]) + len(report.VulnsBySeverity["Defcon1"])
|
||||||
|
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClairLayer will form a layer struct required for a clar scan
|
||||||
|
func (c *Clair) NewClairLayer(r *registry.Registry, image string, fsLayers []schema1.FSLayer, index int) (*Layer, error) {
|
||||||
|
var parentName string
|
||||||
|
if index < len(fsLayers)-1 {
|
||||||
|
parentName = fsLayers[index+1].BlobSum.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// form the path
|
||||||
|
p := strings.Join([]string{r.URL, "v2", image, "blobs", fsLayers[index].BlobSum.String()}, "/")
|
||||||
|
|
||||||
|
// get the token
|
||||||
|
token, err := r.Token(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := make(map[string]string)
|
||||||
|
if token != "" {
|
||||||
|
h = map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Layer{
|
||||||
|
Name: fsLayers[index].BlobSum.String(),
|
||||||
|
Path: p,
|
||||||
|
ParentName: parentName,
|
||||||
|
Format: "Docker",
|
||||||
|
Headers: h,
|
||||||
|
}, nil
|
||||||
|
}
|
4
main.go
4
main.go
|
@ -266,6 +266,8 @@ func main() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME use clair.Vulnerabilities
|
||||||
|
|
||||||
// get the manifest
|
// get the manifest
|
||||||
m, err := r.ManifestV1(repo, ref)
|
m, err := r.ManifestV1(repo, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -293,7 +295,7 @@ func main() {
|
||||||
|
|
||||||
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
||||||
// form the clair layer
|
// form the clair layer
|
||||||
l, err := utils.NewClairLayer(r, repo, m.FSLayers, i)
|
l, err := cr.NewClairLayer(r, repo, m.FSLayers, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
252
server/server.go
252
server/server.go
|
@ -1,29 +1,22 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
humanize "github.com/dustin/go-humanize"
|
|
||||||
"github.com/jessfraz/reg/clair"
|
"github.com/jessfraz/reg/clair"
|
||||||
"github.com/jessfraz/reg/registry"
|
"github.com/jessfraz/reg/registry"
|
||||||
"github.com/jessfraz/reg/utils"
|
"github.com/jessfraz/reg/utils"
|
||||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// VERSION is the binary version.
|
// VERSION is the binary version.
|
||||||
VERSION = "v0.1.0"
|
VERSION = "v0.2.0"
|
||||||
|
|
||||||
dockerConfigPath = ".docker/config.json"
|
dockerConfigPath = ".docker/config.json"
|
||||||
)
|
)
|
||||||
|
@ -31,8 +24,8 @@ const (
|
||||||
var (
|
var (
|
||||||
updating = false
|
updating = false
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
tmpl *template.Template
|
|
||||||
r *registry.Registry
|
r *registry.Registry
|
||||||
|
cl *clair.Clair
|
||||||
)
|
)
|
||||||
|
|
||||||
// preload initializes any global options and configuration
|
// preload initializes any global options and configuration
|
||||||
|
@ -121,127 +114,58 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the path to the static directory
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
staticDir := filepath.Join(wd, "static")
|
|
||||||
|
|
||||||
// create the template
|
|
||||||
templateDir := filepath.Join(staticDir, "../templates")
|
|
||||||
funcMap := template.FuncMap{
|
|
||||||
"trim": func(s string) string {
|
|
||||||
return wordwrap.WrapString(s, 80)
|
|
||||||
},
|
|
||||||
"color": func(s string) string {
|
|
||||||
switch s = strings.ToLower(s); s {
|
|
||||||
case "high":
|
|
||||||
return "danger"
|
|
||||||
case "critical":
|
|
||||||
return "danger"
|
|
||||||
case "defcon1":
|
|
||||||
return "danger"
|
|
||||||
case "medium":
|
|
||||||
return "warning"
|
|
||||||
case "low":
|
|
||||||
return "info"
|
|
||||||
case "negligible":
|
|
||||||
return "info"
|
|
||||||
case "unknown":
|
|
||||||
return "default"
|
|
||||||
default:
|
|
||||||
return "default"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
vulns := filepath.Join(templateDir, "vulns.html")
|
|
||||||
if _, err := os.Stat(vulns); os.IsNotExist(err) {
|
|
||||||
logrus.Fatalf("Template %s not found", vulns)
|
|
||||||
}
|
|
||||||
layout := filepath.Join(templateDir, "layout.html")
|
|
||||||
if _, err := os.Stat(layout); os.IsNotExist(err) {
|
|
||||||
logrus.Fatalf("Template %s not found", layout)
|
|
||||||
}
|
|
||||||
tmpl = template.Must(template.New("").Funcs(funcMap).ParseFiles(vulns, layout))
|
|
||||||
|
|
||||||
// create the initial index
|
|
||||||
logrus.Info("creating initial static index")
|
|
||||||
if err := createStaticIndex(r, staticDir, "", c.GlobalBool("debug"), c.GlobalInt("workers")); err != nil {
|
|
||||||
logrus.Fatalf("Error creating index: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the duration
|
// parse the duration
|
||||||
dur, err := time.ParseDuration(c.String("interval"))
|
dur, err := time.ParseDuration(c.String("interval"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("parsing %s as duration failed: %v", c.String("interval"), err)
|
logrus.Fatalf("parsing %s as duration failed: %v", c.String("interval"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a clair instance if needed
|
||||||
|
if c.GlobalString("clair") != "" {
|
||||||
|
cl, err = clair.New(c.GlobalString("clair"), c.GlobalBool("debug"))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("creation of clair failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
ticker := time.NewTicker(dur)
|
ticker := time.NewTicker(dur)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// create more indexes every X minutes based off interval
|
// analyse repositories every X minutes based off interval
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
if !updating {
|
if !updating {
|
||||||
logrus.Info("creating timer based static index")
|
logrus.Info("start repository analysis")
|
||||||
if err := createStaticIndex(r, staticDir, c.GlobalString("clair"), c.GlobalBool("debug"), c.GlobalInt("workers")); err != nil {
|
start := time.Now()
|
||||||
logrus.Warnf("creating static index failed: %v", err)
|
if err := analyseRepositories(r, cl, c.GlobalBool("debug"), c.GlobalInt("workers")); err != nil {
|
||||||
|
logrus.Warnf("repository analysis failed: %v", err)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
updating = false
|
updating = false
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
logrus.Info("finished waiting for vulns wait group")
|
elapsed := time.Since(start)
|
||||||
|
logrus.Infof("finished repository analysis in %s", elapsed)
|
||||||
} else {
|
} else {
|
||||||
logrus.Warnf("skipping timer based static index update for %s", c.String("interval"))
|
logrus.Warnf("skipping timer based repository analysis for %s", c.String("interval"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// create mux server
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
// static files handler
|
|
||||||
staticHandler := http.FileServer(http.Dir(staticDir))
|
|
||||||
mux.Handle("/", staticHandler)
|
|
||||||
|
|
||||||
// set up the server
|
|
||||||
port := c.String("port")
|
port := c.String("port")
|
||||||
server := &http.Server{
|
keyfile := c.String("key")
|
||||||
Addr: ":" + port,
|
certfile := c.String("cert")
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
logrus.Infof("Starting server on port %q", port)
|
|
||||||
if c.String("cert") != "" && c.String("key") != "" {
|
|
||||||
logrus.Fatal(server.ListenAndServeTLS(c.String("cert"), c.String("key")))
|
|
||||||
} else {
|
|
||||||
logrus.Fatal(server.ListenAndServe())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
logrus.Fatal(listenAndServe(port, keyfile, certfile, r, cl))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Run(os.Args)
|
app.Run(os.Args)
|
||||||
}
|
}
|
||||||
|
|
||||||
type data struct {
|
|
||||||
RegistryURL string
|
|
||||||
LastUpdated string
|
|
||||||
Repos []repository
|
|
||||||
}
|
|
||||||
|
|
||||||
type repository struct {
|
|
||||||
Name string
|
|
||||||
Tag string
|
|
||||||
RepoURI string
|
|
||||||
CreatedDate string
|
|
||||||
VulnURI string
|
|
||||||
}
|
|
||||||
|
|
||||||
type v1Compatibility struct {
|
type v1Compatibility struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug bool, workers int) error {
|
func analyseRepositories(r *registry.Registry, cl *clair.Clair, debug bool, workers int) error {
|
||||||
updating = true
|
updating = true
|
||||||
logrus.Info("fetching catalog")
|
logrus.Info("fetching catalog")
|
||||||
repoList, err := r.Catalog("")
|
repoList, err := r.Catalog("")
|
||||||
|
@ -250,7 +174,6 @@ func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug b
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Info("fetching tags")
|
logrus.Info("fetching tags")
|
||||||
var repos []repository
|
|
||||||
sem := make(chan int, workers)
|
sem := make(chan int, workers)
|
||||||
for i, repo := range repoList {
|
for i, repo := range repoList {
|
||||||
// get the tags
|
// get the tags
|
||||||
|
@ -260,35 +183,12 @@ func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug b
|
||||||
}
|
}
|
||||||
for j, tag := range tags {
|
for j, tag := range tags {
|
||||||
// get the manifest
|
// get the manifest
|
||||||
|
|
||||||
m1, err := r.ManifestV1(repo, tag)
|
m1, err := r.ManifestV1(repo, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warnf("getting v1 manifest for %s:%s failed: %v", repo, tag, err)
|
logrus.Warnf("getting v1 manifest for %s:%s failed: %v", repo, tag, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var createdDate string
|
if cl != nil {
|
||||||
for _, h := range m1.History {
|
|
||||||
var comp v1Compatibility
|
|
||||||
if err := json.Unmarshal([]byte(h.V1Compatibility), &comp); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal v1compatibility failed: %v", err)
|
|
||||||
}
|
|
||||||
createdDate = humanize.Time(comp.Created)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
repoURI := fmt.Sprintf("%s/%s", r.Domain, repo)
|
|
||||||
if tag != "latest" {
|
|
||||||
repoURI += ":" + tag
|
|
||||||
}
|
|
||||||
|
|
||||||
newrepo := repository{
|
|
||||||
Name: repo,
|
|
||||||
Tag: tag,
|
|
||||||
RepoURI: repoURI,
|
|
||||||
CreatedDate: createdDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
if clairURI != "" {
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
sem <- 1
|
sem <- 1
|
||||||
go func(repo, tag string, i, j int) {
|
go func(repo, tag string, i, j int) {
|
||||||
|
@ -297,52 +197,21 @@ func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug b
|
||||||
<-sem
|
<-sem
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logrus.Infof("creating vuln static page for %s:%s", repo, tag)
|
logrus.Infof("search vulnerabilities for %s:%s", repo, tag)
|
||||||
|
|
||||||
if err := createVulnStaticPage(r, staticDir, clairURI, repo, tag, m1, debug); err != nil {
|
if err := searchVulnerabilities(r, cl, repo, tag, m1, debug); err != nil {
|
||||||
logrus.Warnf("creating vuln static page for %s:%s failed: %v", repo, tag, err)
|
logrus.Warnf("searching vulnerabilities for %s:%s failed: %v", repo, tag, err)
|
||||||
}
|
}
|
||||||
}(repo, tag, i, j)
|
}(repo, tag, i, j)
|
||||||
|
|
||||||
newrepo.VulnURI = filepath.Join(repo, tag)
|
|
||||||
}
|
}
|
||||||
repos = append(repos, newrepo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d := data{
|
|
||||||
RegistryURL: r.Domain,
|
|
||||||
Repos: repos,
|
|
||||||
LastUpdated: time.Now().Local().Format(time.RFC1123),
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("rendering index template")
|
|
||||||
if err := renderTemplate(staticDir, "index", "index.html", d); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
updating = false
|
updating = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type vulnsReport struct {
|
func searchVulnerabilities(r *registry.Registry, cl *clair.Clair, repo, tag string, m schema1.SignedManifest, debug bool) error {
|
||||||
RegistryURL string
|
|
||||||
Repo string
|
|
||||||
Tag string
|
|
||||||
Date string
|
|
||||||
Vulns []clair.Vulnerability
|
|
||||||
VulnsBySeverity map[string][]clair.Vulnerability
|
|
||||||
BadVulns int
|
|
||||||
}
|
|
||||||
|
|
||||||
func createVulnStaticPage(r *registry.Registry, staticDir, clairURI, repo, tag string, m schema1.SignedManifest, debug bool) error {
|
|
||||||
report := vulnsReport{
|
|
||||||
RegistryURL: r.Domain,
|
|
||||||
Repo: repo,
|
|
||||||
Tag: tag,
|
|
||||||
Date: time.Now().Local().Format(time.RFC1123),
|
|
||||||
VulnsBySeverity: make(map[string][]clair.Vulnerability),
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out the empty layers
|
// filter out the empty layers
|
||||||
var filteredLayers []schema1.FSLayer
|
var filteredLayers []schema1.FSLayer
|
||||||
for _, layer := range m.FSLayers {
|
for _, layer := range m.FSLayers {
|
||||||
|
@ -356,81 +225,18 @@ func createVulnStaticPage(r *registry.Registry, staticDir, clairURI, repo, tag s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize clair
|
|
||||||
cr, err := clair.New(clairURI, debug)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
||||||
// form the clair layer
|
// form the clair layer
|
||||||
l, err := utils.NewClairLayer(r, repo, m.FSLayers, i)
|
l, err := cl.NewClairLayer(r, repo, m.FSLayers, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// post the layer
|
// post the layer
|
||||||
if _, err := cr.PostLayer(l); err != nil {
|
if _, err := cl.PostLayer(l); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vl, err := cr.GetLayer(m.FSLayers[0].BlobSum.String(), false, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the vulns
|
|
||||||
for _, f := range vl.Features {
|
|
||||||
for _, v := range f.Vulnerabilities {
|
|
||||||
report.Vulns = append(report.Vulns, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vulnsBy := func(sev string, store map[string][]clair.Vulnerability) []clair.Vulnerability {
|
|
||||||
items, found := store[sev]
|
|
||||||
if !found {
|
|
||||||
items = make([]clair.Vulnerability, 0)
|
|
||||||
store[sev] = items
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
// group by severity
|
|
||||||
for _, v := range report.Vulns {
|
|
||||||
sevRow := vulnsBy(v.Severity, report.VulnsBySeverity)
|
|
||||||
report.VulnsBySeverity[v.Severity] = append(sevRow, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate number of bad vulns
|
|
||||||
report.BadVulns = len(report.VulnsBySeverity["High"]) + len(report.VulnsBySeverity["Critical"]) + len(report.VulnsBySeverity["Defcon1"])
|
|
||||||
|
|
||||||
path := filepath.Join(repo, tag, "index.html")
|
|
||||||
if err := renderTemplate(staticDir, "vulns", path, report); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderTemplate(staticDir, templateName, dest string, data interface{}) error {
|
|
||||||
// parse & execute the template
|
|
||||||
logrus.Debugf("executing the template %s", templateName)
|
|
||||||
|
|
||||||
path := filepath.Join(staticDir, dest)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logrus.Debugf("creating/opening file %s", path)
|
|
||||||
f, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if err := tmpl.ExecuteTemplate(f, templateName, data); err != nil {
|
|
||||||
f.Close()
|
|
||||||
return fmt.Errorf("execute template %s failed: %v", templateName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,7 +194,7 @@ td {
|
||||||
td:last-child,
|
td:last-child,
|
||||||
th:last-child {
|
th:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 0px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
td a {
|
td a {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -205,6 +205,38 @@ tr.parent a {
|
||||||
.parent a:hover {
|
.parent a:hover {
|
||||||
color: #2a2a2a;
|
color: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*------------------------------------*\
|
||||||
|
Loading Indicator
|
||||||
|
\*------------------------------------*/
|
||||||
|
.signal {
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 15px;
|
||||||
|
left: 50%;
|
||||||
|
margin: -8px 0 0 -8px;
|
||||||
|
opacity: 0;
|
||||||
|
top: 50%;
|
||||||
|
width: 15px;
|
||||||
|
float: right;
|
||||||
|
animation: pulsate 1s ease-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulsate {
|
||||||
|
0% {
|
||||||
|
transform: scale(.1);
|
||||||
|
opacity: 0.0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*------------------------------------*\
|
/*------------------------------------*\
|
||||||
Footer
|
Footer
|
||||||
\*------------------------------------*/
|
\*------------------------------------*/
|
||||||
|
|
|
@ -23,7 +23,7 @@ function prettyDate(time){
|
||||||
function search(search_val){
|
function search(search_val){
|
||||||
var suche = search_val.toLowerCase();
|
var suche = search_val.toLowerCase();
|
||||||
var table = document.getElementById("directory");
|
var table = document.getElementById("directory");
|
||||||
var cellNr = 3;
|
var cellNr = 1;
|
||||||
var ele;
|
var ele;
|
||||||
for (var r = 1; r < table.rows.length; r++){
|
for (var r = 1; r < table.rows.length; r++){
|
||||||
ele = table.rows[r].cells[cellNr].innerHTML.replace(/<[^>]+>/g,"");
|
ele = table.rows[r].cells[cellNr].innerHTML.replace(/<[^>]+>/g,"");
|
||||||
|
@ -35,6 +35,25 @@ function search(search_val){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadVulnerabilityCount(url){
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url);
|
||||||
|
xhr.setRequestHeader("Accept-Encoding", "text/json")
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var report = JSON.parse(xhr.responseText);
|
||||||
|
var id = report.Repo + ':' + report.Tag;
|
||||||
|
var element = document.getElementById(id);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = report.BadVulns;
|
||||||
|
} else {
|
||||||
|
console.log("element not found for given id ", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
var el = document.querySelectorAll('tr:nth-child(2)')[0].querySelectorAll('td:nth-child(2)')[0];
|
var el = document.querySelectorAll('tr:nth-child(2)')[0].querySelectorAll('td:nth-child(2)')[0];
|
||||||
if (el.textContent == 'Parent Directory'){
|
if (el.textContent == 'Parent Directory'){
|
||||||
|
@ -72,22 +91,26 @@ our_table.setAttribute('id', 'directory');
|
||||||
var search_input = document.querySelectorAll('input[name="filter"]')[0];
|
var search_input = document.querySelectorAll('input[name="filter"]')[0];
|
||||||
var clear_button = document.querySelectorAll('a.clear')[0];
|
var clear_button = document.querySelectorAll('a.clear')[0];
|
||||||
|
|
||||||
if (search_input.value !== ''){
|
if (search_input) {
|
||||||
search(search_input.value);
|
if (search_input.value !== ''){
|
||||||
|
search(search_input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
search_input.addEventListener('keyup', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
search(search_input.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_input.addEventListener('keypress', function(e){
|
||||||
|
if ( e.which == 13 ) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
search_input.addEventListener('keyup', function(e){
|
if (clear_button) {
|
||||||
e.preventDefault();
|
clear_button.addEventListener('click', function(e){
|
||||||
search(search_input.value);
|
search_input.value = '';
|
||||||
});
|
search('');
|
||||||
|
});
|
||||||
search_input.addEventListener('keypress', function(e){
|
}
|
||||||
if ( e.which == 13 ) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clear_button.addEventListener('click', function(e){
|
|
||||||
search_input.value = '';
|
|
||||||
search('');
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{{define "index"}}
|
{{define "repositories"}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
@ -8,12 +8,12 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<base href="/" >
|
<base href="/" >
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<title>{{ .RegistryURL }}</title>
|
<title>{{ .RegistryDomain }}</title>
|
||||||
<link rel="icon" type="image/ico" href="/favicon.ico">
|
<link rel="icon" type="image/ico" href="/favicon.ico">
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{{ .RegistryURL }}</h1>
|
<h1>{{ .RegistryDomain }}</h1>
|
||||||
<form>
|
<form>
|
||||||
<input name="filter" type="search"><a class="clear">clear</a>
|
<input name="filter" type="search"><a class="clear">clear</a>
|
||||||
</form>
|
</form>
|
||||||
|
@ -21,32 +21,21 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Repository Name</th>
|
||||||
<th>Tag</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th>Pull Command</th>
|
<th>Pull Command</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{ range $key, $value := .Repos }}
|
{{ range $key, $value := .Repositories }}
|
||||||
<tr>
|
<tr>
|
||||||
<td valign="top">
|
<td valign="top">
|
||||||
{{ if $value.VulnURI }}<a href="{{ $value.VulnURI }}">{{ end }}
|
<a href="/repo/{{ $value.Name | urlquery }}">
|
||||||
{{ $value.Name }}
|
{{ $value.Name }}
|
||||||
{{ if $value.VulnURI }}</a>{{ end }}
|
</a>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ if $value.VulnURI }}<a href="{{ $value.VulnURI }}">{{ end }}
|
|
||||||
{{ $value.Tag }}
|
|
||||||
{{ if $value.VulnURI }}</a>{{ end }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ if $value.VulnURI }}<a href="{{ $value.VulnURI }}">{{ end }}
|
|
||||||
{{ $value.CreatedDate }}
|
|
||||||
{{ if $value.VulnURI }}</a>{{ end }}
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td align="right" nowrap>
|
<td align="right" nowrap>
|
||||||
{{ if $value.VulnURI }}<a href="{{ $value.VulnURI }}">{{ end }}
|
<a href="/repo/{{ $value.Name | urlquery }}">
|
||||||
<code>docker pull {{ $value.RepoURI }}</code>
|
<code>docker pull {{ $value.URI }}</code>
|
||||||
{{ if $value.VulnURI }}</a>{{ end }}
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -55,7 +44,6 @@
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="https://twitter.com/jessfraz">@jessfraz</a>
|
<a href="https://twitter.com/jessfraz">@jessfraz</a>
|
||||||
<p>Last Updated: {{ .LastUpdated }}</p>
|
|
||||||
</div><!--/.footer-->
|
</div><!--/.footer-->
|
||||||
<script src="/js/scripts.js"></script>
|
<script src="/js/scripts.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
@ -68,4 +56,4 @@ ga('send', 'pageview');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
76
server/templates/echo/tags.html
Normal file
76
server/templates/echo/tags.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{{define "tags"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<base href="/" >
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<title>{{ .RegistryDomain }}/{{ .Name }}</title>
|
||||||
|
<link rel="icon" type="image/ico" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ .RegistryDomain }}/{{ .Name }}</h1>
|
||||||
|
<div class="wrapper">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Tag</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Vulnerabilities</th>
|
||||||
|
</tr>
|
||||||
|
{{ range $key, $value := .Repositories }}
|
||||||
|
<tr>
|
||||||
|
<td valign="left" nowrap>
|
||||||
|
<a href="/repo/{{ $value.Name | urlquery }}/{{ $value.Tag }}/vulns">
|
||||||
|
{{ $value.Name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="right" nowrap>
|
||||||
|
<a href="/repo/{{ $value.Name | urlquery }}/{{ $value.Tag }}/vulns">
|
||||||
|
{{ $value.Tag }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="right" nowrap>
|
||||||
|
{{ $value.Created.Format "02 Jan, 2006 15:04:05 UTC" }}
|
||||||
|
</td>
|
||||||
|
<td align="right" nowrap>
|
||||||
|
<a href="/repo/{{ $value.Name | urlquery }}/{{ $value.Tag }}/vulns" id="{{ $value.Name }}:{{ $value.Tag }}">
|
||||||
|
<div class="signal"></div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<a href="https://twitter.com/jessfraz">@jessfraz</a>
|
||||||
|
</div><!--/.footer-->
|
||||||
|
<script src="/js/scripts.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var ajaxCalls = [
|
||||||
|
{{ range $key, $value := .Repositories }}
|
||||||
|
'/repo/{{ $value.Name | urlquery }}/{{ $value.Tag }}/vulns',
|
||||||
|
{{ end }}
|
||||||
|
];
|
||||||
|
window.onload = function() {
|
||||||
|
Array.prototype.forEach.call(ajaxCalls, function(url, index){
|
||||||
|
loadVulnerabilityCount(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
|
ga('create', 'UA-29404280-12', 'jessfraz.com');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
337
server/web.go
Normal file
337
server/web.go
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/jessfraz/reg/clair"
|
||||||
|
"github.com/jessfraz/reg/registry"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type registryController struct {
|
||||||
|
reg *registry.Registry
|
||||||
|
cl *clair.Clair
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Template hold template data
|
||||||
|
type Template struct {
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Repository holds data after a vulnerability scan of a single repo
|
||||||
|
type Repository struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
URI string `json:"uri"`
|
||||||
|
VulnerabilityReport clair.VulnerabilityReport `json:"vulnerability"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// A AnalysisResult holds all vulnerabilities of a scan
|
||||||
|
type AnalysisResult struct {
|
||||||
|
Repositories []Repository `json:"repositories"`
|
||||||
|
RegistryDomain string `json:"registrydomain"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a template
|
||||||
|
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenAndServe(port, keyfile, certfile string, r *registry.Registry, c *clair.Clair) error {
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.Recover())
|
||||||
|
e.Use(middleware.Static("static"))
|
||||||
|
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"trim": func(s string) string {
|
||||||
|
return wordwrap.WrapString(s, 80)
|
||||||
|
},
|
||||||
|
"color": func(s string) string {
|
||||||
|
switch s = strings.ToLower(s); s {
|
||||||
|
case "high":
|
||||||
|
return "danger"
|
||||||
|
case "critical":
|
||||||
|
return "danger"
|
||||||
|
case "defcon1":
|
||||||
|
return "danger"
|
||||||
|
case "medium":
|
||||||
|
return "warning"
|
||||||
|
case "low":
|
||||||
|
return "info"
|
||||||
|
case "negligible":
|
||||||
|
return "info"
|
||||||
|
case "unknown":
|
||||||
|
return "default"
|
||||||
|
default:
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// precompile templates
|
||||||
|
t := &Template{
|
||||||
|
templates: template.Must(template.New("").Funcs(funcMap).ParseGlob("templates/echo/*.html")),
|
||||||
|
}
|
||||||
|
e.Renderer = t
|
||||||
|
|
||||||
|
rc := registryController{
|
||||||
|
reg: r,
|
||||||
|
cl: c,
|
||||||
|
}
|
||||||
|
|
||||||
|
e.GET("/", func(c echo.Context) error {
|
||||||
|
return c.Redirect(http.StatusMovedPermanently, "/repo")
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET("/repo", rc.repositories)
|
||||||
|
e.GET("/repo/:repo", rc.tags)
|
||||||
|
e.GET("/repo/:repo/:tag", rc.tag)
|
||||||
|
e.GET("/repo/:repo/:tag/vulns", rc.vulnerabilities)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":" + port,
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyfile != "" && certfile != "" {
|
||||||
|
cer, err := tls.LoadX509KeyPair(certfile, keyfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.StartServer(srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *registryController) repositories(c echo.Context) error {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"method": "repositories",
|
||||||
|
"context": c,
|
||||||
|
}).Debug("fetching repositories")
|
||||||
|
|
||||||
|
result := AnalysisResult{}
|
||||||
|
result.RegistryDomain = rc.reg.Domain
|
||||||
|
|
||||||
|
repoList, err := rc.reg.Catalog("")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting catalog failed: %v", err)
|
||||||
|
}
|
||||||
|
for _, repo := range repoList {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"repo": repo,
|
||||||
|
}).Debug("fetched repo")
|
||||||
|
repoURI := fmt.Sprintf("%s/%s", rc.reg.Domain, repo)
|
||||||
|
r := Repository{
|
||||||
|
Name: repo,
|
||||||
|
URI: repoURI,
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Repositories = append(result.Repositories, r)
|
||||||
|
}
|
||||||
|
err = c.Render(http.StatusOK, "repositories", result)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Error("error during template rendering")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.GET("/repo/:repo/:tag", rc.tag)
|
||||||
|
func (rc *registryController) tag(c echo.Context) error {
|
||||||
|
repo, err := url.QueryUnescape(c.Param("repo"))
|
||||||
|
if err != nil {
|
||||||
|
return c.String(http.StatusNotFound, "Given repo can not be unescaped.")
|
||||||
|
}
|
||||||
|
if repo == "" {
|
||||||
|
return c.String(http.StatusNotFound, "No repo given")
|
||||||
|
}
|
||||||
|
tag := c.Param("tag")
|
||||||
|
if tag == "" {
|
||||||
|
return c.String(http.StatusNotFound, "No tag given")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, fmt.Sprintf("Repo: %s Tag: %s ", repo, tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *registryController) tags(c echo.Context) error {
|
||||||
|
repo, err := url.QueryUnescape(c.Param("repo"))
|
||||||
|
if err != nil {
|
||||||
|
return c.String(http.StatusNotFound, "Given repo can not be unescaped.")
|
||||||
|
}
|
||||||
|
if repo == "" {
|
||||||
|
return c.String(http.StatusNotFound, "No repo given")
|
||||||
|
}
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"method": "tags",
|
||||||
|
"context": c,
|
||||||
|
"repo": repo,
|
||||||
|
}).Info("fetching tags")
|
||||||
|
|
||||||
|
tags, err := rc.reg.Tags(repo)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
}).Error("getting tags failed.", repo, err)
|
||||||
|
return c.String(http.StatusNotFound, "No Tags found")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := AnalysisResult{}
|
||||||
|
result.RegistryDomain = rc.reg.Domain
|
||||||
|
result.Name = repo
|
||||||
|
for _, tag := range tags {
|
||||||
|
// get the manifest
|
||||||
|
|
||||||
|
m1, err := r.ManifestV1(repo, tag)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
"tag": tag,
|
||||||
|
}).Warn("getting v1 manifest failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdDate time.Time
|
||||||
|
for _, h := range m1.History {
|
||||||
|
var comp v1Compatibility
|
||||||
|
if err := json.Unmarshal([]byte(h.V1Compatibility), &comp); err != nil {
|
||||||
|
msg := "unmarshal v1compatibility failed"
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
"tag": tag,
|
||||||
|
}).Warn(msg)
|
||||||
|
return c.String(http.StatusInternalServerError, msg)
|
||||||
|
}
|
||||||
|
createdDate = comp.Created
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
repoURI := fmt.Sprintf("%s/%s", r.Domain, repo)
|
||||||
|
if tag != "latest" {
|
||||||
|
repoURI += ":" + tag
|
||||||
|
}
|
||||||
|
r := Repository{
|
||||||
|
Name: repo,
|
||||||
|
Tag: tag,
|
||||||
|
URI: repoURI,
|
||||||
|
Created: createdDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.cl != nil {
|
||||||
|
vuln, err := rc.cl.Vulnerabilities(rc.reg, repo, tag, m1)
|
||||||
|
if err != nil {
|
||||||
|
msg := "error during vulnerability scanning."
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
"tag": tag,
|
||||||
|
}).Error(msg)
|
||||||
|
return c.String(http.StatusInternalServerError, msg)
|
||||||
|
}
|
||||||
|
r.VulnerabilityReport = vuln
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Repositories = append(result.Repositories, r)
|
||||||
|
}
|
||||||
|
err = c.Render(http.StatusOK, "tags", result)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Error("error during template rendering")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *registryController) vulnerabilities(c echo.Context) error {
|
||||||
|
repo, err := url.QueryUnescape(c.Param("repo"))
|
||||||
|
if err != nil {
|
||||||
|
return c.String(http.StatusNotFound, "Given repo can not be unescaped.")
|
||||||
|
}
|
||||||
|
if repo == "" {
|
||||||
|
return c.String(http.StatusNotFound, "No repo given")
|
||||||
|
}
|
||||||
|
tag := c.Param("tag")
|
||||||
|
if tag == "" {
|
||||||
|
return c.String(http.StatusNotFound, "No tag given")
|
||||||
|
}
|
||||||
|
|
||||||
|
m1, err := r.ManifestV1(repo, tag)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
"tag": tag,
|
||||||
|
}).Warn("getting v1 manifest failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range m1.History {
|
||||||
|
var comp v1Compatibility
|
||||||
|
if err := json.Unmarshal([]byte(h.V1Compatibility), &comp); err != nil {
|
||||||
|
msg := "unmarshal v1compatibility failed"
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
"tag": tag,
|
||||||
|
}).Warn(msg)
|
||||||
|
return c.String(http.StatusInternalServerError, msg)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
result := clair.VulnerabilityReport{}
|
||||||
|
|
||||||
|
if rc.cl != nil {
|
||||||
|
result, err = rc.cl.Vulnerabilities(rc.reg, repo, tag, m1)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"repo": repo,
|
||||||
|
"tag": tag,
|
||||||
|
}).Error("error during vulnerability scanning.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputType := outputType(c.Request())
|
||||||
|
if outputType == "json" {
|
||||||
|
err = c.JSON(http.StatusOK, result)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Error("error creating json response")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = c.Render(http.StatusOK, "vulns", result)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Error("error during template rendering")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputType(r *http.Request) string {
|
||||||
|
outputtype := "json"
|
||||||
|
if r.Header.Get("Accept-Encoding") == "text/html" {
|
||||||
|
outputtype = "html"
|
||||||
|
}
|
||||||
|
return outputtype
|
||||||
|
}
|
19
startClair.sh
Executable file
19
startClair.sh
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p $PWD/clair_config
|
||||||
|
|
||||||
|
docker rm -f postgres.test || true
|
||||||
|
docker rm -f clair.test || true
|
||||||
|
|
||||||
|
curl -sfL https://raw.githubusercontent.com/coreos/clair/master/config.example.yaml -o $PWD/clair_config/config.yaml
|
||||||
|
sed -i -s "s/host=localhost/host=postgres/g" $PWD/clair_config/config.yaml
|
||||||
|
docker run -d --name postgres.test -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6-alpine
|
||||||
|
sleep 5
|
||||||
|
docker run -it \
|
||||||
|
--name clair.test \
|
||||||
|
--link postgres.test:postgres \
|
||||||
|
-p 6060-6061:6060-6061 \
|
||||||
|
-v $PWD/clair_config:/config \
|
||||||
|
quay.io/coreos/clair-git:latest -config=/config/config.yaml
|
19
startRegistryAndCreateImages.sh
Executable file
19
startRegistryAndCreateImages.sh
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
docker rm -f registry.test || true
|
||||||
|
docker run -it -d --name registry.test -p 5000:5000 registry:2.6.0
|
||||||
|
|
||||||
|
docker pull alpine:3.5 || true
|
||||||
|
|
||||||
|
for repo in `seq 1 20`;
|
||||||
|
do
|
||||||
|
for tag in `seq 1 10`;
|
||||||
|
do
|
||||||
|
docker tag alpine:3.5 127.0.0.1:5000/company/alpine-${repo}:${tag}
|
||||||
|
docker push 127.0.0.1:5000/company/alpine-${repo}:${tag}
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
docker logs -f registry.test
|
|
@ -5,11 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/cli/config"
|
"github.com/docker/docker/cli/config"
|
||||||
"github.com/jessfraz/reg/clair"
|
|
||||||
"github.com/jessfraz/reg/registry"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,35 +75,3 @@ func GetRepoAndRef(c *cli.Context) (repo, ref string, err error) {
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClairLayer creates a clair layer from a docker registry image and fsLayers.
|
|
||||||
func NewClairLayer(r *registry.Registry, image string, fsLayers []schema1.FSLayer, index int) (*clair.Layer, error) {
|
|
||||||
var parentName string
|
|
||||||
if index < len(fsLayers)-1 {
|
|
||||||
parentName = fsLayers[index+1].BlobSum.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// form the path
|
|
||||||
p := strings.Join([]string{r.URL, "v2", image, "blobs", fsLayers[index].BlobSum.String()}, "/")
|
|
||||||
|
|
||||||
// get the token
|
|
||||||
token, err := r.Token(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h := make(map[string]string)
|
|
||||||
if token != "" {
|
|
||||||
h = map[string]string{
|
|
||||||
"Authorization": fmt.Sprintf("Bearer %s", token),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &clair.Layer{
|
|
||||||
Name: fsLayers[index].BlobSum.String(),
|
|
||||||
Path: p,
|
|
||||||
ParentName: parentName,
|
|
||||||
Format: "Docker",
|
|
||||||
Headers: h,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue