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:
Jess Frazelle 2017-04-24 12:30:36 -04:00
commit 510b24edff
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
16 changed files with 696 additions and 304 deletions

View file

@ -15,6 +15,7 @@
install:
- go get github.com/golang/lint/golint
script:
- go get ./...
- go build -v
- go vet $(go list ./... | grep -v vendor)
- test -z "$(golint ./... | grep -v vendor | tee /dev/stderr)"

View file

@ -1,4 +1,5 @@
FROM golang:alpine
RUN apk add --no-cache \
build-base
build-base \
git

View file

@ -25,6 +25,8 @@ lint:
test:
@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)
vet:

View file

@ -56,6 +56,16 @@ type Vulnerability struct {
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 {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`

111
clair/vulns.go Normal file
View 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
}

View file

@ -266,6 +266,8 @@ func main() {
return err
}
// FIXME use clair.Vulnerabilities
// get the manifest
m, err := r.ManifestV1(repo, ref)
if err != nil {
@ -293,7 +295,7 @@ func main() {
for i := len(m.FSLayers) - 1; i >= 0; i-- {
// 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 {
return err
}

View file

@ -1,29 +1,22 @@
package main
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/manifest/schema1"
humanize "github.com/dustin/go-humanize"
"github.com/jessfraz/reg/clair"
"github.com/jessfraz/reg/registry"
"github.com/jessfraz/reg/utils"
wordwrap "github.com/mitchellh/go-wordwrap"
"github.com/urfave/cli"
)
const (
// VERSION is the binary version.
VERSION = "v0.1.0"
VERSION = "v0.2.0"
dockerConfigPath = ".docker/config.json"
)
@ -31,8 +24,8 @@ const (
var (
updating = false
wg sync.WaitGroup
tmpl *template.Template
r *registry.Registry
cl *clair.Clair
)
// 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
dur, err := time.ParseDuration(c.String("interval"))
if err != nil {
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)
go func() {
// create more indexes every X minutes based off interval
// analyse repositories every X minutes based off interval
for range ticker.C {
if !updating {
logrus.Info("creating timer based static index")
if err := createStaticIndex(r, staticDir, c.GlobalString("clair"), c.GlobalBool("debug"), c.GlobalInt("workers")); err != nil {
logrus.Warnf("creating static index failed: %v", err)
logrus.Info("start repository analysis")
start := time.Now()
if err := analyseRepositories(r, cl, c.GlobalBool("debug"), c.GlobalInt("workers")); err != nil {
logrus.Warnf("repository analysis failed: %v", err)
wg.Wait()
updating = false
}
wg.Wait()
logrus.Info("finished waiting for vulns wait group")
elapsed := time.Since(start)
logrus.Infof("finished repository analysis in %s", elapsed)
} 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")
server := &http.Server{
Addr: ":" + port,
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())
}
keyfile := c.String("key")
certfile := c.String("cert")
logrus.Fatal(listenAndServe(port, keyfile, certfile, r, cl))
return nil
}
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 {
ID string `json:"id"`
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
logrus.Info("fetching catalog")
repoList, err := r.Catalog("")
@ -250,7 +174,6 @@ func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug b
}
logrus.Info("fetching tags")
var repos []repository
sem := make(chan int, workers)
for i, repo := range repoList {
// get the tags
@ -260,35 +183,12 @@ func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug b
}
for j, tag := range tags {
// get the manifest
m1, err := r.ManifestV1(repo, tag)
if err != nil {
logrus.Warnf("getting v1 manifest for %s:%s failed: %v", repo, tag, err)
}
var createdDate string
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 != "" {
if cl != nil {
wg.Add(1)
sem <- 1
go func(repo, tag string, i, j int) {
@ -297,52 +197,21 @@ func createStaticIndex(r *registry.Registry, staticDir, clairURI string, debug b
<-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 {
logrus.Warnf("creating vuln static page for %s:%s failed: %v", repo, tag, err)
if err := searchVulnerabilities(r, cl, repo, tag, m1, debug); err != nil {
logrus.Warnf("searching vulnerabilities for %s:%s failed: %v", repo, tag, err)
}
}(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
return nil
}
type vulnsReport struct {
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),
}
func searchVulnerabilities(r *registry.Registry, cl *clair.Clair, repo, tag string, m schema1.SignedManifest, debug bool) error {
// filter out the empty layers
var filteredLayers []schema1.FSLayer
for _, layer := range m.FSLayers {
@ -356,81 +225,18 @@ func createVulnStaticPage(r *registry.Registry, staticDir, clairURI, repo, tag s
return nil
}
// initialize clair
cr, err := clair.New(clairURI, debug)
if err != nil {
return err
}
for i := len(m.FSLayers) - 1; i >= 0; i-- {
// 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 {
return err
}
// post the layer
if _, err := cr.PostLayer(l); err != nil {
if _, err := cl.PostLayer(l); err != nil {
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
}

View file

@ -194,7 +194,7 @@ td {
td:last-child,
th:last-child {
text-align: right;
padding-right: 0px;
padding-right: 5px;
}
td a {
display: block;
@ -205,6 +205,38 @@ tr.parent a {
.parent a:hover {
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
\*------------------------------------*/

View file

@ -23,7 +23,7 @@ function prettyDate(time){
function search(search_val){
var suche = search_val.toLowerCase();
var table = document.getElementById("directory");
var cellNr = 3;
var cellNr = 1;
var ele;
for (var r = 1; r < table.rows.length; r++){
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];
if (el.textContent == 'Parent Directory'){
@ -72,22 +91,26 @@ our_table.setAttribute('id', 'directory');
var search_input = document.querySelectorAll('input[name="filter"]')[0];
var clear_button = document.querySelectorAll('a.clear')[0];
if (search_input.value !== ''){
search(search_input.value);
if (search_input) {
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){
e.preventDefault();
search(search_input.value);
});
search_input.addEventListener('keypress', function(e){
if ( e.which == 13 ) {
e.preventDefault();
}
});
clear_button.addEventListener('click', function(e){
search_input.value = '';
search('');
});
if (clear_button) {
clear_button.addEventListener('click', function(e){
search_input.value = '';
search('');
});
}

View file

@ -1,4 +1,4 @@
{{define "index"}}
{{define "repositories"}}
<!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]-->
@ -8,12 +8,12 @@
<meta charset="utf-8">
<base href="/" >
<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="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>{{ .RegistryURL }}</h1>
<h1>{{ .RegistryDomain }}</h1>
<form>
<input name="filter" type="search"><a class="clear">clear</a>
</form>
@ -21,32 +21,21 @@
<div class="wrapper">
<table>
<tr>
<th>Name</th>
<th>Tag</th>
<th>Created</th>
<th>Repository Name</th>
<th>Pull Command</th>
</tr>
{{ range $key, $value := .Repos }}
{{ range $key, $value := .Repositories }}
<tr>
<td valign="top">
{{ if $value.VulnURI }}<a href="{{ $value.VulnURI }}">{{ end }}
{{ $value.Name }}
{{ if $value.VulnURI }}</a>{{ end }}
</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 }}
<a href="/repo/{{ $value.Name | urlquery }}">
{{ $value.Name }}
</a>
</td>
<td align="right" nowrap>
{{ if $value.VulnURI }}<a href="{{ $value.VulnURI }}">{{ end }}
<code>docker pull {{ $value.RepoURI }}</code>
{{ if $value.VulnURI }}</a>{{ end }}
<a href="/repo/{{ $value.Name | urlquery }}">
<code>docker pull {{ $value.URI }}</code>
</a>
</td>
</tr>
{{ end }}
@ -55,7 +44,6 @@
<div class="footer">
<a href="https://twitter.com/jessfraz">@jessfraz</a>
<p>Last Updated: {{ .LastUpdated }}</p>
</div><!--/.footer-->
<script src="/js/scripts.js"></script>
<script>
@ -68,4 +56,4 @@ ga('send', 'pageview');
</script>
</body>
</html>
{{end}}
{{end}}

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

View file

@ -5,11 +5,8 @@ import (
"fmt"
"strings"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli/config"
"github.com/jessfraz/reg/clair"
"github.com/jessfraz/reg/registry"
"github.com/urfave/cli"
)
@ -78,35 +75,3 @@ func GetRepoAndRef(c *cli.Context) (repo, ref string, err error) {
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
}