Add experimental export of vulnerabilities to Grafeas server
This commit is contained in:
parent
db6379bc9e
commit
8397359296
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/coreos/clair/ext/featurens"
|
"github.com/coreos/clair/ext/featurens"
|
||||||
"github.com/coreos/clair/ext/notification"
|
"github.com/coreos/clair/ext/notification"
|
||||||
"github.com/coreos/clair/ext/vulnsrc"
|
"github.com/coreos/clair/ext/vulnsrc"
|
||||||
|
"github.com/coreos/clair/grafeas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrDatasourceNotLoaded is returned when the datasource variable in the
|
// ErrDatasourceNotLoaded is returned when the datasource variable in the
|
||||||
@ -50,6 +51,7 @@ type Config struct {
|
|||||||
Worker *clair.WorkerConfig
|
Worker *clair.WorkerConfig
|
||||||
Notifier *notification.Config
|
Notifier *notification.Config
|
||||||
API *api.Config
|
API *api.Config
|
||||||
|
Grafeas *grafeas.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig is a configuration that can be used as a fallback value.
|
// DefaultConfig is a configuration that can be used as a fallback value.
|
||||||
@ -75,6 +77,11 @@ func DefaultConfig() Config {
|
|||||||
Attempts: 5,
|
Attempts: 5,
|
||||||
RenotifyInterval: 2 * time.Hour,
|
RenotifyInterval: 2 * time.Hour,
|
||||||
},
|
},
|
||||||
|
Grafeas: &grafeas.Config{
|
||||||
|
Enabled: false,
|
||||||
|
Addr: "0.0.0.0:8080",
|
||||||
|
ProjectId: "vuln-scanner",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
"github.com/coreos/clair/ext/featurens"
|
"github.com/coreos/clair/ext/featurens"
|
||||||
"github.com/coreos/clair/ext/imagefmt"
|
"github.com/coreos/clair/ext/imagefmt"
|
||||||
"github.com/coreos/clair/ext/vulnsrc"
|
"github.com/coreos/clair/ext/vulnsrc"
|
||||||
|
"github.com/coreos/clair/grafeas"
|
||||||
"github.com/coreos/clair/pkg/formatter"
|
"github.com/coreos/clair/pkg/formatter"
|
||||||
"github.com/coreos/clair/pkg/stopper"
|
"github.com/coreos/clair/pkg/stopper"
|
||||||
"github.com/coreos/clair/pkg/strutil"
|
"github.com/coreos/clair/pkg/strutil"
|
||||||
@ -133,6 +134,8 @@ func Boot(config *Config) {
|
|||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
st := stopper.NewStopper()
|
st := stopper.NewStopper()
|
||||||
|
|
||||||
|
g := grafeas.NewGrafeas(config.Grafeas)
|
||||||
|
|
||||||
// Open database
|
// Open database
|
||||||
var db database.Datastore
|
var db database.Datastore
|
||||||
var dbError error
|
var dbError error
|
||||||
@ -162,7 +165,7 @@ func Boot(config *Config) {
|
|||||||
|
|
||||||
// Start updater
|
// Start updater
|
||||||
st.Begin()
|
st.Begin()
|
||||||
go clair.RunUpdater(config.Updater, db, st)
|
go clair.RunUpdater(config.Updater, db, st, g)
|
||||||
|
|
||||||
// Wait for interruption and shutdown gracefully.
|
// Wait for interruption and shutdown gracefully.
|
||||||
waitForSignals(syscall.SIGINT, syscall.SIGTERM)
|
waitForSignals(syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
@ -97,3 +97,8 @@ clair:
|
|||||||
|
|
||||||
# Optional HTTP Proxy: must be a valid URL (including the scheme).
|
# Optional HTTP Proxy: must be a valid URL (including the scheme).
|
||||||
proxy:
|
proxy:
|
||||||
|
|
||||||
|
grafeas:
|
||||||
|
enabled: false
|
||||||
|
addr: "http://grafeas.example.com"
|
||||||
|
projectId: "vuln-scanner"
|
@ -148,6 +148,7 @@ type Session interface {
|
|||||||
// already in the database.
|
// already in the database.
|
||||||
InsertVulnerabilities([]VulnerabilityWithAffected) error
|
InsertVulnerabilities([]VulnerabilityWithAffected) error
|
||||||
|
|
||||||
|
ListVulnerabilities() ([]NullableVulnerability, error)
|
||||||
// FindVulnerability retrieves a set of Vulnerabilities with affected
|
// FindVulnerability retrieves a set of Vulnerabilities with affected
|
||||||
// features.
|
// features.
|
||||||
FindVulnerabilities([]VulnerabilityID) ([]NullableVulnerability, error)
|
FindVulnerabilities([]VulnerabilityID) ([]NullableVulnerability, error)
|
||||||
|
@ -100,6 +100,13 @@ const (
|
|||||||
removeLockExpired = `DELETE FROM LOCK WHERE until < CURRENT_TIMESTAMP`
|
removeLockExpired = `DELETE FROM LOCK WHERE until < CURRENT_TIMESTAMP`
|
||||||
|
|
||||||
// vulnerability.go
|
// vulnerability.go
|
||||||
|
listVulnerabilities = `
|
||||||
|
SELECT v.id, v.name, v.description, v.link, v.severity, v.metadata, n.name, n.version_format
|
||||||
|
FROM vulnerability AS v, namespace AS n
|
||||||
|
WHERE v.namespace_id = n.id
|
||||||
|
AND v.deleted_at IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
searchVulnerability = `
|
searchVulnerability = `
|
||||||
SELECT v.id, v.description, v.link, v.severity, v.metadata, n.version_format
|
SELECT v.id, v.description, v.link, v.severity, v.metadata, n.version_format
|
||||||
FROM vulnerability AS v, namespace AS n
|
FROM vulnerability AS v, namespace AS n
|
||||||
|
@ -46,6 +46,93 @@ type affectedFeatureRows struct {
|
|||||||
rows map[int64]database.AffectedFeature
|
rows map[int64]database.AffectedFeature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tx *pgSession) ListVulnerabilities() ([]database.NullableVulnerability, error) {
|
||||||
|
defer observeQueryTime("listVulnerabilities", "", time.Now())
|
||||||
|
vulnIDMap := map[int64][]*database.NullableVulnerability{}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(listVulnerabilities)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := stmt.Query()
|
||||||
|
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
stmt.Close()
|
||||||
|
return nil, handleError("listVulnerabilities", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
// load vulnerabilities
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
id sql.NullInt64
|
||||||
|
vuln = database.NullableVulnerability{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&id,
|
||||||
|
&vuln.Name,
|
||||||
|
&vuln.Description,
|
||||||
|
&vuln.Link,
|
||||||
|
&vuln.Severity,
|
||||||
|
&vuln.Metadata,
|
||||||
|
&vuln.Namespace.Name,
|
||||||
|
&vuln.Namespace.VersionFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
stmt.Close()
|
||||||
|
return nil, handleError("searchVulnerability", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vuln.Valid = id.Valid
|
||||||
|
if id.Valid {
|
||||||
|
vulnIDMap[id.Int64] = append(vulnIDMap[id.Int64], &vuln)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stmt.Close(); err != nil {
|
||||||
|
return nil, handleError("listVulnerabilities", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
toQuery := make([]int64, 0, len(vulnIDMap))
|
||||||
|
for id := range vulnIDMap {
|
||||||
|
toQuery = append(toQuery, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load vulnerability affected features
|
||||||
|
rows, err = tx.Query(searchVulnerabilityAffected, pq.Array(toQuery))
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleError("searchVulnerabilityAffected", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
id int64
|
||||||
|
f database.AffectedFeature
|
||||||
|
)
|
||||||
|
|
||||||
|
err := rows.Scan(&id, &f.FeatureName, &f.AffectedVersion, &f.FixedInVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleError("searchVulnerabilityAffected", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vuln := range vulnIDMap[id] {
|
||||||
|
f.Namespace = vuln.Namespace
|
||||||
|
vuln.Affected = append(vuln.Affected, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultVuln []database.NullableVulnerability
|
||||||
|
for _, vulns := range vulnIDMap {
|
||||||
|
for _, vuln := range vulns {
|
||||||
|
resultVuln = append(resultVuln, *vuln)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVuln, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *pgSession) FindVulnerabilities(vulnerabilities []database.VulnerabilityID) ([]database.NullableVulnerability, error) {
|
func (tx *pgSession) FindVulnerabilities(vulnerabilities []database.VulnerabilityID) ([]database.NullableVulnerability, error) {
|
||||||
defer observeQueryTime("findVulnerabilities", "", time.Now())
|
defer observeQueryTime("findVulnerabilities", "", time.Now())
|
||||||
resultVuln := make([]database.NullableVulnerability, len(vulnerabilities))
|
resultVuln := make([]database.NullableVulnerability, len(vulnerabilities))
|
||||||
|
198
grafeas/grafeas.go
Normal file
198
grafeas/grafeas.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
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{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/coreos/clair/database"
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/ext/vulnmdsrc"
|
"github.com/coreos/clair/ext/vulnmdsrc"
|
||||||
"github.com/coreos/clair/ext/vulnsrc"
|
"github.com/coreos/clair/ext/vulnsrc"
|
||||||
|
"github.com/coreos/clair/grafeas"
|
||||||
"github.com/coreos/clair/pkg/stopper"
|
"github.com/coreos/clair/pkg/stopper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ type vulnerabilityChange struct {
|
|||||||
|
|
||||||
// RunUpdater begins a process that updates the vulnerability database at
|
// RunUpdater begins a process that updates the vulnerability database at
|
||||||
// regular intervals.
|
// regular intervals.
|
||||||
func RunUpdater(config *UpdaterConfig, datastore database.Datastore, st *stopper.Stopper) {
|
func RunUpdater(config *UpdaterConfig, datastore database.Datastore, st *stopper.Stopper, g grafeas.Grafeas) {
|
||||||
defer st.End()
|
defer st.End()
|
||||||
|
|
||||||
// Do not run the updater if there is no config or if the interval is 0.
|
// Do not run the updater if there is no config or if the interval is 0.
|
||||||
@ -129,6 +130,8 @@ func RunUpdater(config *UpdaterConfig, datastore database.Datastore, st *stopper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.Export(datastore)
|
||||||
|
|
||||||
// Unlock the updater.
|
// Unlock the updater.
|
||||||
unlock(datastore, updaterLockName, whoAmI)
|
unlock(datastore, updaterLockName, whoAmI)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user