199 lines
4.8 KiB
Go
199 lines
4.8 KiB
Go
package grafeas
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/coreos/clair/database"
|
|
"github.com/grafeas/client-go/v1alpha1"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type Config struct {
|
|
Enabled bool
|
|
Addr string
|
|
ProjectId string
|
|
}
|
|
|
|
type Grafeas struct {
|
|
Config *Config
|
|
}
|
|
|
|
func NewGrafeas(config *Config) Grafeas {
|
|
return Grafeas{config}
|
|
}
|
|
|
|
func (g *Grafeas) Export(datastore database.Datastore) error {
|
|
if !g.Config.Enabled {
|
|
return nil
|
|
}
|
|
|
|
log.Info("exporting vulnerabilities to Grafeas")
|
|
|
|
tx, err := datastore.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer tx.Rollback()
|
|
vulnerabilities, err := tx.ListVulnerabilities()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pID := g.Config.ProjectId
|
|
client := v1alpha1.NewGrafeasApiWithBasePath(g.Config.Addr)
|
|
for _, vuln := range vulnerabilities {
|
|
nID := vuln.Name
|
|
score, nistVectors := extractMetadata(vuln.Metadata)
|
|
note, _, err := client.GetNote(pID, nID)
|
|
createNewNote := false
|
|
if err != nil {
|
|
n := noteWithoutDetails(pID, nID, vuln.Description, string(vuln.Severity), nistVectors, score)
|
|
note = &n
|
|
createNewNote = true
|
|
}
|
|
|
|
containsUpdatedDetail := false
|
|
for _, affected := range vuln.Affected {
|
|
cpeUri := createCpeUri(affected.Namespace.Name)
|
|
detail := detail(cpeUri, affected.FeatureName, vuln.Description, string(vuln.Severity), affected.FixedInVersion)
|
|
index := findDetail(note.VulnerabilityType.Details, detail)
|
|
if index == -1 {
|
|
note.VulnerabilityType.Details = append(note.VulnerabilityType.Details, detail)
|
|
containsUpdatedDetail = true
|
|
} else if !reflect.DeepEqual(note.VulnerabilityType.Details[index], detail) {
|
|
note.VulnerabilityType.Details[index] = detail
|
|
containsUpdatedDetail = true
|
|
}
|
|
}
|
|
|
|
if createNewNote {
|
|
_, _, err = client.CreateNote(pID, nID, *note)
|
|
} else if containsUpdatedDetail {
|
|
_, _, err = client.UpdateNote(pID, nID, *note)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Warn("Error creating note %v", err)
|
|
}
|
|
}
|
|
|
|
log.Info("export done")
|
|
|
|
return nil
|
|
}
|
|
|
|
func extractMetadata(metadata map[string]interface{}) (score float32, vectors string) {
|
|
if nvd, ok := metadata["NVD"].(map[string]interface{}); ok {
|
|
if cvss, ok := nvd["CVSSv2"].(map[string]interface{}); ok {
|
|
score = float32(cvss["Score"].(float64))
|
|
vectors = cvss["Vectors"].(string)
|
|
}
|
|
}
|
|
|
|
return score, vectors
|
|
}
|
|
|
|
// Clair does not report cpe uri:s so we'll have to create one from the namespace.
|
|
func createCpeUri(namespaceName string) string {
|
|
ss := strings.Split(namespaceName, ":")
|
|
if len(ss) != 2 {
|
|
return "CPE_UNSPECIFIED"
|
|
}
|
|
os := ss[0]
|
|
ver := ss[1]
|
|
|
|
switch os {
|
|
case "alpine":
|
|
return "cpe:/o:alpine:alpine_linux:" + ver
|
|
case "debian":
|
|
return "cpe:/o:debian:debian_linux:" + ver
|
|
case "ubuntu":
|
|
return "cpe:/o:canonical:ubuntu_linux:" + ver
|
|
case "centos":
|
|
return "cpe:/o:centos:centos:" + ver
|
|
case "rhel":
|
|
return "cpe:/o:redhat:enterprise_linux:" + ver
|
|
case "fedora":
|
|
return "cpe:/o:fedoraproject:fedora:" + ver
|
|
}
|
|
|
|
return "CPE_UNSPECIFIED"
|
|
}
|
|
|
|
func findDetail(details []v1alpha1.Detail, detail v1alpha1.Detail) int {
|
|
for i, d := range details {
|
|
if d.CpeUri == detail.CpeUri && d.Package_ == detail.Package_ {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func fixedLocation(fixedBy, cpeUri, pkg string) v1alpha1.VulnerabilityLocation {
|
|
var version v1alpha1.Version
|
|
if fixedBy == "" {
|
|
version = v1alpha1.Version{
|
|
Kind: "MAXIMUM",
|
|
}
|
|
} else {
|
|
// EVR: Epoch:Version.Revision
|
|
evrRegexp := regexp.MustCompile(`(?:(\d*):)?([\w~]+[\w.~]*)-(~?\w+[\w.]*)`)
|
|
matches := evrRegexp.FindStringSubmatch(fixedBy)
|
|
if len(matches) != 0 {
|
|
versionEpoch, _ := strconv.ParseInt(matches[1], 10, 32)
|
|
versionName := matches[2]
|
|
versionRev := matches[3]
|
|
version = v1alpha1.Version{
|
|
Epoch: int32(versionEpoch),
|
|
Name: versionName,
|
|
Revision: versionRev,
|
|
}
|
|
} else {
|
|
version = v1alpha1.Version{
|
|
Name: fixedBy,
|
|
}
|
|
}
|
|
}
|
|
return v1alpha1.VulnerabilityLocation{
|
|
CpeUri: cpeUri,
|
|
Package_: pkg,
|
|
Version: version,
|
|
}
|
|
}
|
|
|
|
func detail(cpeUri, packageName, description, severity, fixedBy string) v1alpha1.Detail {
|
|
return v1alpha1.Detail{
|
|
CpeUri: cpeUri,
|
|
Package_: packageName,
|
|
Description: description,
|
|
MinAffectedVersion: v1alpha1.Version{
|
|
Kind: "MINIMUM",
|
|
},
|
|
SeverityName: severity,
|
|
FixedLocation: fixedLocation(fixedBy, cpeUri, packageName),
|
|
}
|
|
}
|
|
|
|
func noteWithoutDetails(pID, name, description, severity, nistVectors string, score float32) v1alpha1.Note {
|
|
var longDescription string
|
|
if nistVectors != "" {
|
|
longDescription = fmt.Sprintf("NIST vectors: %v", nistVectors)
|
|
}
|
|
return v1alpha1.Note{
|
|
Name: fmt.Sprintf("projects/%v/notes/%v", pID, name),
|
|
ShortDescription: name,
|
|
LongDescription: longDescription,
|
|
Kind: "PACKAGE_VULNERABILITY",
|
|
VulnerabilityType: v1alpha1.VulnerabilityType{
|
|
CvssScore: score,
|
|
Severity: severity,
|
|
Details: []v1alpha1.Detail{},
|
|
},
|
|
}
|
|
}
|