Skip to content

Commit

Permalink
Merge pull request #22 from arminc/output-as-json-in-file
Browse files Browse the repository at this point in the history
Report vulnerabilities to file
  • Loading branch information
arminc authored Sep 24, 2017
2 parents d894f52 + 0560ba4 commit b21d72b
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 69 deletions.
23 changes: 23 additions & 0 deletions clair.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

Expand Down Expand Up @@ -55,3 +56,25 @@ func analyzeLayer(clairURL, path, layerName, parentLayerName string) {
Logger.Fatalf("Could not analyze layer, Clair responded with a failure: Got response %d with message %s", response.StatusCode, string(body))
}
}

func fetchLayerVulnerabilities(clairURL string, layerID string) v1.Layer {
response, err := http.Get(clairURL + fmt.Sprintf(getLayerFeaturesURI, layerID))
if err != nil {
Logger.Fatalf("Fetch vulnerabilities, Clair responded with a failure %v", err)
}
defer response.Body.Close()

if response.StatusCode != 200 {
body, _ := ioutil.ReadAll(response.Body)
Logger.Fatalf("Fetch vulnerabilities, Clair responded with a failure: Got response %d with message %s", response.StatusCode, string(body))
}

var apiResponse v1.LayerEnvelope
if err = json.NewDecoder(response.Body).Decode(&apiResponse); err != nil {
Logger.Fatalf("Fetch vulnerabilities, Could not decode response %v", err)
} else if apiResponse.Error != nil {
Logger.Fatalf("Fetch vulnerabilities, Response contains errors %s", apiResponse.Error.Message)
}

return *apiResponse.Layer
}
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func main() {
clair = app.StringOpt("c clair", "http://127.0.0.1:6060", "Clair url")
ip = app.StringOpt("ip", "localhost", "IP addres where clair-scanner is running on")
logFile = app.StringOpt("l log", "", "Log to a file")
reportFile = app.StringOpt("r report", "", "Report output file, as json")
imageName = app.StringArg("IMAGE", "", "Name of the Docker image to scan")
)

Expand All @@ -45,7 +46,7 @@ func main() {
log.Fatalf("Application interupted [%v]", s)
})

scan(*imageName, whitelist, *clair, *ip)
scan(*imageName, whitelist, *clair, *ip, *reportFile)
}
app.Run(os.Args)
}
Expand Down
129 changes: 61 additions & 68 deletions scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@ package main

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"

"github.com/coreos/clair/api/v1"
"github.com/fatih/color"
)

type vulnerabilityInfo struct {
vulnerability string
namespace string
severity string
Vulnerability string `json:"vulnerability"`
Namespace string `json:"namespace"`
Severity string `json:"severity"`
}

