database: fix notification design and add vulnerability history
This commit is contained in:
parent
99f3552470
commit
94ece7bf2b
@ -162,7 +162,7 @@ type Notification struct {
|
|||||||
Page string `json:"Page,omitempty"`
|
Page string `json:"Page,omitempty"`
|
||||||
NextPage string `json:"NextPage,omitempty"`
|
NextPage string `json:"NextPage,omitempty"`
|
||||||
Old *VulnerabilityWithLayers `json:"Old,omitempty"`
|
Old *VulnerabilityWithLayers `json:"Old,omitempty"`
|
||||||
New VulnerabilityWithLayers `json:"New,omitempty"`
|
New *VulnerabilityWithLayers `json:"New,omitempty"`
|
||||||
Changed []string `json:"Changed,omitempty"`
|
Changed []string `json:"Changed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +172,11 @@ func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotifica
|
|||||||
*oldVuln = VulnerabilityWithLayersFromDatabaseModel(*dbNotification.OldVulnerability)
|
*oldVuln = VulnerabilityWithLayersFromDatabaseModel(*dbNotification.OldVulnerability)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var newVuln *VulnerabilityWithLayers
|
||||||
|
if dbNotification.NewVulnerability != nil {
|
||||||
|
*newVuln = VulnerabilityWithLayersFromDatabaseModel(*dbNotification.NewVulnerability)
|
||||||
|
}
|
||||||
|
|
||||||
var nextPageStr string
|
var nextPageStr string
|
||||||
if nextPage != database.NoVulnerabilityNotificationPage {
|
if nextPage != database.NoVulnerabilityNotificationPage {
|
||||||
nextPageStr = DBPageNumberToString(nextPage)
|
nextPageStr = DBPageNumberToString(nextPage)
|
||||||
@ -187,7 +192,7 @@ func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotifica
|
|||||||
Page: DBPageNumberToString(page),
|
Page: DBPageNumberToString(page),
|
||||||
NextPage: nextPageStr,
|
NextPage: nextPageStr,
|
||||||
Old: oldVuln,
|
Old: oldVuln,
|
||||||
New: VulnerabilityWithLayersFromDatabaseModel(dbNotification.NewVulnerability),
|
New: newVuln,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +61,9 @@ type FeatureVersion struct {
|
|||||||
type Vulnerability struct {
|
type Vulnerability struct {
|
||||||
Model
|
Model
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
Namespace Namespace
|
Namespace Namespace
|
||||||
|
|
||||||
Description string
|
Description string
|
||||||
Link string
|
Link string
|
||||||
Severity types.Priority
|
Severity types.Priority
|
||||||
@ -102,7 +103,7 @@ type VulnerabilityNotification struct {
|
|||||||
Deleted time.Time
|
Deleted time.Time
|
||||||
|
|
||||||
OldVulnerability *Vulnerability
|
OldVulnerability *Vulnerability
|
||||||
NewVulnerability Vulnerability
|
NewVulnerability *Vulnerability
|
||||||
}
|
}
|
||||||
|
|
||||||
type VulnerabilityNotificationPageNumber struct {
|
type VulnerabilityNotificationPageNumber struct {
|
||||||
|
@ -155,6 +155,4 @@ func TestRaceAffects(t *testing.T) {
|
|||||||
assert.Len(t, utils.CompareStringLists(expectedAffectedNames, actualAffectedNames), 0)
|
assert.Len(t, utils.CompareStringLists(expectedAffectedNames, actualAffectedNames), 0)
|
||||||
assert.Len(t, utils.CompareStringLists(actualAffectedNames, expectedAffectedNames), 0)
|
assert.Len(t, utils.CompareStringLists(actualAffectedNames, expectedAffectedNames), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(Quentin-M): May be worth having a test for updates as well.
|
|
||||||
}
|
}
|
||||||
|
@ -89,8 +89,8 @@ CREATE TABLE IF NOT EXISTS Vulnerability (
|
|||||||
link VARCHAR(128) NULL,
|
link VARCHAR(128) NULL,
|
||||||
severity severity NOT NULL,
|
severity severity NOT NULL,
|
||||||
metadata TEXT NULL,
|
metadata TEXT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE,
|
||||||
UNIQUE (namespace_id, name));
|
deleted_at TIMESTAMP WITH TIME ZONE NULL);
|
||||||
|
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
@ -152,8 +152,8 @@ CREATE TABLE IF NOT EXISTS Vulnerability_Notification (
|
|||||||
created_at TIMESTAMP WITH TIME ZONE,
|
created_at TIMESTAMP WITH TIME ZONE,
|
||||||
notified_at TIMESTAMP WITH TIME ZONE NULL,
|
notified_at TIMESTAMP WITH TIME ZONE NULL,
|
||||||
deleted_at TIMESTAMP WITH TIME ZONE NULL,
|
deleted_at TIMESTAMP WITH TIME ZONE NULL,
|
||||||
old_vulnerability TEXT NULL,
|
old_vulnerability_id INT NULL REFERENCES Vulnerability ON DELETE CASCADE,
|
||||||
new_vulnerability TEXT);
|
new_vulnerability_id INT NULL REFERENCES Vulnerability ON DELETE CASCADE);
|
||||||
|
|
||||||
CREATE INDEX ON Vulnerability_Notification (notified_at);
|
CREATE INDEX ON Vulnerability_Notification (notified_at);
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package pgsql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/clair/database"
|
"github.com/coreos/clair/database"
|
||||||
@ -13,29 +12,13 @@ import (
|
|||||||
|
|
||||||
// do it in tx so we won't insert/update a vuln without notification and vice-versa.
|
// do it in tx so we won't insert/update a vuln without notification and vice-versa.
|
||||||
// name and created doesn't matter.
|
// name and created doesn't matter.
|
||||||
// Vuln ID must be filled in.
|
func createNotification(tx *sql.Tx, oldVulnerabilityID, newVulnerabilityID int) error {
|
||||||
func (pgSQL *pgSQL) insertNotification(tx *sql.Tx, notification database.VulnerabilityNotification) error {
|
defer observeQueryTime("createNotification", "all", time.Now())
|
||||||
defer observeQueryTime("insertNotification", "all", time.Now())
|
|
||||||
|
|
||||||
// Marshal old and new Vulnerabilities.
|
|
||||||
var oldVulnerability sql.NullString
|
|
||||||
if notification.OldVulnerability != nil {
|
|
||||||
oldVulnerabilityJSON, err := json.Marshal(notification.OldVulnerability)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return cerrors.NewBadRequestError("could not marshal old Vulnerability in insertNotification")
|
|
||||||
}
|
|
||||||
oldVulnerability = sql.NullString{String: string(oldVulnerabilityJSON), Valid: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
newVulnerability, err := json.Marshal(notification.NewVulnerability)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return cerrors.NewBadRequestError("could not marshal new Vulnerability in insertNotification")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert Notification.
|
// Insert Notification.
|
||||||
_, err = tx.Exec(getQuery("i_notification"), uuid.New(), oldVulnerability, newVulnerability)
|
oldVulnerabilityNullableID := sql.NullInt64{Int64: int64(oldVulnerabilityID), Valid: oldVulnerabilityID != 0}
|
||||||
|
newVulnerabilityNullableID := sql.NullInt64{Int64: int64(newVulnerabilityID), Valid: newVulnerabilityID != 0}
|
||||||
|
_, err := tx.Exec(getQuery("i_notification"), uuid.New(), oldVulnerabilityNullableID, newVulnerabilityNullableID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return handleError("i_notification", err)
|
return handleError("i_notification", err)
|
||||||
@ -51,7 +34,7 @@ func (pgSQL *pgSQL) GetAvailableNotification(renotifyInterval time.Duration) (da
|
|||||||
|
|
||||||
before := time.Now().Add(-renotifyInterval)
|
before := time.Now().Add(-renotifyInterval)
|
||||||
row := pgSQL.QueryRow(getQuery("s_notification_available"), before)
|
row := pgSQL.QueryRow(getQuery("s_notification_available"), before)
|
||||||
notification, err := scanNotification(row, false)
|
notification, err := pgSQL.scanNotification(row, false)
|
||||||
|
|
||||||
return notification, handleError("s_notification_available", err)
|
return notification, handleError("s_notification_available", err)
|
||||||
}
|
}
|
||||||
@ -60,20 +43,28 @@ func (pgSQL *pgSQL) GetNotification(name string, limit int, page database.Vulner
|
|||||||
defer observeQueryTime("GetNotification", "all", time.Now())
|
defer observeQueryTime("GetNotification", "all", time.Now())
|
||||||
|
|
||||||
// Get Notification.
|
// Get Notification.
|
||||||
notification, err := scanNotification(pgSQL.QueryRow(getQuery("s_notification"), name), true)
|
notification, err := pgSQL.scanNotification(pgSQL.QueryRow(getQuery("s_notification"), name), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return notification, page, handleError("s_notification", err)
|
return notification, page, handleError("s_notification", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load vulnerabilities' LayersIntroducingVulnerability.
|
// Load vulnerabilities' LayersIntroducingVulnerability.
|
||||||
page.OldVulnerability, err = pgSQL.loadLayerIntroducingVulnerability(
|
page.OldVulnerability, err = pgSQL.loadLayerIntroducingVulnerability(
|
||||||
notification.OldVulnerability, limit, page.OldVulnerability)
|
notification.OldVulnerability,
|
||||||
|
limit,
|
||||||
|
page.OldVulnerability,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return notification, page, err
|
return notification, page, err
|
||||||
}
|
}
|
||||||
|
|
||||||
page.NewVulnerability, err = pgSQL.loadLayerIntroducingVulnerability(
|
page.NewVulnerability, err = pgSQL.loadLayerIntroducingVulnerability(
|
||||||
¬ification.NewVulnerability, limit, page.NewVulnerability)
|
notification.NewVulnerability,
|
||||||
|
limit,
|
||||||
|
page.NewVulnerability,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return notification, page, err
|
return notification, page, err
|
||||||
}
|
}
|
||||||
@ -81,22 +72,35 @@ func (pgSQL *pgSQL) GetNotification(name string, limit int, page database.Vulner
|
|||||||
return notification, page, nil
|
return notification, page, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanNotification(row *sql.Row, hasVulns bool) (notification database.VulnerabilityNotification, err error) {
|
func (pgSQL *pgSQL) scanNotification(row *sql.Row, hasVulns bool) (database.VulnerabilityNotification, error) {
|
||||||
|
var notification database.VulnerabilityNotification
|
||||||
var created zero.Time
|
var created zero.Time
|
||||||
var notified zero.Time
|
var notified zero.Time
|
||||||
var deleted zero.Time
|
var deleted zero.Time
|
||||||
var oldVulnerability []byte
|
var oldVulnerabilityNullableID sql.NullInt64
|
||||||
var newVulnerability []byte
|
var newVulnerabilityNullableID sql.NullInt64
|
||||||
|
|
||||||
// Query notification.
|
// Scan notification.
|
||||||
if hasVulns {
|
if hasVulns {
|
||||||
err = row.Scan(¬ification.ID, ¬ification.Name, &created, ¬ified, &deleted,
|
err := row.Scan(
|
||||||
&oldVulnerability, &newVulnerability)
|
¬ification.ID,
|
||||||
|
¬ification.Name,
|
||||||
|
&created,
|
||||||
|
¬ified,
|
||||||
|
&deleted,
|
||||||
|
&oldVulnerabilityNullableID,
|
||||||
|
&newVulnerabilityNullableID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return notification, err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
err = row.Scan(¬ification.ID, ¬ification.Name, &created, ¬ified, &deleted)
|
err := row.Scan(¬ification.ID, ¬ification.Name, &created, ¬ified, &deleted)
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return notification, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.Created = created.Time
|
notification.Created = created.Time
|
||||||
@ -104,19 +108,26 @@ func scanNotification(row *sql.Row, hasVulns bool) (notification database.Vulner
|
|||||||
notification.Deleted = deleted.Time
|
notification.Deleted = deleted.Time
|
||||||
|
|
||||||
if hasVulns {
|
if hasVulns {
|
||||||
// Unmarshal old and new Vulnerabilities.
|
if oldVulnerabilityNullableID.Valid {
|
||||||
err = json.Unmarshal(oldVulnerability, notification.OldVulnerability)
|
vulnerability, err := pgSQL.findVulnerabilityByIDWithDeleted(int(oldVulnerabilityNullableID.Int64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = cerrors.NewBadRequestError("could not unmarshal old Vulnerability in GetNotification")
|
return notification, err
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.OldVulnerability = &vulnerability
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(newVulnerability, ¬ification.NewVulnerability)
|
if newVulnerabilityNullableID.Valid {
|
||||||
if err != nil {
|
vulnerability, err := pgSQL.findVulnerabilityByIDWithDeleted(int(newVulnerabilityNullableID.Int64))
|
||||||
err = cerrors.NewBadRequestError("could not unmarshal new Vulnerability in GetNotification")
|
if err != nil {
|
||||||
|
return notification, err
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.NewVulnerability = &vulnerability
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return notification, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fills Vulnerability.LayersIntroducingVulnerability.
|
// Fills Vulnerability.LayersIntroducingVulnerability.
|
||||||
|
@ -4,8 +4,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/database"
|
"github.com/coreos/clair/database"
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
cerrors "github.com/coreos/clair/utils/errors"
|
||||||
"github.com/coreos/clair/utils/types"
|
"github.com/coreos/clair/utils/types"
|
||||||
@ -60,64 +58,68 @@ func TestNotification(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if assert.Nil(t, datastore.InsertLayer(l1)) && assert.Nil(t, datastore.InsertLayer(l2)) &&
|
if !assert.Nil(t, datastore.InsertLayer(l1)) || !assert.Nil(t, datastore.InsertLayer(l2)) || !assert.Nil(t, datastore.InsertLayer(l3)) {
|
||||||
assert.Nil(t, datastore.InsertLayer(l3)) {
|
return
|
||||||
|
|
||||||
// Insert a new vulnerability that is introduced by three layers.
|
|
||||||
v1 := database.Vulnerability{
|
|
||||||
Name: "TestNotificationVulnerability1",
|
|
||||||
Namespace: f1.Namespace,
|
|
||||||
Description: "TestNotificationDescription1",
|
|
||||||
Link: "TestNotificationLink1",
|
|
||||||
Severity: "Unknown",
|
|
||||||
FixedIn: []database.FeatureVersion{
|
|
||||||
database.FeatureVersion{
|
|
||||||
Feature: f1,
|
|
||||||
Version: types.NewVersionUnsafe("1.0"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Nil(t, datastore.insertVulnerability(v1))
|
|
||||||
|
|
||||||
// Get the notification associated to the previously inserted vulnerability.
|
|
||||||
notification, err := datastore.GetAvailableNotification(time.Second)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.NotEmpty(t, notification.Name)
|
|
||||||
|
|
||||||
// Verify the renotify behaviour.
|
|
||||||
if assert.Nil(t, datastore.SetNotificationNotified(notification.Name)) {
|
|
||||||
_, err := datastore.GetAvailableNotification(time.Second)
|
|
||||||
assert.Equal(t, cerrors.ErrNotFound, err)
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
notificationB, err := datastore.GetAvailableNotification(20 * time.Millisecond)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, notification.Name, notificationB.Name)
|
|
||||||
|
|
||||||
datastore.SetNotificationNotified(notification.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get notification.
|
|
||||||
filledNotification, nextPage, err := datastore.GetNotification(notification.Name, 2, database.VulnerabilityNotificationFirstPage)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.NotEqual(t, database.NoVulnerabilityNotificationPage, nextPage)
|
|
||||||
assert.Nil(t, filledNotification.OldVulnerability)
|
|
||||||
assert.Equal(t, v1.Name, filledNotification.NewVulnerability.Name)
|
|
||||||
assert.Len(t, filledNotification.NewVulnerability.LayersIntroducingVulnerability, 2)
|
|
||||||
|
|
||||||
// Get second page.
|
|
||||||
filledNotification, nextPage, err = datastore.GetNotification(notification.Name, 2, nextPage)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, database.NoVulnerabilityNotificationPage, nextPage)
|
|
||||||
assert.Nil(t, filledNotification.OldVulnerability)
|
|
||||||
assert.Equal(t, v1.Name, filledNotification.NewVulnerability.Name)
|
|
||||||
assert.Len(t, filledNotification.NewVulnerability.LayersIntroducingVulnerability, 1)
|
|
||||||
|
|
||||||
// Delete notification.
|
|
||||||
assert.Nil(t, datastore.DeleteNotification(notification.Name))
|
|
||||||
|
|
||||||
n, err := datastore.GetAvailableNotification(time.Millisecond)
|
|
||||||
assert.Equal(t, cerrors.ErrNotFound, err)
|
|
||||||
fmt.Println(n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert a new vulnerability that is introduced by three layers.
|
||||||
|
v1 := database.Vulnerability{
|
||||||
|
Name: "TestNotificationVulnerability1",
|
||||||
|
Namespace: f1.Namespace,
|
||||||
|
Description: "TestNotificationDescription1",
|
||||||
|
Link: "TestNotificationLink1",
|
||||||
|
Severity: "Unknown",
|
||||||
|
FixedIn: []database.FeatureVersion{
|
||||||
|
database.FeatureVersion{
|
||||||
|
Feature: f1,
|
||||||
|
Version: types.NewVersionUnsafe("1.0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Nil(t, datastore.insertVulnerability(v1, false))
|
||||||
|
|
||||||
|
// Get the notification associated to the previously inserted vulnerability.
|
||||||
|
notification, err := datastore.GetAvailableNotification(time.Second)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotEmpty(t, notification.Name)
|
||||||
|
|
||||||
|
// Verify the renotify behaviour.
|
||||||
|
if assert.Nil(t, datastore.SetNotificationNotified(notification.Name)) {
|
||||||
|
_, err := datastore.GetAvailableNotification(time.Second)
|
||||||
|
assert.Equal(t, cerrors.ErrNotFound, err)
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
notificationB, err := datastore.GetAvailableNotification(20 * time.Millisecond)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, notification.Name, notificationB.Name)
|
||||||
|
|
||||||
|
datastore.SetNotificationNotified(notification.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get notification.
|
||||||
|
filledNotification, nextPage, err := datastore.GetNotification(notification.Name, 2, database.VulnerabilityNotificationFirstPage)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotEqual(t, database.NoVulnerabilityNotificationPage, nextPage)
|
||||||
|
assert.Nil(t, filledNotification.OldVulnerability)
|
||||||
|
assert.Equal(t, v1.Name, filledNotification.NewVulnerability.Name)
|
||||||
|
assert.Len(t, filledNotification.NewVulnerability.LayersIntroducingVulnerability, 2)
|
||||||
|
|
||||||
|
// Get second page.
|
||||||
|
filledNotification, nextPage, err = datastore.GetNotification(notification.Name, 2, nextPage)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, database.NoVulnerabilityNotificationPage, nextPage)
|
||||||
|
assert.Nil(t, filledNotification.OldVulnerability)
|
||||||
|
assert.Equal(t, v1.Name, filledNotification.NewVulnerability.Name)
|
||||||
|
assert.Len(t, filledNotification.NewVulnerability.LayersIntroducingVulnerability, 1)
|
||||||
|
|
||||||
|
// Delete notification.
|
||||||
|
assert.Nil(t, datastore.DeleteNotification(notification.Name))
|
||||||
|
|
||||||
|
_, err = datastore.GetAvailableNotification(time.Millisecond)
|
||||||
|
assert.Equal(t, cerrors.ErrNotFound, err)
|
||||||
|
|
||||||
|
// Update a vulnerability and ensure that the old/new vulnerabilities are correct.
|
||||||
|
|
||||||
|
// Delete a vulnerability and verify the notification.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,8 @@ func init() {
|
|||||||
WHERE vafv.featureversion_id = ANY($1::integer[])
|
WHERE vafv.featureversion_id = ANY($1::integer[])
|
||||||
AND vafv.vulnerability_id = v.id
|
AND vafv.vulnerability_id = v.id
|
||||||
AND vafv.fixedin_id = vfif.id
|
AND vafv.fixedin_id = vfif.id
|
||||||
AND v.namespace_id = vn.id`
|
AND v.namespace_id = vn.id
|
||||||
|
AND v.deleted_at IS NULL`
|
||||||
|
|
||||||
queries["i_layer"] = `
|
queries["i_layer"] = `
|
||||||
INSERT INTO Layer(name, engineversion, parent_id, namespace_id)
|
INSERT INTO Layer(name, engineversion, parent_id, namespace_id)
|
||||||
@ -143,63 +144,43 @@ func init() {
|
|||||||
queries["r_lock_expired"] = `DELETE FROM LOCK WHERE until < CURRENT_TIMESTAMP`
|
queries["r_lock_expired"] = `DELETE FROM LOCK WHERE until < CURRENT_TIMESTAMP`
|
||||||
|
|
||||||
// vulnerability.go
|
// vulnerability.go
|
||||||
queries["f_vulnerability"] = `
|
queries["f_vulnerability_base"] = `
|
||||||
SELECT v.id, n.id, v.description, v.link, v.severity, v.metadata, vfif.version, f.id, f.Name
|
SELECT v.id, v.name, n.id, n.name, v.description, v.link, v.severity, v.metadata
|
||||||
FROM Vulnerability v
|
FROM Vulnerability v JOIN Namespace n ON v.namespace_id = n.id`
|
||||||
JOIN Namespace n ON v.namespace_id = n.id
|
|
||||||
LEFT JOIN Vulnerability_FixedIn_Feature vfif ON v.id = vfif.vulnerability_id
|
|
||||||
LEFT JOIN Feature f ON vfif.feature_id = f.id
|
|
||||||
WHERE n.Name = $1 AND v.Name = $2`
|
|
||||||
|
|
||||||
queries["f_vulnerability_for_update"] = `
|
queries["f_vulnerability_for_update"] = ` FOR UPDATE OF v`
|
||||||
SELECT FOR UPDATE v.id, n.id, v.description, v.link, v.severity, v.metadata, vfif.version, f.id, f.Name
|
queries["f_vulnerability_+by_name_namespace"] = ` WHERE n.name = $1 AND v.name = $2 AND v.deleted_at IS NULL`
|
||||||
FROM Vulnerability v
|
queries["f_vulnerability_+by_id"] = ` WHERE v.id = $1`
|
||||||
JOIN Namespace n ON v.namespace_id = n.id
|
|
||||||
LEFT JOIN Vulnerability_FixedIn_Feature vfif ON v.id = vfif.vulnerability_id
|
queries["f_vulnerability_fixedin"] = `
|
||||||
LEFT JOIN Feature f ON vfif.feature_id = f.id
|
SELECT vfif.version, f.id, f.Name
|
||||||
WHERE n.Name = $1 AND v.Name = $2`
|
FROM Vulnerability_FixedIn_Feature vfif JOIN Feature f ON vfif.feature_id = f.id
|
||||||
|
WHERE vfif.vulnerability_id = $1`
|
||||||
|
|
||||||
queries["i_vulnerability"] = `
|
queries["i_vulnerability"] = `
|
||||||
INSERT INTO Vulnerability(namespace_id, name, description, link, severity, metadata)
|
INSERT INTO Vulnerability(namespace_id, name, description, link, severity, metadata)
|
||||||
VALUES($1, $2, $3, $4, $5, $6)
|
VALUES($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id`
|
RETURNING id`
|
||||||
|
|
||||||
queries["u_vulnerability"] = `
|
|
||||||
UPDATE Vulnerability
|
|
||||||
SET description = $2, link = $3, severity = $4, metadata = $5
|
|
||||||
WHERE id = $1`
|
|
||||||
|
|
||||||
queries["i_vulnerability_fixedin_feature"] = `
|
queries["i_vulnerability_fixedin_feature"] = `
|
||||||
INSERT INTO Vulnerability_FixedIn_Feature(vulnerability_id, feature_id, version)
|
INSERT INTO Vulnerability_FixedIn_Feature(vulnerability_id, feature_id, version)
|
||||||
VALUES($1, $2, $3)
|
VALUES($1, $2, $3)
|
||||||
RETURNING id`
|
RETURNING id`
|
||||||
|
|
||||||
queries["u_vulnerability_fixedin_feature"] = `
|
|
||||||
UPDATE Vulnerability_FixedIn_Feature
|
|
||||||
SET version = $3
|
|
||||||
WHERE vulnerability_id = $1 AND feature_id = $2
|
|
||||||
RETURNING id`
|
|
||||||
|
|
||||||
queries["r_vulnerability_fixedin_feature"] = `
|
|
||||||
DELETE FROM Vulnerability_FixedIn_Feature
|
|
||||||
WHERE vulnerability_id = $1 AND feature_id = $2
|
|
||||||
RETURNING id`
|
|
||||||
|
|
||||||
queries["r_vulnerability_affects_featureversion"] = `
|
|
||||||
DELETE FROM Vulnerability_Affects_FeatureVersion
|
|
||||||
WHERE fixedin_id = $1`
|
|
||||||
|
|
||||||
queries["f_featureversion_by_feature"] = `
|
queries["f_featureversion_by_feature"] = `
|
||||||
SELECT id, version FROM FeatureVersion WHERE feature_id = $1`
|
SELECT id, version FROM FeatureVersion WHERE feature_id = $1`
|
||||||
|
|
||||||
queries["r_vulnerability"] = `
|
queries["r_vulnerability"] = `
|
||||||
DELETE FROM Vulnerability
|
UPDATE Vulnerability
|
||||||
|
SET deleted_at = CURRENT_TIMESTAMP
|
||||||
WHERE namespace_id = (SELECT id FROM Namespace WHERE name = $1)
|
WHERE namespace_id = (SELECT id FROM Namespace WHERE name = $1)
|
||||||
AND name = $2`
|
AND name = $2
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
RETURNING id`
|
||||||
|
|
||||||
// notification.go
|
// notification.go
|
||||||
queries["i_notification"] = `
|
queries["i_notification"] = `
|
||||||
INSERT INTO Vulnerability_Notification(name, created_at, old_vulnerability, new_vulnerability)
|
INSERT INTO Vulnerability_Notification(name, created_at, old_vulnerability_id, new_vulnerability_id)
|
||||||
VALUES($1, CURRENT_TIMESTAMP, $2, $3)`
|
VALUES($1, CURRENT_TIMESTAMP, $2, $3)`
|
||||||
|
|
||||||
queries["u_notification_notified"] = `
|
queries["u_notification_notified"] = `
|
||||||
@ -222,7 +203,7 @@ func init() {
|
|||||||
LIMIT 1`
|
LIMIT 1`
|
||||||
|
|
||||||
queries["s_notification"] = `
|
queries["s_notification"] = `
|
||||||
SELECT id, name, created_at, notified_at, deleted_at, old_vulnerability, new_vulnerability
|
SELECT id, name, created_at, notified_at, deleted_at, old_vulnerability_id, new_vulnerability_id
|
||||||
FROM Vulnerability_Notification
|
FROM Vulnerability_Notification
|
||||||
WHERE name = $1`
|
WHERE name = $1`
|
||||||
|
|
||||||
|
@ -35,36 +35,67 @@ func (pgSQL *pgSQL) FindVulnerability(namespaceName, name string) (database.Vuln
|
|||||||
func findVulnerability(queryer Queryer, namespaceName, name string, forUpdate bool) (database.Vulnerability, error) {
|
func findVulnerability(queryer Queryer, namespaceName, name string, forUpdate bool) (database.Vulnerability, error) {
|
||||||
defer observeQueryTime("findVulnerability", "all", time.Now())
|
defer observeQueryTime("findVulnerability", "all", time.Now())
|
||||||
|
|
||||||
vulnerability := database.Vulnerability{
|
|
||||||
Name: name,
|
|
||||||
Namespace: database.Namespace{
|
|
||||||
Name: namespaceName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find Vulnerability.
|
|
||||||
queryName := "f_vulnerability"
|
queryName := "f_vulnerability"
|
||||||
|
query := getQuery("f_vulnerability_base") + getQuery("f_vulnerability_+by_name_namespace")
|
||||||
if forUpdate {
|
if forUpdate {
|
||||||
queryName = "f_vulnerability_for_update"
|
queryName = queryName + "+for_update"
|
||||||
|
query = query + getQuery("f_vulnerability_for_update")
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := queryer.Query(getQuery(queryName), namespaceName, name)
|
return scanVulnerability(queryer, queryName, queryer.QueryRow(query, namespaceName, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) findVulnerabilityByIDWithDeleted(id int) (database.Vulnerability, error) {
|
||||||
|
defer observeQueryTime("findVulnerabilityByIDWithDeleted", "all", time.Now())
|
||||||
|
|
||||||
|
queryName := "f_vulnerability"
|
||||||
|
query := getQuery("f_vulnerability_base") + getQuery("f_vulnerability_+by_id")
|
||||||
|
|
||||||
|
return scanVulnerability(pgSQL, queryName, pgSQL.QueryRow(query, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql.Row) (database.Vulnerability, error) {
|
||||||
|
var vulnerability database.Vulnerability
|
||||||
|
|
||||||
|
err := vulnerabilityRow.Scan(
|
||||||
|
&vulnerability.ID,
|
||||||
|
&vulnerability.Name,
|
||||||
|
&vulnerability.Namespace.ID,
|
||||||
|
&vulnerability.Namespace.Name,
|
||||||
|
&vulnerability.Description,
|
||||||
|
&vulnerability.Link,
|
||||||
|
&vulnerability.Severity,
|
||||||
|
&vulnerability.Metadata,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vulnerability, handleError(queryName, err)
|
return vulnerability, handleError(queryName+".Scan()", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vulnerability.ID == 0 {
|
||||||
|
return vulnerability, cerrors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the FixedIn FeatureVersion now.
|
||||||
|
rows, err := queryer.Query(getQuery("f_vulnerability_fixedin"), vulnerability.ID)
|
||||||
|
if err != nil {
|
||||||
|
return vulnerability, handleError("f_vulnerability_fixedin.Scan()", err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
// Iterate to scan the Vulnerability and its FixedIn FeatureVersions.
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var featureVersionID zero.Int
|
var featureVersionID zero.Int
|
||||||
var featureVersionVersion zero.String
|
var featureVersionVersion zero.String
|
||||||
var featureVersionFeatureName zero.String
|
var featureVersionFeatureName zero.String
|
||||||
|
|
||||||
err := rows.Scan(&vulnerability.ID, &vulnerability.Namespace.ID, &vulnerability.Description,
|
err := rows.Scan(
|
||||||
&vulnerability.Link, &vulnerability.Severity, &vulnerability.Metadata,
|
&featureVersionVersion,
|
||||||
&featureVersionVersion, &featureVersionID, &featureVersionFeatureName)
|
&featureVersionID,
|
||||||
|
&featureVersionFeatureName,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vulnerability, handleError(queryName+".Scan()", err)
|
return vulnerability, handleError("f_vulnerability_fixedin.Scan()", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !featureVersionID.IsZero() {
|
if !featureVersionID.IsZero() {
|
||||||
@ -82,11 +113,9 @@ func findVulnerability(queryer Queryer, namespaceName, name string, forUpdate bo
|
|||||||
vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion)
|
vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return vulnerability, handleError("s_featureversions_vulnerabilities.Rows()", err)
|
if err := rows.Err(); err != nil {
|
||||||
}
|
return vulnerability, handleError("f_vulnerability_fixedin.Rows()", err)
|
||||||
if vulnerability.ID == 0 {
|
|
||||||
return vulnerability, cerrors.ErrNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return vulnerability, nil
|
return vulnerability, nil
|
||||||
@ -96,7 +125,7 @@ func findVulnerability(queryer Queryer, namespaceName, name string, forUpdate bo
|
|||||||
// By setting the fixed version to minVersion, we can say that the vuln does'nt affect anymore.
|
// By setting the fixed version to minVersion, we can say that the vuln does'nt affect anymore.
|
||||||
func (pgSQL *pgSQL) InsertVulnerabilities(vulnerabilities []database.Vulnerability) error {
|
func (pgSQL *pgSQL) InsertVulnerabilities(vulnerabilities []database.Vulnerability) error {
|
||||||
for _, vulnerability := range vulnerabilities {
|
for _, vulnerability := range vulnerabilities {
|
||||||
err := pgSQL.insertVulnerability(vulnerability)
|
err := pgSQL.insertVulnerability(vulnerability, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%#v\n", vulnerability)
|
fmt.Printf("%#v\n", vulnerability)
|
||||||
return err
|
return err
|
||||||
@ -105,22 +134,27 @@ func (pgSQL *pgSQL) InsertVulnerabilities(vulnerabilities []database.Vulnerabili
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability) error {
|
func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, onlyFixedIn bool) error {
|
||||||
tf := time.Now()
|
tf := time.Now()
|
||||||
|
|
||||||
// Verify parameters
|
// Verify parameters
|
||||||
if vulnerability.Name == "" || len(vulnerability.FixedIn) == 0 ||
|
if vulnerability.Name == "" || vulnerability.Namespace.Name == "" {
|
||||||
vulnerability.Namespace.Name == "" || !vulnerability.Severity.IsValid() {
|
return cerrors.NewBadRequestError("insertVulnerability needs at least the Name and the Namespace")
|
||||||
log.Warning("could not insert an invalid vulnerability")
|
|
||||||
return cerrors.NewBadRequestError("could not insert an invalid vulnerability")
|
|
||||||
}
|
}
|
||||||
|
if !onlyFixedIn && !vulnerability.Severity.IsValid() {
|
||||||
|
msg := fmt.Sprintf("could not insert a vulnerability that has an invalid Severity: %s", vulnerability.Severity)
|
||||||
|
log.Warning(msg)
|
||||||
|
return cerrors.NewBadRequestError(msg)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(vulnerability.FixedIn); i++ {
|
||||||
|
fifv := &vulnerability.FixedIn[i]
|
||||||
|
|
||||||
for _, fixedInFeatureVersion := range vulnerability.FixedIn {
|
if fifv.Feature.Namespace.Name == "" {
|
||||||
if fixedInFeatureVersion.Feature.Namespace.Name == "" {
|
// As there is no Namespace on that FixedIn FeatureVersion, set it to the Vulnerability's
|
||||||
fixedInFeatureVersion.Feature.Namespace.Name = vulnerability.Namespace.Name
|
// Namespace.
|
||||||
} else if fixedInFeatureVersion.Feature.Namespace.Name != vulnerability.Namespace.Name {
|
fifv.Feature.Namespace.Name = vulnerability.Namespace.Name
|
||||||
msg := "could not insert an invalid vulnerability: FixedIn FeatureVersion must be in the " +
|
} else if fifv.Feature.Namespace.Name != vulnerability.Namespace.Name {
|
||||||
"same namespace as the Vulnerability"
|
msg := "could not insert an invalid vulnerability that contains FixedIn FeatureVersion that are not in the same namespace as the Vulnerability"
|
||||||
log.Warning(msg)
|
log.Warning(msg)
|
||||||
return cerrors.NewBadRequestError(msg)
|
return cerrors.NewBadRequestError(msg)
|
||||||
}
|
}
|
||||||
@ -129,12 +163,6 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability) er
|
|||||||
// We do `defer observeQueryTime` here because we don't want to observe invalid vulnerabilities.
|
// We do `defer observeQueryTime` here because we don't want to observe invalid vulnerabilities.
|
||||||
defer observeQueryTime("insertVulnerability", "all", tf)
|
defer observeQueryTime("insertVulnerability", "all", tf)
|
||||||
|
|
||||||
// Find or insert Vulnerability's Namespace.
|
|
||||||
namespaceID, err := pgSQL.insertNamespace(vulnerability.Namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin transaction.
|
// Begin transaction.
|
||||||
tx, err := pgSQL.Begin()
|
tx, err := pgSQL.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -142,71 +170,84 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability) er
|
|||||||
return handleError("insertVulnerability.Begin()", err)
|
return handleError("insertVulnerability.Begin()", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find vulnerability and its Vulnerability_FixedIn_Features.
|
// Find existing vulnerability and its Vulnerability_FixedIn_Features (for update).
|
||||||
existingVulnerability, err := findVulnerability(tx, vulnerability.Namespace.Name,
|
existingVulnerability, err := findVulnerability(tx, vulnerability.Namespace.Name, vulnerability.Name, true)
|
||||||
vulnerability.Name, true)
|
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
if err != nil && err != cerrors.ErrNotFound {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert or update vulnerability.
|
if onlyFixedIn {
|
||||||
if existingVulnerability.ID == 0 {
|
// Because this call tries to update FixedIn FeatureVersion, import all other data from the
|
||||||
// The vulnerability is a new one, insert it.
|
// existing one.
|
||||||
err = tx.QueryRow(getQuery("i_vulnerability"), namespaceID, vulnerability.Name,
|
if existingVulnerability.ID == 0 {
|
||||||
vulnerability.Description, vulnerability.Link, &vulnerability.Severity,
|
return cerrors.ErrNotFound
|
||||||
&vulnerability.Metadata).Scan(&vulnerability.ID)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return handleError("i_vulnerability", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The vulnerability exists, update it.
|
|
||||||
if vulnerability.Description != existingVulnerability.Description ||
|
|
||||||
vulnerability.Link != existingVulnerability.Link ||
|
|
||||||
vulnerability.Severity != existingVulnerability.Severity ||
|
|
||||||
!reflect.DeepEqual(castMetadata(vulnerability.Metadata), existingVulnerability.Metadata) {
|
|
||||||
_, err = tx.Exec(getQuery("u_vulnerability"), existingVulnerability.ID,
|
|
||||||
vulnerability.Description, vulnerability.Link, &vulnerability.Severity,
|
|
||||||
&vulnerability.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return handleError("u_vulnerability", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vulnerability.ID = existingVulnerability.ID
|
fixedIn := vulnerability.FixedIn
|
||||||
|
vulnerability = existingVulnerability
|
||||||
|
vulnerability.FixedIn = fixedIn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the new/updated/removed FeatureVersions and the resulting full list.
|
if existingVulnerability.ID != 0 {
|
||||||
var newFIFV, updatedFIFV, removedFIFV []database.FeatureVersion
|
updateMetadata := vulnerability.Description != existingVulnerability.Description ||
|
||||||
if existingVulnerability.ID == 0 {
|
vulnerability.Link != existingVulnerability.Link ||
|
||||||
// The vulnerability is a new new, the new FeatureVersions are the entire list of FixedIn.
|
vulnerability.Severity != existingVulnerability.Severity ||
|
||||||
newFIFV = vulnerability.FixedIn
|
!reflect.DeepEqual(castMetadata(vulnerability.Metadata), existingVulnerability.Metadata)
|
||||||
} else {
|
|
||||||
// The vulnerability exists, compute the lists using diffFixedIn.
|
// Construct the entire list of FixedIn FeatureVersion, by using the
|
||||||
// We overwrite vulnerability.FixedIn with the entire list of FixedIn FeatureVersions, we'll
|
// the FixedIn list of the old vulnerability.
|
||||||
// then use the vulnerability in the notification, with that list instead of a potential diff.
|
//
|
||||||
newFIFV, updatedFIFV, removedFIFV, vulnerability.FixedIn =
|
// TODO(Quentin-M): We could use !updateFixedIn to just copy FixedIn/Affects rows from the
|
||||||
diffFixedIn(existingVulnerability.FixedIn, vulnerability.FixedIn)
|
// existing vulnerability in order to make metadata updates much faster.
|
||||||
|
fixedIn, updateFixedIn := applyFixedInDiff(existingVulnerability.FixedIn, vulnerability.FixedIn)
|
||||||
|
vulnerability.FixedIn = fixedIn
|
||||||
|
|
||||||
|
if !updateMetadata && !updateFixedIn {
|
||||||
|
tx.Commit()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the old vulnerability as non latest.
|
||||||
|
_, err = tx.Exec(getQuery("r_vulnerability"), vulnerability.Namespace.Name, vulnerability.Name)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return handleError("r_vulnerability", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or insert Vulnerability's Namespace.
|
||||||
|
namespaceID, err := pgSQL.insertNamespace(vulnerability.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert vulnerability.
|
||||||
|
err = tx.QueryRow(
|
||||||
|
getQuery("i_vulnerability"),
|
||||||
|
namespaceID,
|
||||||
|
vulnerability.Name,
|
||||||
|
vulnerability.Description,
|
||||||
|
vulnerability.Link,
|
||||||
|
&vulnerability.Severity,
|
||||||
|
&vulnerability.Metadata,
|
||||||
|
).Scan(&vulnerability.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return handleError("i_vulnerability", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Vulnerability_FixedIn_Feature and Vulnerability_Affects_FeatureVersion now.
|
// Update Vulnerability_FixedIn_Feature and Vulnerability_Affects_FeatureVersion now.
|
||||||
if err = pgSQL.updateVulnerabilityFeatureVersions(tx, vulnerability.ID, newFIFV, updatedFIFV,
|
err = pgSQL.insertVulnerabilityFixedInFeatureVersions(tx, vulnerability.ID, vulnerability.FixedIn)
|
||||||
removedFIFV); err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create notification.
|
// Create a notification.
|
||||||
notification := database.VulnerabilityNotification{
|
err = createNotification(tx, existingVulnerability.ID, vulnerability.ID)
|
||||||
NewVulnerability: vulnerability,
|
if err != nil {
|
||||||
}
|
|
||||||
if existingVulnerability.ID != 0 {
|
|
||||||
notification.OldVulnerability = &existingVulnerability
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pgSQL.insertNotification(tx, notification); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,55 +272,48 @@ func castMetadata(m database.MetadataMap) database.MetadataMap {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func diffFixedIn(existingFIFVList, newFIFVList []database.FeatureVersion) (newFIFV, updatedFIFV, removedFIFV, allFIFV []database.FeatureVersion) {
|
// applyFixedInDiff applies a FeatureVersion diff on a FeatureVersion list and returns the result.
|
||||||
// Build FeatureVersion.Feature.Namespace.Name:FeatureVersion.Feature.Name (NaN) structures.
|
func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.FeatureVersion, bool) {
|
||||||
allFIFVMap, _ := createFeatureVersionNameMap(existingFIFVList)
|
currentMap, currentNames := createFeatureVersionNameMap(currentList)
|
||||||
vulnerabilityFixedInNameMap, vulnerabilityFixedInNameSlice := createFeatureVersionNameMap(newFIFVList)
|
diffMap, diffNames := createFeatureVersionNameMap(diff)
|
||||||
existingFixedInMapNameMap, existingFixedInNameSlice := createFeatureVersionNameMap(existingFIFVList)
|
|
||||||
|
|
||||||
// Calculate the new FixedIn FeatureVersion NaN and updated ones.
|
addedNames := utils.CompareStringLists(diffNames, currentNames)
|
||||||
newFixedInName := utils.CompareStringLists(vulnerabilityFixedInNameSlice,
|
inBothNames := utils.CompareStringListsInBoth(diffNames, currentNames)
|
||||||
existingFixedInNameSlice)
|
|
||||||
updatedFixedInName := utils.CompareStringListsInBoth(vulnerabilityFixedInNameSlice,
|
different := false
|
||||||
existingFixedInNameSlice)
|
|
||||||
|
for _, name := range addedNames {
|
||||||
|
if diffMap[name].Version == types.MinVersion {
|
||||||
|
// MinVersion only makes sense when a Feature is already fixed in some version,
|
||||||
|
// in which case we would be in the "inBothNames".
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMap[name] = diffMap[name]
|
||||||
|
different = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range inBothNames {
|
||||||
|
fv := diffMap[name]
|
||||||
|
|
||||||
for _, nan := range newFixedInName {
|
|
||||||
fv := vulnerabilityFixedInNameMap[nan]
|
|
||||||
if fv.Version == types.MinVersion {
|
if fv.Version == types.MinVersion {
|
||||||
// We don't want to mark a Feature as fixed in MinVersion. MinVersion only makes sense when a
|
// MinVersion means that the Feature doesn't affect the Vulnerability anymore.
|
||||||
// Feature is already marked as fixed in some version, in which case we would be in the
|
delete(currentMap, name)
|
||||||
// "updatedFixedInFeatureVersions" loop and removes the fixed in mark.
|
different = true
|
||||||
continue
|
} else if fv.Version != currentMap[name].Version {
|
||||||
}
|
// The version got updated.
|
||||||
|
currentMap[name] = diffMap[name]
|
||||||
newFIFV = append(newFIFV, fv)
|
different = true
|
||||||
allFIFVMap[fv.Feature.Namespace.Name+":"+fv.Feature.Name] = fv
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nan := range updatedFixedInName {
|
|
||||||
fv := existingFixedInMapNameMap[nan]
|
|
||||||
fv.Version = vulnerabilityFixedInNameMap[nan].Version
|
|
||||||
|
|
||||||
if existingFixedInMapNameMap[nan].Version == fv.Version {
|
|
||||||
// Versions are actually the same!
|
|
||||||
// Even though they appear in both lists, it's not an update.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if fv.Version != types.MinVersion {
|
|
||||||
updatedFIFV = append(updatedFIFV, fv)
|
|
||||||
allFIFVMap[fv.Feature.Namespace.Name+":"+fv.Feature.Name] = fv
|
|
||||||
} else {
|
|
||||||
removedFIFV = append(removedFIFV, fv)
|
|
||||||
delete(allFIFVMap, fv.Feature.Namespace.Name+":"+fv.Feature.Name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fv := range allFIFVMap {
|
// Convert currentMap to a slice and return it.
|
||||||
allFIFV = append(allFIFV, fv)
|
var newList []database.FeatureVersion
|
||||||
|
for _, fv := range currentMap {
|
||||||
|
newList = append(newList, fv)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return newList, different
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFeatureVersionNameMap(features []database.FeatureVersion) (map[string]database.FeatureVersion, []string) {
|
func createFeatureVersionNameMap(features []database.FeatureVersion) (map[string]database.FeatureVersion, []string) {
|
||||||
@ -295,123 +329,19 @@ func createFeatureVersionNameMap(features []database.FeatureVersion) (map[string
|
|||||||
return m, s
|
return m, s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pgSQL *pgSQL) InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []database.FeatureVersion) error {
|
// insertVulnerabilityFixedInFeatureVersions populates Vulnerability_FixedIn_Feature for the given
|
||||||
// Verify parameters
|
// vulnerability with the specified database.FeatureVersion list and uses
|
||||||
for _, fifv := range fixes {
|
// linkVulnerabilityToFeatureVersions to propagate the changes on Vulnerability_FixedIn_Feature to
|
||||||
if fifv.Feature.Namespace.Name == "" {
|
// Vulnerability_Affects_FeatureVersion.
|
||||||
fifv.Feature.Namespace.Name = vulnerabilityNamespace
|
func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulnerabilityID int, fixedIn []database.FeatureVersion) error {
|
||||||
} else if fifv.Feature.Namespace.Name != vulnerabilityNamespace {
|
defer observeQueryTime("insertVulnerabilityFixedInFeatureVersions", "all", time.Now())
|
||||||
msg := "could not add/update a FixedIn FeatureVersion: FixedIn FeatureVersion must be in the " +
|
|
||||||
"same namespace as the Vulnerability"
|
|
||||||
log.Warning(msg)
|
|
||||||
return cerrors.NewBadRequestError(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f := func(vulnerability database.Vulnerability) (newFIFV, updatedFIFV, removedFIFV, allFIFV []database.FeatureVersion, err error) {
|
|
||||||
newFIFV, updatedFIFV, _, allFIFV = diffFixedIn(vulnerability.FixedIn, fixes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return pgSQL.doVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error {
|
|
||||||
f := func(vulnerability database.Vulnerability) (newFIFV, updatedFIFV, removedFIFV, allFIFV []database.FeatureVersion, err error) {
|
|
||||||
// Search the specified featureName.
|
|
||||||
for i, vulnerabilityFV := range vulnerability.FixedIn {
|
|
||||||
if vulnerabilityFV.Feature.Name == featureName {
|
|
||||||
removedFIFV = append(removedFIFV, vulnerabilityFV)
|
|
||||||
allFIFV = append(vulnerability.FixedIn[:i], vulnerability.FixedIn[i+1:]...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cerrors.ErrNotFound
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return pgSQL.doVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// doVulnerabilityFixes is used by InsertVulnerabilityFixes and DeleteVulnerabilityFix. It
|
|
||||||
// adds/updates/removes FeatureVersions on the specified vulnerability using
|
|
||||||
// updateVulnerabilityFeatureVersions and creates a database.VulnerabilityNotification.
|
|
||||||
func (pgSQL *pgSQL) doVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, f func(vulnerability database.Vulnerability) (newFIFV, updatedFIFV, removedFIFV, allFIFV []database.FeatureVersion, err error)) error {
|
|
||||||
// Begin transaction.
|
|
||||||
tx, err := pgSQL.Begin()
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return handleError("doVulnerabilityFixes.Begin()", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select for update the vulnerability in order to prevent everyone else from executing updates
|
|
||||||
// on the vulnerability (and consequently on Vulnerability_FixedIn_Feature for that particular
|
|
||||||
// vulnerability)
|
|
||||||
vulnerability, err := findVulnerability(tx, vulnerabilityNamespace, vulnerabilityName, true)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the new/updated/removed FeatureVersions and the resulting full list, using the given fct.
|
|
||||||
newFIFV, updatedFIFV, removedFIFV, allFIFV, err := f(vulnerability)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(newFIFV) == 0 && len(updatedFIFV) == 0 && len(removedFIFV) == 0 {
|
|
||||||
// Nothing to do.
|
|
||||||
tx.Commit()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Vulnerability_FixedIn_Feature and Vulnerability_Affects_FeatureVersion now.
|
|
||||||
err = pgSQL.updateVulnerabilityFeatureVersions(tx, vulnerability.ID, newFIFV, updatedFIFV,
|
|
||||||
removedFIFV)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create notification.
|
|
||||||
newVulnerability := vulnerability
|
|
||||||
newVulnerability.FixedIn = allFIFV
|
|
||||||
|
|
||||||
notification := database.VulnerabilityNotification{
|
|
||||||
NewVulnerability: newVulnerability,
|
|
||||||
OldVulnerability: &vulnerability,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pgSQL.insertNotification(tx, notification); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction.
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return handleError("insertVulnerability.Commit()", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pgSQL *pgSQL) updateVulnerabilityFeatureVersions(tx *sql.Tx, vulnerabilityID int, newFIFV, updatedFIFV, removedFIFV []database.FeatureVersion) error {
|
|
||||||
defer observeQueryTime("updateVulnerabilityFeatureVersions", "all", time.Now())
|
|
||||||
|
|
||||||
// Insert or find the Features.
|
// Insert or find the Features.
|
||||||
// TODO(Quentin-M): Batch me.
|
// TODO(Quentin-M): Batch me.
|
||||||
var err error
|
var err error
|
||||||
var features []*database.Feature
|
var features []*database.Feature
|
||||||
for _, fv := range newFIFV {
|
for i := 0; i < len(fixedIn); i++ {
|
||||||
features = append(features, &fv.Feature)
|
features = append(features, &fixedIn[i].Feature)
|
||||||
}
|
|
||||||
for _, fv := range updatedFIFV {
|
|
||||||
features = append(features, &fv.Feature)
|
|
||||||
}
|
|
||||||
for _, fv := range removedFIFV {
|
|
||||||
features = append(features, &fv.Feature)
|
|
||||||
}
|
}
|
||||||
for _, feature := range features {
|
for _, feature := range features {
|
||||||
if feature.ID == 0 {
|
if feature.ID == 0 {
|
||||||
@ -434,56 +364,27 @@ func (pgSQL *pgSQL) updateVulnerabilityFeatureVersions(tx *sql.Tx, vulnerability
|
|||||||
return handleError("insertVulnerability.l_vulnerability_affects_featureversion", err)
|
return handleError("insertVulnerability.l_vulnerability_affects_featureversion", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fixedInID int
|
for _, fv := range fixedIn {
|
||||||
|
var fixedInID int
|
||||||
|
|
||||||
for _, fv := range newFIFV {
|
|
||||||
// Insert Vulnerability_FixedIn_Feature.
|
// Insert Vulnerability_FixedIn_Feature.
|
||||||
err = tx.QueryRow(getQuery("i_vulnerability_fixedin_feature"), vulnerabilityID, fv.Feature.ID,
|
err = tx.QueryRow(
|
||||||
&fv.Version).Scan(&fixedInID)
|
getQuery("i_vulnerability_fixedin_feature"),
|
||||||
|
vulnerabilityID, fv.Feature.ID,
|
||||||
|
&fv.Version,
|
||||||
|
).Scan(&fixedInID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleError("i_vulnerability_fixedin_feature", err)
|
return handleError("i_vulnerability_fixedin_feature", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert Vulnerability_Affects_FeatureVersion.
|
// Insert Vulnerability_Affects_FeatureVersion.
|
||||||
err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID,
|
err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID, fv.Version)
|
||||||
fv.Version)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fv := range updatedFIFV {
|
|
||||||
// Update Vulnerability_FixedIn_Feature.
|
|
||||||
err = tx.QueryRow(getQuery("u_vulnerability_fixedin_feature"), vulnerabilityID,
|
|
||||||
fv.Feature.ID, &fv.Version).Scan(&fixedInID)
|
|
||||||
if err != nil {
|
|
||||||
return handleError("u_vulnerability_fixedin_feature", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop all old Vulnerability_Affects_FeatureVersion.
|
|
||||||
_, err = tx.Exec(getQuery("r_vulnerability_affects_featureversion"), fixedInID)
|
|
||||||
if err != nil {
|
|
||||||
return handleError("r_vulnerability_affects_featureversion", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert Vulnerability_Affects_FeatureVersion.
|
|
||||||
err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID,
|
|
||||||
fv.Version)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fv := range removedFIFV {
|
|
||||||
// Drop it from Vulnerability_FixedIn_Feature and let it cascade to
|
|
||||||
// Vulnerability_Affects_FeatureVersion.
|
|
||||||
err = tx.QueryRow(getQuery("r_vulnerability_fixedin_feature"), vulnerabilityID,
|
|
||||||
fv.Feature.ID).Scan(&fixedInID)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return handleError("r_vulnerability_fixedin_feature", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,21 +430,72 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []database.FeatureVersion) error {
|
||||||
|
defer observeQueryTime("InsertVulnerabilityFixes", "all", time.Now())
|
||||||
|
|
||||||
|
v := database.Vulnerability{
|
||||||
|
Name: vulnerabilityName,
|
||||||
|
Namespace: database.Namespace{
|
||||||
|
Name: vulnerabilityNamespace,
|
||||||
|
},
|
||||||
|
FixedIn: fixes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return pgSQL.insertVulnerability(v, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error {
|
||||||
|
defer observeQueryTime("DeleteVulnerabilityFix", "all", time.Now())
|
||||||
|
|
||||||
|
v := database.Vulnerability{
|
||||||
|
Name: vulnerabilityName,
|
||||||
|
Namespace: database.Namespace{
|
||||||
|
Name: vulnerabilityNamespace,
|
||||||
|
},
|
||||||
|
FixedIn: []database.FeatureVersion{
|
||||||
|
database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Name: featureName,
|
||||||
|
Namespace: database.Namespace{
|
||||||
|
Name: vulnerabilityNamespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Version: types.MinVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return pgSQL.insertVulnerability(v, true)
|
||||||
|
}
|
||||||
|
|
||||||
func (pgSQL *pgSQL) DeleteVulnerability(namespaceName, name string) error {
|
func (pgSQL *pgSQL) DeleteVulnerability(namespaceName, name string) error {
|
||||||
defer observeQueryTime("DeleteVulnerability", "all", time.Now())
|
defer observeQueryTime("DeleteVulnerability", "all", time.Now())
|
||||||
|
|
||||||
result, err := pgSQL.Exec(getQuery("r_vulnerability"), namespaceName, name)
|
// Begin transaction.
|
||||||
|
tx, err := pgSQL.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return handleError("DeleteVulnerability.Begin()", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var vulnerabilityID int
|
||||||
|
err = tx.QueryRow(getQuery("r_vulnerability"), namespaceName, name).Scan(&vulnerabilityID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
return handleError("r_vulnerability", err)
|
return handleError("r_vulnerability", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
affected, err := result.RowsAffected()
|
// Create a notification.
|
||||||
|
err = createNotification(tx, vulnerabilityID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleError("r_vulnerability.RowsAffected()", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected <= 0 {
|
// Commit transaction.
|
||||||
return cerrors.ErrNotFound
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return handleError("DeleteVulnerability.Commit()", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
Reference in New Issue
Block a user