Skip to content

Commit

Permalink
Whitelist (#98)
Browse files Browse the repository at this point in the history
* Added support for whitelisting CVEs, deduping the list returned by
Clair, and additional detail in the output report.

* Fixed bug that was returning dupes and removed deduping function. A few
semantic changes per code review.

* Fixed pointer issue with the v3 API as well.
  • Loading branch information
thereverendtom authored and hashmap committed May 16, 2018
1 parent be29731 commit ae9dbba
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 24 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ need to be booted with `-insecure-tls` for this to work.

* `JSON_OUTPUT` - Output JSON, not plain text. Default is `false`.

* `WHITELIST_FILE` - Path to the YAML file with the CVE whitelist. Look at `whitelist-example.yaml` for the file format.

Usage:

CLAIR_ADDR=localhost CLAIR_OUTPUT=High CLAIR_THRESHOLD=10 DOCKER_USER=docker DOCKER_PASSWORD=secret klar postgres:9.5.1
Expand Down
14 changes: 12 additions & 2 deletions clair/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ func (a *apiV1) Analyze(image *docker.Image) ([]*Vulnerability, error) {
for _, f := range envelope.Layer.Features {
for _, v := range f.Vulnerabilities {
v.FeatureName = f.Name
vs = append(vs, &v)
v.FeatureVersion = f.Version
//the for loop uses the same variable for "v", reloading with new values
//since we are appending a pointer to the variable to the slice, we need to create a copy of the struct
//otherwise the slice winds up with multiple pointers to the same struct
vulnerability := v
vs = append(vs, &vulnerability)
}
}
return vs, nil
Expand Down Expand Up @@ -179,7 +184,12 @@ func (a *apiV3) Analyze(image *docker.Image) ([]*Vulnerability, error) {
for _, v := range f.Vulnerabilities {
cv := convertVulnerability(v)
cv.FeatureName = f.Name
vs = append(vs, cv)
cv.FeatureVersion = f.Version
//the for loop uses the same variable for "cv", reloading with new values
//since we are appending a pointer to the variable to the slice, we need to create a copy of the struct
//otherwise the slice winds up with multiple pointers to the same struct
vulnerability := cv
vs = append(vs, vulnerability)
}
}
return vs, nil
Expand Down
19 changes: 10 additions & 9 deletions clair/clair.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ type feature struct {

// Vulnerability represents vulnerability entity returned by Clair
type Vulnerability struct {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
Description string `json:"Description,omitempty"`
Link string `json:"Link,omitempty"`
Severity string `json:"Severity,omitempty"`
Metadata map[string]interface{} `json:"Metadata,omitempty"`
FixedBy string `json:"FixedBy,omitempty"`
FixedIn []feature `json:"FixedIn,omitempty"`
FeatureName string `json:"featureName",omitempty`
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
Description string `json:"Description,omitempty"`
Link string `json:"Link,omitempty"`
Severity string `json:"Severity,omitempty"`
Metadata map[string]interface{} `json:"Metadata,omitempty"`
FixedBy string `json:"FixedBy,omitempty"`
FixedIn []feature `json:"FixedIn,omitempty"`
FeatureName string `json:"featureName",omitempty`
FeatureVersion string `json:"featureName",omitempty`
}

type layerError struct {
Expand Down
73 changes: 62 additions & 11 deletions klar.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@ import (
"strconv"
"strings"
"time"
"io/ioutil"

"github.com/optiopay/klar/clair"
"github.com/optiopay/klar/docker"
"github.com/optiopay/klar/utils"

"gopkg.in/yaml.v2"
)

//Used to represent the structure of the whitelist YAML file
type vulnerabilitiesWhitelistYAML struct {
General []string
Images map[string][]string
}

//Map structure used for ease of searching for whitelisted vulnerabilites
type vulnerabilitiesWhitelist struct {
General map[string]bool //key: CVE and value: true
Images map[string]map[string]bool //key: image name and value: [key: CVE and value: true]
}

const (
optionClairOutput = "CLAIR_OUTPUT"
optionClairAddress = "CLAIR_ADDR"
Expand All @@ -25,6 +40,7 @@ const (
optionDockerToken = "DOCKER_TOKEN"
optionDockerInsecure = "DOCKER_INSECURE"
optionRegistryInsecure = "REGISTRY_INSECURE"
optionWhiteListFile = "WHITELIST_FILE"
)

var priorities = []string{"Unknown", "Negligible", "Low", "Medium", "High", "Critical", "Defcon1"}
Expand Down Expand Up @@ -73,12 +89,13 @@ type jsonOutput struct {
}

type config struct {
ClairAddr string
ClairOutput string
Threshold int
JSONOutput bool
ClairTimeout time.Duration
DockerConfig docker.Config
ClairAddr string
ClairOutput string
Threshold int
JSONOutput bool
ClairTimeout time.Duration
DockerConfig docker.Config
WhiteListFile string
}

func newConfig(args []string) (*config, error) {
Expand Down Expand Up @@ -107,11 +124,12 @@ func newConfig(args []string) (*config, error) {
}

return &config{
ClairAddr: clairAddr,
ClairOutput: clairOutput,
Threshold: parseIntOption(optionClairThreshold),
JSONOutput: parseBoolOption(optionJSONOutput),
ClairTimeout: time.Duration(clairTimeout) * time.Minute,
ClairAddr: clairAddr,
ClairOutput: clairOutput,
Threshold: parseIntOption(optionClairThreshold),
JSONOutput: parseBoolOption(optionJSONOutput),
ClairTimeout: time.Duration(clairTimeout) * time.Minute,
WhiteListFile: os.Getenv(optionWhiteListFile),
DockerConfig: docker.Config{
ImageName: args[1],
User: os.Getenv(optionDockerUser),
Expand All @@ -123,3 +141,36 @@ func newConfig(args []string) (*config, error) {
},
}, nil
}

//Parse the whitelist file
func parseWhitelistFile(whitelistFile string) (*vulnerabilitiesWhitelist, error) {
whitelistYAML := vulnerabilitiesWhitelistYAML{}
whitelist := vulnerabilitiesWhitelist{}

//read the whitelist file
whitelistBytes, err := ioutil.ReadFile(whitelistFile)
if err != nil {
return nil, fmt.Errorf("could not read file %v", err)
}
if err = yaml.Unmarshal(whitelistBytes, &whitelistYAML); err != nil {
return nil, fmt.Errorf("could not unmarshal %v", err)
}

//Initialize the whitelist maps
whitelist.General = make(map[string]bool)
whitelist.Images = make(map[string]map[string]bool)

//Populate the maps
for _,cve := range whitelistYAML.General {
whitelist.General[cve] = true
}

for image,cveList := range whitelistYAML.Images {
whitelist.Images[image] = make(map[string]bool)
for _,cve := range cveList {
whitelist.Images[image][cve] = true
}
}

return &whitelist, nil
}
49 changes: 47 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/optiopay/klar/clair"
"github.com/optiopay/klar/docker"
Expand All @@ -28,6 +29,17 @@ func main() {

fmt.Fprintf(os.Stderr, "clair timeout %s\n", conf.ClairTimeout)
fmt.Fprintf(os.Stderr, "docker timeout: %s\n", conf.DockerConfig.Timeout)

whitelist := &vulnerabilitiesWhitelist{}
if (conf.WhiteListFile != "") {
fmt.Fprintf(os.Stderr, "whitelist file: %s\n", conf.WhiteListFile)
whitelist, err = parseWhitelistFile(conf.WhiteListFile)
if err != nil {
fail("Could not parse whitelist file: %s", err)
}
} else {
fmt.Fprintf(os.Stderr, "no whitelist file\n")
}

image, err := docker.NewImage(&conf.DockerConfig)
if err != nil {
Expand Down Expand Up @@ -70,6 +82,11 @@ func main() {
fail("Failed to analyze, exiting")
}

//apply whitelist
numVulnerabilites := len(vs)
vs = filterWhitelist(whitelist,vs)
numVulnerabilitiesAfterWhitelist := len(vs)

groupBySeverity(vs)
vsNumber := 0

Expand All @@ -81,15 +98,22 @@ func main() {
enc := json.NewEncoder(os.Stdout)
enc.Encode(output)
} else {
if numVulnerabilitiesAfterWhitelist < numVulnerabilites {
//display how many vulnerabilities were whitelisted
fmt.Printf("Whitelisted %d vulnerabilities\n", numVulnerabilites - numVulnerabilitiesAfterWhitelist)
}
fmt.Printf("Found %d vulnerabilities\n", len(vs))
iteratePriorities(priorities[0], func(sev string) { fmt.Printf("%s: %d\n", sev, len(store[sev])) })
fmt.Printf("\n")

iteratePriorities(conf.ClairOutput, func(sev string) {
vsNumber += len(store[sev])
for _, v := range store[sev] {
fmt.Printf("%s: [%s] \nFound in: %s\n%s\n%s\n", v.Name, v.Severity, v.FeatureName, v.Description, v.Link)
fmt.Printf("%s: [%s] \nFound in: %s [%s]\nFixed By: %s\n%s\n%s\n", v.Name, v.Severity, v.FeatureName, v.FeatureVersion, v.FixedBy, v.Description, v.Link)
fmt.Println("-----------------------------------------")
}
})
iteratePriorities(priorities[0], func(sev string) { fmt.Printf("%s: %d\n", sev, len(store[sev])) })

}

if vsNumber > conf.Threshold {
Expand Down Expand Up @@ -129,3 +153,24 @@ func vulnsBy(sev string, store map[string][]*clair.Vulnerability) []*clair.Vulne
}
return items
}

//Filter out whitelisted vulnerabilites
func filterWhitelist(whitelist *vulnerabilitiesWhitelist, vs []*clair.Vulnerability) []*clair.Vulnerability {
generalWhitelist := whitelist.General
imageWhitelist := whitelist.Images

filteredVs := make([]*clair.Vulnerability, 0, len(vs))

for _, v := range vs {
if _, exists := generalWhitelist[v.Name]; !exists {
//vulnerability is not in the general whitelist, so get the image name by removing ":version" from the value returned via the Clair API
imageName := strings.Split(v.NamespaceName, ":")[0]
if _, exists := imageWhitelist[imageName][v.Name]; !exists {
//vulnerability is not in the image whitelist, so add it to the list to return
filteredVs = append(filteredVs, v)
}
}
}

return filteredVs
}
8 changes: 8 additions & 0 deletions whitelist-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
general:
- RHSA-2018:1345
images:
centos:
- RHSA-2017:2479
- RHSA-2018:0805
alpine:
- CVE-2017-9671

0 comments on commit ae9dbba

Please sign in to comment.