type acceptedVulnerability struct {
Expand All @@ -29,7 +23,13 @@ type vulnerabilitiesWhitelist struct {
Images map[string]map[string]string
}

func scan(imageName string, whitelist vulnerabilitiesWhitelist, clairURL string, scannerIP string) {
type vulnerabilityReport struct {
Image string `json:"image"`
Unaproved []string `json:"unaproved"`
Vulnerabilities []vulnerabilityInfo `json:"vulnerabilities"`
}

func scan(imageName string, whitelist vulnerabilitiesWhitelist, clairURL string, scannerIP string, reportFile string) {
//Create a temporary folder where the docker image layers are going to be stored
tmpPath := createTmpPath(tmpPrefix)
defer os.RemoveAll(tmpPath)
Expand All @@ -42,22 +42,64 @@ func scan(imageName string, whitelist vulnerabilitiesWhitelist, clairURL string,
defer server.Shutdown(nil)

analyzeLayers(layerIds, clairURL, scannerIP)
vulnerabilities, err := getVulnerabilities(clairURL, layerIds)
vulnerabilities := getVulnerabilities(clairURL, layerIds)

unapproved := vulnerabilitiesApproved(imageName, vulnerabilities, whitelist)
printReport(imageName, vulnerabilities, unapproved, reportFile)
}

func printReport(imageName string, vulnerabilities []vulnerabilityInfo, unapproved []string, file string) {
if len(unapproved) > 0 {
Logger.Infof("Unaproved vulnerabilities [%s]", unapproved)
} else {
Logger.Infof("Image [%s] not vulnerable", imageName)
}

if file != "" {
report := &vulnerabilityReport{
Image: imageName,
Vulnerabilities: vulnerabilities,
Unaproved: unapproved,
}
reportToFile(report, file)
}
}

func reportToFile(report *vulnerabilityReport, file string) {
reportJSON, err := json.MarshalIndent(report, "", " ")
if err != nil {
Logger.Fatalf("Analyzing failed: %s", err)
Logger.Fatalf("Could not create a report, report not proper json %v", err)
}
if err = ioutil.WriteFile(file, reportJSON, 0644); err != nil {
Logger.Fatalf("Could not create a report, could not write to file %v", err)
}
if err = vulnerabilitiesApproved(imageName, vulnerabilities, whitelist); err != nil {
Logger.Fatalf("Image contains unapproved vulnerabilities: %s", err)
}

func getVulnerabilities(clairURL string, layerIds []string) []vulnerabilityInfo {
var vulnerabilities = make([]vulnerabilityInfo, 0)
//Last layer gives you all the vulnerabilities of all layers
rawVulnerabilities := fetchLayerVulnerabilities(clairURL, layerIds[len(layerIds)-1])
if len(rawVulnerabilities.Features) == 0 {
Logger.Fatal("Could not fetch vulnerabilities. No features have been detected in the image. This usually means that the image isn't supported by Clair")
}

for _, feature := range rawVulnerabilities.Features {
if len(feature.Vulnerabilities) > 0 {
for _, vulnerability := range feature.Vulnerabilities {
vulnerability := vulnerabilityInfo{vulnerability.Name, vulnerability.NamespaceName, vulnerability.Severity}
vulnerabilities = append(vulnerabilities, vulnerability)
}
}
}
Logger.Infof("Image [%s] not vulnerable", imageName)
return vulnerabilities
}

func vulnerabilitiesApproved(imageName string, vulnerabilities []vulnerabilityInfo, whitelist vulnerabilitiesWhitelist) error {
var unapproved []string
func vulnerabilitiesApproved(imageName string, vulnerabilities []vulnerabilityInfo, whitelist vulnerabilitiesWhitelist) []string {
unapproved := []string{}
imageVulnerabilities := getImageVulnerabilities(imageName, whitelist.Images)

for i := 0; i < len(vulnerabilities); i++ {
vulnerability := vulnerabilities[i].vulnerability
vulnerability := vulnerabilities[i].Vulnerability
vulnerable := true

if _, exists := whitelist.GeneralWhitelist[vulnerability]; exists {
Expand All @@ -72,10 +114,7 @@ func vulnerabilitiesApproved(imageName string, vulnerabilities []vulnerabilityIn
unapproved = append(unapproved, vulnerability)
}
}
if len(unapproved) > 0 {
return fmt.Errorf("%s", unapproved)
}
return nil
return unapproved
}

func getImageVulnerabilities(imageName string, whitelistImageVulnerabilities map[string]map[string]string) map[string]string {
Expand All @@ -86,49 +125,3 @@ func getImageVulnerabilities(imageName string, whitelistImageVulnerabilities map
}
return imageVulnerabilities
}

func getVulnerabilities(clairURL string, layerIds []string) ([]vulnerabilityInfo, error) {
var vulnerabilities = make([]vulnerabilityInfo, 0)
//Last layer gives you all the vulnerabilities of all layers
rawVulnerabilities, err := fetchLayerVulnerabilities(clairURL, layerIds[len(layerIds)-1])
if err != nil {
return vulnerabilities, err
}
if len(rawVulnerabilities.Features) == 0 {
fmt.Printf("%s No features have been detected in the image. This usually means that the image isn't supported by Clair.\n", color.YellowString("NOTE:"))
return vulnerabilities, nil
}

for _, feature := range rawVulnerabilities.Features {
if len(feature.Vulnerabilities) > 0 {
for _, vulnerability := range feature.Vulnerabilities {
vulnerability := vulnerabilityInfo{vulnerability.Name, vulnerability.NamespaceName, vulnerability.Severity}
vulnerabilities = append(vulnerabilities, vulnerability)
}
}
}
return vulnerabilities, nil
}

func fetchLayerVulnerabilities(clairURL string, layerID string) (v1.Layer, error) {
response, err := http.Get(clairURL + fmt.Sprintf(getLayerFeaturesURI, layerID))
if err != nil {
return v1.Layer{}, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
body, _ := ioutil.ReadAll(response.Body)
err := fmt.Errorf("Got response %d with message %s", response.StatusCode, string(body))
return v1.Layer{}, err
}

var apiResponse v1.LayerEnvelope
if err = json.NewDecoder(response.Body).Decode(&apiResponse); err != nil {
return v1.Layer{}, err
} else if apiResponse.Error != nil {
return v1.Layer{}, errors.New(apiResponse.Error.Message)
}

return *apiResponse.Layer, nil
}

0 comments on commit b21d72b

Please sign in to comment.