*: refactor & do initial work towards PostgreSQL implementation
This commit is contained in:
parent
1a0f4a0f75
commit
2c150b015e
92
Godeps/Godeps.json
generated
92
Godeps/Godeps.json
generated
@ -1,92 +0,0 @@
|
|||||||
{
|
|
||||||
"ImportPath": "github.com/coreos/clair",
|
|
||||||
"GoVersion": "go1.5.1",
|
|
||||||
"Packages": [
|
|
||||||
"./..."
|
|
||||||
],
|
|
||||||
"Deps": [
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/alecthomas/template",
|
|
||||||
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/alecthomas/units",
|
|
||||||
"Rev": "6b4e7dc5e3143b85ea77909c72caf89416fc2915"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/barakmich/glog",
|
|
||||||
"Rev": "fafcb6128a8a2e6360ff034091434d547397d54a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/boltdb/bolt",
|
|
||||||
"Comment": "v1.0-98-gafceb31",
|
|
||||||
"Rev": "afceb316b96ea97cbac6d23afbdf69543d80748a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/coreos/go-systemd/journal",
|
|
||||||
"Comment": "v3-15-gcfa48f3",
|
|
||||||
"Rev": "cfa48f34d8dc4ff58f9b48725181a09f9092dc3c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/coreos/pkg/capnslog",
|
|
||||||
"Rev": "42a8c3b1a6f917bb8346ef738f32712a7ca0ede7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/coreos/pkg/timeutil",
|
|
||||||
"Rev": "42a8c3b1a6f917bb8346ef738f32712a7ca0ede7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/gogo/protobuf/proto",
|
|
||||||
"Rev": "58bbd41c1a2d1b7154f5d99a8d0d839b3093301a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/google/cayley",
|
|
||||||
"Rev": "582c4e1ca46943f2cf09c73bd12a83a6959057c9"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/julienschmidt/httprouter",
|
|
||||||
"Comment": "v1.1",
|
|
||||||
"Rev": "8c199fb6259ffc1af525cc3ad52ee60ba8359669"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/lib/pq",
|
|
||||||
"Comment": "go1.0-cutoff-56-gdc50b6a",
|
|
||||||
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/pborman/uuid",
|
|
||||||
"Rev": "ca53cad383cad2479bbba7f7a1a05797ec1386e4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/stretchr/testify/assert",
|
|
||||||
"Comment": "v1.0-17-g089c718",
|
|
||||||
"Rev": "089c7181b8c728499929ff09b62d3fdd8df8adff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
|
||||||
"Rev": "315fcfb05d4d46d4354b313d146ef688dda272a9"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/syndtr/gosnappy/snappy",
|
|
||||||
"Rev": "156a073208e131d7d2e212cb749feae7c339e846"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/tylerb/graceful",
|
|
||||||
"Comment": "v1.2.3",
|
|
||||||
"Rev": "48afeb21e2fcbcff0f30bd5ad6b97747b0fae38e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "golang.org/x/net/netutil",
|
|
||||||
"Rev": "7654728e381988afd88e58cabfd6363a5ea91810"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "gopkg.in/mgo.v2",
|
|
||||||
"Comment": "r2015.05.29",
|
|
||||||
"Rev": "01ee097136da162d1dd3c9b44fbdf3abf4fd6552"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "gopkg.in/yaml.v2",
|
|
||||||
"Rev": "f7716cbe52baa25d2e9b0d0da546fcf909fc16b4"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
5
Godeps/Readme
generated
5
Godeps/Readme
generated
@ -1,5 +0,0 @@
|
|||||||
This directory tree is generated automatically by godep.
|
|
||||||
|
|
||||||
Please do not edit.
|
|
||||||
|
|
||||||
See https://github.com/tools/godep for more information.
|
|
27
api/api.go
27
api/api.go
@ -26,17 +26,36 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/pkg/capnslog"
|
"github.com/coreos/pkg/capnslog"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/tylerb/graceful"
|
"github.com/tylerb/graceful"
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
"github.com/coreos/clair/config"
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/utils"
|
"github.com/coreos/clair/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "api")
|
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "api")
|
||||||
|
|
||||||
|
// Env stores the environment used by the API.
|
||||||
|
type Env struct {
|
||||||
|
Datastore database.Datastore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle adds a fourth parameter to httprouter.Handle: a pointer to *Env,
|
||||||
|
// allowing us to pass our environment to the handler.
|
||||||
|
type Handle func(http.ResponseWriter, *http.Request, httprouter.Params, *Env)
|
||||||
|
|
||||||
|
// WrapHandle encloses a Handle into a httprouter.Handle to make it usable by
|
||||||
|
// httprouter.
|
||||||
|
func WrapHandle(fn Handle, e *Env) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
fn(w, r, p, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run launches the main API, which exposes every possible interactions
|
// Run launches the main API, which exposes every possible interactions
|
||||||
// with clair.
|
// with clair.
|
||||||
func Run(config *config.APIConfig, st *utils.Stopper) {
|
func Run(config *config.APIConfig, env *Env, st *utils.Stopper) {
|
||||||
defer st.End()
|
defer st.End()
|
||||||
|
|
||||||
// Do not run the API service if there is no config.
|
// Do not run the API service if there is no config.
|
||||||
@ -60,7 +79,7 @@ func Run(config *config.APIConfig, st *utils.Stopper) {
|
|||||||
Server: &http.Server{
|
Server: &http.Server{
|
||||||
Addr: ":" + strconv.Itoa(config.Port),
|
Addr: ":" + strconv.Itoa(config.Port),
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
Handler: NewVersionRouter(config.Timeout),
|
Handler: NewVersionRouter(config.Timeout, env),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
listenAndServeWithStopper(srv, st, config.CertFile, config.KeyFile)
|
listenAndServeWithStopper(srv, st, config.CertFile, config.KeyFile)
|
||||||
@ -69,7 +88,7 @@ func Run(config *config.APIConfig, st *utils.Stopper) {
|
|||||||
|
|
||||||
// RunHealth launches the Health API, which only exposes a method to fetch
|
// RunHealth launches the Health API, which only exposes a method to fetch
|
||||||
// Clair's health without any security or authentication mechanism.
|
// Clair's health without any security or authentication mechanism.
|
||||||
func RunHealth(config *config.APIConfig, st *utils.Stopper) {
|
func RunHealth(config *config.APIConfig, env *Env, st *utils.Stopper) {
|
||||||
defer st.End()
|
defer st.End()
|
||||||
|
|
||||||
// Do not run the API service if there is no config.
|
// Do not run the API service if there is no config.
|
||||||
@ -84,7 +103,7 @@ func RunHealth(config *config.APIConfig, st *utils.Stopper) {
|
|||||||
NoSignalHandling: true, // We want to use our own Stopper
|
NoSignalHandling: true, // We want to use our own Stopper
|
||||||
Server: &http.Server{
|
Server: &http.Server{
|
||||||
Addr: ":" + strconv.Itoa(config.HealthPort),
|
Addr: ":" + strconv.Itoa(config.HealthPort),
|
||||||
Handler: NewHealthRouter(),
|
Handler: NewHealthRouter(env),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
listenAndServeWithStopper(srv, st, "", "")
|
listenAndServeWithStopper(srv, st, "", "")
|
||||||
|
109
api/handlers.go
Normal file
109
api/handlers.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2015 clair authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
"github.com/coreos/clair/health"
|
||||||
|
httputils "github.com/coreos/clair/utils/http"
|
||||||
|
"github.com/coreos/clair/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is an integer representing the API version.
|
||||||
|
const Version = 1
|
||||||
|
|
||||||
|
// POSTLayersParameters represents the expected parameters for POSTLayers.
|
||||||
|
type POSTLayersParameters struct {
|
||||||
|
Name, Path, ParentName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GETVersions returns API and Engine versions.
|
||||||
|
func GETVersions(w http.ResponseWriter, r *http.Request, _ httprouter.Params, _ *Env) {
|
||||||
|
httputils.WriteHTTP(w, http.StatusOK, struct {
|
||||||
|
APIVersion string
|
||||||
|
EngineVersion string
|
||||||
|
}{
|
||||||
|
APIVersion: strconv.Itoa(Version),
|
||||||
|
EngineVersion: strconv.Itoa(worker.Version),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GETHealth sums up the health of all the registered services.
|
||||||
|
func GETHealth(w http.ResponseWriter, r *http.Request, _ httprouter.Params, _ *Env) {
|
||||||
|
globalHealth, statuses := health.Healthcheck()
|
||||||
|
|
||||||
|
httpStatus := http.StatusOK
|
||||||
|
if !globalHealth {
|
||||||
|
httpStatus = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
httputils.WriteHTTP(w, httpStatus, statuses)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTLayers analyzes a layer and returns the engine version that has been used
|
||||||
|
// for the analysis.
|
||||||
|
func POSTLayers(w http.ResponseWriter, r *http.Request, _ httprouter.Params, e *Env) {
|
||||||
|
var parameters POSTLayersParameters
|
||||||
|
if s, err := httputils.ParseHTTPBody(r, ¶meters); err != nil {
|
||||||
|
httputils.WriteHTTPError(w, s, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process data.
|
||||||
|
if err := worker.Process(e.Datastore, parameters.Name, parameters.ParentName, parameters.Path); err != nil {
|
||||||
|
httputils.WriteHTTPError(w, 0, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get engine version and return.
|
||||||
|
httputils.WriteHTTP(w, http.StatusCreated, struct{ Version string }{Version: strconv.Itoa(worker.Version)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETELayers deletes the specified layer and any child layers that are
|
||||||
|
// dependent on the specified layer.
|
||||||
|
func DELETELayers(w http.ResponseWriter, r *http.Request, p httprouter.Params, e *Env) {
|
||||||
|
if err := e.Datastore.DeleteLayer(p.ByName("id")); err != nil {
|
||||||
|
httputils.WriteHTTPError(w, 0, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httputils.WriteHTTP(w, http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GETLayers returns informations about an existing layer, optionally with its features
|
||||||
|
// and vulnerabilities.
|
||||||
|
func GETLayers(w http.ResponseWriter, r *http.Request, p httprouter.Params, e *Env) {
|
||||||
|
withFeatures := false
|
||||||
|
withVulnerabilities := false
|
||||||
|
if r.URL.Query().Get("withFeatures") == "true" {
|
||||||
|
withFeatures = true
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("withVulnerabilities") == "true" {
|
||||||
|
withFeatures = true
|
||||||
|
withVulnerabilities = true
|
||||||
|
}
|
||||||
|
|
||||||
|
layer, err := e.Datastore.FindLayer(p.ByName("id"), withFeatures, withVulnerabilities)
|
||||||
|
if err != nil {
|
||||||
|
httputils.WriteHTTPError(w, 0, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httputils.WriteHTTP(w, http.StatusOK, struct{ Layer database.Layer }{Layer: layer})
|
||||||
|
}
|
@ -13,18 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// Package wrappers contains httprouter.Handle wrappers that are used in the API.
|
// Package wrappers contains httprouter.Handle wrappers that are used in the API.
|
||||||
package wrappers
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/pkg/capnslog"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "api")
|
|
||||||
|
|
||||||
type logWriter struct {
|
type logWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
status int
|
status int
|
||||||
@ -61,8 +58,8 @@ func (lw *logWriter) Status() int {
|
|||||||
return lw.status
|
return lw.status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log wraps a http.HandlerFunc and logs the API call
|
// Logger wraps an Handler and logs the API call
|
||||||
func Log(fn httprouter.Handle) httprouter.Handle {
|
func Logger(fn httprouter.Handle) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
lw := &logWriter{ResponseWriter: w}
|
lw := &logWriter{ResponseWriter: w}
|
||||||
start := time.Now()
|
start := time.Now()
|
@ -1,55 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Package logic implements all the available API methods.
|
|
||||||
// Every methods are documented in docs/API.md.
|
|
||||||
package logic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/health"
|
|
||||||
httputils "github.com/coreos/clair/utils/http"
|
|
||||||
"github.com/coreos/clair/worker"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Version is an integer representing the API version.
|
|
||||||
const Version = 1
|
|
||||||
|
|
||||||
// GETVersions returns API and Engine versions.
|
|
||||||
func GETVersions(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct {
|
|
||||||
APIVersion string
|
|
||||||
EngineVersion string
|
|
||||||
}{
|
|
||||||
APIVersion: strconv.Itoa(Version),
|
|
||||||
EngineVersion: strconv.Itoa(worker.Version),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETHealth sums up the health of all the registered services.
|
|
||||||
func GETHealth(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
||||||
globalHealth, statuses := health.Healthcheck()
|
|
||||||
|
|
||||||
httpStatus := http.StatusOK
|
|
||||||
if !globalHealth {
|
|
||||||
httpStatus = http.StatusServiceUnavailable
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, httpStatus, statuses)
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,378 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package logic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/database"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
httputils "github.com/coreos/clair/utils/http"
|
|
||||||
"github.com/coreos/clair/utils/types"
|
|
||||||
"github.com/coreos/clair/worker"
|
|
||||||
)
|
|
||||||
|
|
||||||
// POSTLayersParameters represents the expected parameters for POSTLayers.
|
|
||||||
type POSTLayersParameters struct {
|
|
||||||
ID, Path, ParentID, ImageFormat string
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTLayers analyzes a layer and returns the engine version that has been used
|
|
||||||
// for the analysis.
|
|
||||||
func POSTLayers(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
||||||
var parameters POSTLayersParameters
|
|
||||||
if s, err := httputils.ParseHTTPBody(r, ¶meters); err != nil {
|
|
||||||
httputils.WriteHTTPError(w, s, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process data.
|
|
||||||
if err := worker.Process(parameters.ID, parameters.ParentID, parameters.Path, parameters.ImageFormat); err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get engine version and return.
|
|
||||||
httputils.WriteHTTP(w, http.StatusCreated, struct{ Version string }{Version: strconv.Itoa(worker.Version)})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETELayers deletes the specified layer and any child layers that are
|
|
||||||
// dependent on the specified layer.
|
|
||||||
func DELETELayers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
err := database.DeleteLayer(p.ByName("id"))
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETLayersOS returns the operating system of a layer if it exists.
|
|
||||||
// It uses not only the specified layer but also its parent layers if necessary.
|
|
||||||
// An empty OS string is returned if no OS has been detected.
|
|
||||||
func GETLayersOS(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Find layer.
|
|
||||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent, database.FieldLayerOS})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get OS.
|
|
||||||
os, err := layer.OperatingSystem()
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ OS string }{OS: os})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETLayersParent returns the parent ID of a layer if it exists.
|
|
||||||
// An empty ID string is returned if the layer has no parent.
|
|
||||||
func GETLayersParent(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Find layer
|
|
||||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get layer's parent.
|
|
||||||
parent, err := layer.Parent([]string{database.FieldLayerID})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ID := ""
|
|
||||||
if parent != nil {
|
|
||||||
ID = parent.ID
|
|
||||||
}
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ ID string }{ID: ID})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETLayersPackages returns the complete list of packages that a layer has
|
|
||||||
// if it exists.
|
|
||||||
func GETLayersPackages(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Find layer
|
|
||||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent, database.FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer's packages.
|
|
||||||
packagesNodes, err := layer.AllPackages()
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
packages := []*database.Package{}
|
|
||||||
if len(packagesNodes) > 0 {
|
|
||||||
packages, err = database.FindAllPackagesByNodes(packagesNodes, []string{database.FieldPackageOS, database.FieldPackageName, database.FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ Packages []*database.Package }{Packages: packages})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETLayersPackagesDiff returns the list of packages that a layer installs and
|
|
||||||
// removes if it exists.
|
|
||||||
func GETLayersPackagesDiff(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Find layer.
|
|
||||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer's packages.
|
|
||||||
installedPackages, removedPackages := make([]*database.Package, 0), make([]*database.Package, 0)
|
|
||||||
if len(layer.InstalledPackagesNodes) > 0 {
|
|
||||||
installedPackages, err = database.FindAllPackagesByNodes(layer.InstalledPackagesNodes, []string{database.FieldPackageOS, database.FieldPackageName, database.FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(layer.RemovedPackagesNodes) > 0 {
|
|
||||||
removedPackages, err = database.FindAllPackagesByNodes(layer.RemovedPackagesNodes, []string{database.FieldPackageOS, database.FieldPackageName, database.FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ InstalledPackages, RemovedPackages []*database.Package }{InstalledPackages: installedPackages, RemovedPackages: removedPackages})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETLayersVulnerabilities returns the complete list of vulnerabilities that
|
|
||||||
// a layer has if it exists.
|
|
||||||
func GETLayersVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Get minumum priority parameter.
|
|
||||||
minimumPriority := types.Priority(r.URL.Query().Get("minimumPriority"))
|
|
||||||
if minimumPriority == "" {
|
|
||||||
minimumPriority = "High" // Set default priority to High
|
|
||||||
} else if !minimumPriority.IsValid() {
|
|
||||||
httputils.WriteHTTPError(w, 0, cerrors.NewBadRequestError("invalid priority"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer
|
|
||||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerParent, database.FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer's packages.
|
|
||||||
packagesNodes, err := layer.AllPackages()
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find vulnerabilities.
|
|
||||||
vulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(packagesNodes, minimumPriority, []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription, database.FieldVulnerabilityCausedByPackage})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ Vulnerabilities []*database.Vulnerability }{Vulnerabilities: vulnerabilities})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETLayersVulnerabilitiesDiff returns the list of vulnerabilities that a layer
|
|
||||||
// adds and removes if it exists.
|
|
||||||
func GETLayersVulnerabilitiesDiff(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Get minumum priority parameter.
|
|
||||||
minimumPriority := types.Priority(r.URL.Query().Get("minimumPriority"))
|
|
||||||
if minimumPriority == "" {
|
|
||||||
minimumPriority = "High" // Set default priority to High
|
|
||||||
} else if !minimumPriority.IsValid() {
|
|
||||||
httputils.WriteHTTPError(w, 0, cerrors.NewBadRequestError("invalid priority"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer.
|
|
||||||
layer, err := database.FindOneLayerByID(p.ByName("id"), []string{database.FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected fields for vulnerabilities.
|
|
||||||
selectedFields := []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription, database.FieldVulnerabilityCausedByPackage}
|
|
||||||
|
|
||||||
// Find vulnerabilities for installed packages.
|
|
||||||
addedVulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(layer.InstalledPackagesNodes, minimumPriority, selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find vulnerabilities for removed packages.
|
|
||||||
removedVulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(layer.RemovedPackagesNodes, minimumPriority, selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove vulnerabilities which appears both in added and removed lists (eg. case of updated packages but still vulnerable).
|
|
||||||
for ia, a := range addedVulnerabilities {
|
|
||||||
for ir, r := range removedVulnerabilities {
|
|
||||||
if a.ID == r.ID {
|
|
||||||
addedVulnerabilities = append(addedVulnerabilities[:ia], addedVulnerabilities[ia+1:]...)
|
|
||||||
removedVulnerabilities = append(removedVulnerabilities[:ir], removedVulnerabilities[ir+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ Adds, Removes []*database.Vulnerability }{Adds: addedVulnerabilities, Removes: removedVulnerabilities})
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTBatchLayersVulnerabilitiesParameters represents the expected parameters
|
|
||||||
// for POSTBatchLayersVulnerabilities.
|
|
||||||
type POSTBatchLayersVulnerabilitiesParameters struct {
|
|
||||||
LayersIDs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTBatchLayersVulnerabilities returns the complete list of vulnerabilities
|
|
||||||
// that the provided layers have, if they all exist.
|
|
||||||
func POSTBatchLayersVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Parse body
|
|
||||||
var parameters POSTBatchLayersVulnerabilitiesParameters
|
|
||||||
if s, err := httputils.ParseHTTPBody(r, ¶meters); err != nil {
|
|
||||||
httputils.WriteHTTPError(w, s, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(parameters.LayersIDs) == 0 {
|
|
||||||
httputils.WriteHTTPError(w, http.StatusBadRequest, errors.New("at least one LayerID query parameter must be provided"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get minumum priority parameter.
|
|
||||||
minimumPriority := types.Priority(r.URL.Query().Get("minimumPriority"))
|
|
||||||
if minimumPriority == "" {
|
|
||||||
minimumPriority = "High" // Set default priority to High
|
|
||||||
} else if !minimumPriority.IsValid() {
|
|
||||||
httputils.WriteHTTPError(w, 0, cerrors.NewBadRequestError("invalid priority"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := make(map[string]interface{})
|
|
||||||
// For each LayerID parameter
|
|
||||||
for _, layerID := range parameters.LayersIDs {
|
|
||||||
// Find layer
|
|
||||||
layer, err := database.FindOneLayerByID(layerID, []string{database.FieldLayerParent, database.FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer's packages.
|
|
||||||
packagesNodes, err := layer.AllPackages()
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find vulnerabilities.
|
|
||||||
vulnerabilities, err := getVulnerabilitiesFromLayerPackagesNodes(packagesNodes, minimumPriority, []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription, database.FieldVulnerabilityCausedByPackage})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response[layerID] = struct{ Vulnerabilities []*database.Vulnerability }{Vulnerabilities: vulnerabilities}
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSuccessorsFromPackagesNodes returns the node list of packages that have
|
|
||||||
// versions following the versions of the provided packages.
|
|
||||||
func getSuccessorsFromPackagesNodes(packagesNodes []string) ([]string, error) {
|
|
||||||
if len(packagesNodes) == 0 {
|
|
||||||
return []string{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get packages.
|
|
||||||
packages, err := database.FindAllPackagesByNodes(packagesNodes, []string{database.FieldPackageNextVersion})
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all packages' successors.
|
|
||||||
var packagesNextVersions []string
|
|
||||||
for _, pkg := range packages {
|
|
||||||
nextVersions, err := pkg.NextVersions([]string{})
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
for _, version := range nextVersions {
|
|
||||||
packagesNextVersions = append(packagesNextVersions, version.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return packagesNextVersions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getVulnerabilitiesFromLayerPackagesNodes returns the list of vulnerabilities
|
|
||||||
// affecting the provided package nodes, filtered by Priority.
|
|
||||||
func getVulnerabilitiesFromLayerPackagesNodes(packagesNodes []string, minimumPriority types.Priority, selectedFields []string) ([]*database.Vulnerability, error) {
|
|
||||||
if len(packagesNodes) == 0 {
|
|
||||||
return []*database.Vulnerability{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get successors of the packages.
|
|
||||||
packagesNextVersions, err := getSuccessorsFromPackagesNodes(packagesNodes)
|
|
||||||
if err != nil {
|
|
||||||
return []*database.Vulnerability{}, err
|
|
||||||
}
|
|
||||||
if len(packagesNextVersions) == 0 {
|
|
||||||
return []*database.Vulnerability{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find vulnerabilities fixed in these successors.
|
|
||||||
vulnerabilities, err := database.FindAllVulnerabilitiesByFixedIn(packagesNextVersions, selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return []*database.Vulnerability{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter vulnerabilities depending on their priority and remove duplicates.
|
|
||||||
filteredVulnerabilities := []*database.Vulnerability{}
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
for _, v := range vulnerabilities {
|
|
||||||
if minimumPriority.Compare(v.Priority) <= 0 {
|
|
||||||
if _, alreadySeen := seen[v.ID]; !alreadySeen {
|
|
||||||
filteredVulnerabilities = append(filteredVulnerabilities, v)
|
|
||||||
seen[v.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredVulnerabilities, nil
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package logic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/database"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
httputils "github.com/coreos/clair/utils/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GETVulnerabilities returns a vulnerability identified by an ID if it exists.
|
|
||||||
func GETVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Find vulnerability.
|
|
||||||
vulnerability, err := database.FindOneVulnerability(p.ByName("id"), []string{database.FieldVulnerabilityID, database.FieldVulnerabilityLink, database.FieldVulnerabilityPriority, database.FieldVulnerabilityDescription, database.FieldVulnerabilityFixedIn})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
abstractVulnerability, err := vulnerability.ToAbstractVulnerability()
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, abstractVulnerability)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTVulnerabilities manually inserts a vulnerability into the database if it
|
|
||||||
// does not exist yet.
|
|
||||||
func POSTVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
var parameters *database.AbstractVulnerability
|
|
||||||
if s, err := httputils.ParseHTTPBody(r, ¶meters); err != nil {
|
|
||||||
httputils.WriteHTTPError(w, s, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the vulnerability does not exist.
|
|
||||||
vulnerability, err := database.FindOneVulnerability(parameters.ID, []string{})
|
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if vulnerability != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, cerrors.NewBadRequestError("vulnerability already exists"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert packages.
|
|
||||||
packages := database.AbstractPackagesToPackages(parameters.AffectedPackages)
|
|
||||||
err = database.InsertPackages(packages)
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var pkgNodes []string
|
|
||||||
for _, p := range packages {
|
|
||||||
pkgNodes = append(pkgNodes, p.Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert vulnerability.
|
|
||||||
notifications, err := database.InsertVulnerabilities([]*database.Vulnerability{parameters.ToVulnerability(pkgNodes)})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert notifications.
|
|
||||||
err = database.InsertNotifications(notifications, database.GetDefaultNotificationWrapper())
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusCreated, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUTVulnerabilities updates a vulnerability if it exists.
|
|
||||||
func PUTVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
var parameters *database.AbstractVulnerability
|
|
||||||
if s, err := httputils.ParseHTTPBody(r, ¶meters); err != nil {
|
|
||||||
httputils.WriteHTTPError(w, s, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parameters.ID = p.ByName("id")
|
|
||||||
|
|
||||||
// Ensure that the vulnerability exists.
|
|
||||||
_, err := database.FindOneVulnerability(parameters.ID, []string{})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert packages.
|
|
||||||
packages := database.AbstractPackagesToPackages(parameters.AffectedPackages)
|
|
||||||
err = database.InsertPackages(packages)
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var pkgNodes []string
|
|
||||||
for _, p := range packages {
|
|
||||||
pkgNodes = append(pkgNodes, p.Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert vulnerability.
|
|
||||||
notifications, err := database.InsertVulnerabilities([]*database.Vulnerability{parameters.ToVulnerability(pkgNodes)})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert notifications.
|
|
||||||
err = database.InsertNotifications(notifications, database.GetDefaultNotificationWrapper())
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusCreated, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELVulnerabilities deletes a vulnerability if it exists.
|
|
||||||
func DELVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
err := database.DeleteVulnerability(p.ByName("id"))
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETVulnerabilitiesIntroducingLayers returns the list of layers that
|
|
||||||
// introduces a given vulnerability, if it exists.
|
|
||||||
// To clarify, it does not return the list of every layers that have
|
|
||||||
// the vulnerability.
|
|
||||||
func GETVulnerabilitiesIntroducingLayers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Find vulnerability to verify that it exists.
|
|
||||||
_, err := database.FindOneVulnerability(p.ByName("id"), []string{})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
layers, err := database.FindAllLayersIntroducingVulnerability(p.ByName("id"), []string{database.FieldLayerID})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
layersIDs := []string{}
|
|
||||||
for _, l := range layers {
|
|
||||||
layersIDs = append(layersIDs, l.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, struct{ IntroducingLayersIDs []string }{IntroducingLayersIDs: layersIDs})
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTVulnerabilitiesAffectedLayersParameters represents the expected
|
|
||||||
// parameters for POSTVulnerabilitiesAffectedLayers.
|
|
||||||
type POSTVulnerabilitiesAffectedLayersParameters struct {
|
|
||||||
LayersIDs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTVulnerabilitiesAffectedLayers returns whether the specified layers
|
|
||||||
// (by their IDs) are vulnerable to the given Vulnerability or not.
|
|
||||||
func POSTVulnerabilitiesAffectedLayers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
// Parse body.
|
|
||||||
var parameters POSTBatchLayersVulnerabilitiesParameters
|
|
||||||
if s, err := httputils.ParseHTTPBody(r, ¶meters); err != nil {
|
|
||||||
httputils.WriteHTTPError(w, s, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(parameters.LayersIDs) == 0 {
|
|
||||||
httputils.WriteHTTPError(w, http.StatusBadRequest, errors.New("getting the entire list of affected layers is not supported yet: at least one LayerID query parameter must be provided"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find vulnerability.
|
|
||||||
vulnerability, err := database.FindOneVulnerability(p.ByName("id"), []string{database.FieldVulnerabilityFixedIn})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the fixed in nodes into a map for fast check.
|
|
||||||
fixedInPackagesMap := make(map[string]struct{})
|
|
||||||
for _, fixedInNode := range vulnerability.FixedInNodes {
|
|
||||||
fixedInPackagesMap[fixedInNode] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
response := make(map[string]interface{})
|
|
||||||
// For each LayerID parameter.
|
|
||||||
for _, layerID := range parameters.LayersIDs {
|
|
||||||
// Find layer
|
|
||||||
layer, err := database.FindOneLayerByID(layerID, []string{database.FieldLayerParent, database.FieldLayerPackages, database.FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer's packages.
|
|
||||||
packagesNodes, err := layer.AllPackages()
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get successors packages of layer' packages.
|
|
||||||
successors, err := getSuccessorsFromPackagesNodes(packagesNodes)
|
|
||||||
if err != nil {
|
|
||||||
httputils.WriteHTTPError(w, 0, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if the layer is vulnerable by verifying if one of the successors
|
|
||||||
// of its packages are fixed by the vulnerability.
|
|
||||||
vulnerable := false
|
|
||||||
for _, p := range successors {
|
|
||||||
if _, fixed := fixedInPackagesMap[p]; fixed {
|
|
||||||
vulnerable = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response[layerID] = struct{ Vulnerable bool }{Vulnerable: vulnerable}
|
|
||||||
}
|
|
||||||
|
|
||||||
httputils.WriteHTTP(w, http.StatusOK, response)
|
|
||||||
}
|
|
@ -19,8 +19,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/clair/api/logic"
|
|
||||||
"github.com/coreos/clair/api/wrappers"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,9 +28,9 @@ type VersionRouter map[string]*httprouter.Router
|
|||||||
|
|
||||||
// NewVersionRouter instantiates a VersionRouter and every sub-routers that are
|
// NewVersionRouter instantiates a VersionRouter and every sub-routers that are
|
||||||
// necessary to handle supported API versions.
|
// necessary to handle supported API versions.
|
||||||
func NewVersionRouter(to time.Duration) *VersionRouter {
|
func NewVersionRouter(to time.Duration, env *Env) *VersionRouter {
|
||||||
return &VersionRouter{
|
return &VersionRouter{
|
||||||
"/v1": NewRouterV1(to),
|
"/v1": NewRouterV1(to, env),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,42 +54,38 @@ func (vs VersionRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRouterV1 creates a new router for the API (Version 1)
|
// NewRouterV1 creates a new router for the API (Version 1)
|
||||||
func NewRouterV1(to time.Duration) *httprouter.Router {
|
func NewRouterV1(to time.Duration, env *Env) *httprouter.Router {
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
wrap := func(fn httprouter.Handle) httprouter.Handle {
|
|
||||||
return wrappers.Log(wrappers.TimeOut(to, fn))
|
// Create a wrapper that will wrap a Handle into a httprouter.Handle and that adds
|
||||||
|
// logging and time-out capabilities.
|
||||||
|
wrap := func(fn Handle, e *Env) httprouter.Handle {
|
||||||
|
return Logger(TimeOut(to, WrapHandle(fn, e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// General
|
// General
|
||||||
router.GET("/versions", wrap(logic.GETVersions))
|
router.GET("/versions", wrap(GETVersions, env))
|
||||||
router.GET("/health", wrap(logic.GETHealth))
|
router.GET("/health", wrap(GETHealth, env))
|
||||||
|
|
||||||
// Layers
|
// Layers
|
||||||
router.POST("/layers", wrap(logic.POSTLayers))
|
router.POST("/layers", wrap(POSTLayers, env))
|
||||||
router.DELETE("/layers/:id", wrap(logic.DELETELayers))
|
router.DELETE("/layers/:id", wrap(DELETELayers, env))
|
||||||
router.GET("/layers/:id/os", wrap(logic.GETLayersOS))
|
router.GET("/layers/:id", wrap(GETLayers, env))
|
||||||
router.GET("/layers/:id/parent", wrap(logic.GETLayersParent))
|
|
||||||
router.GET("/layers/:id/packages", wrap(logic.GETLayersPackages))
|
|
||||||
router.GET("/layers/:id/packages/diff", wrap(logic.GETLayersPackagesDiff))
|
|
||||||
router.GET("/layers/:id/vulnerabilities", wrap(logic.GETLayersVulnerabilities))
|
|
||||||
router.GET("/layers/:id/vulnerabilities/diff", wrap(logic.GETLayersVulnerabilitiesDiff))
|
|
||||||
// # Batch version of "/layers/:id/vulnerabilities"
|
|
||||||
router.POST("/batch/layers/vulnerabilities", wrap(logic.POSTBatchLayersVulnerabilities))
|
|
||||||
|
|
||||||
// Vulnerabilities
|
// Vulnerabilities
|
||||||
router.POST("/vulnerabilities", wrap(logic.POSTVulnerabilities))
|
// router.POST("/vulnerabilities", wrap(logic.POSTVulnerabilities))
|
||||||
router.PUT("/vulnerabilities/:id", wrap(logic.PUTVulnerabilities))
|
// router.PUT("/vulnerabilities/:id", wrap(logic.PUTVulnerabilities))
|
||||||
router.GET("/vulnerabilities/:id", wrap(logic.GETVulnerabilities))
|
// router.GET("/vulnerabilities/:id", wrap(logic.GETVulnerabilities))
|
||||||
router.DELETE("/vulnerabilities/:id", wrap(logic.DELVulnerabilities))
|
// router.DELETE("/vulnerabilities/:id", wrap(logic.DELVulnerabilities))
|
||||||
router.GET("/vulnerabilities/:id/introducing-layers", wrap(logic.GETVulnerabilitiesIntroducingLayers))
|
// router.GET("/vulnerabilities/:id/introducing-layers", wrap(logic.GETVulnerabilitiesIntroducingLayers))
|
||||||
router.POST("/vulnerabilities/:id/affected-layers", wrap(logic.POSTVulnerabilitiesAffectedLayers))
|
// router.POST("/vulnerabilities/:id/affected-layers", wrap(logic.POSTVulnerabilitiesAffectedLayers))
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHealthRouter creates a new router that only serve the Health function on /
|
// NewHealthRouter creates a new router that only serve the Health function on /
|
||||||
func NewHealthRouter() *httprouter.Router {
|
func NewHealthRouter(env *Env) *httprouter.Router {
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
router.GET("/", logic.GETHealth)
|
router.GET("/", WrapHandle(GETHealth, env))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package wrappers
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -61,7 +61,7 @@ func (tw *timeoutWriter) WriteHeader(status int) {
|
|||||||
tw.ResponseWriter.WriteHeader(status)
|
tw.ResponseWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeOut wraps a http.HandlerFunc and ensure that a response is given under
|
// TimeOut wraps an Handler and ensure that a response is given under
|
||||||
// the specified duration.
|
// the specified duration.
|
||||||
//
|
//
|
||||||
// If the handler takes longer than the time limit, the wrapper responds with
|
// If the handler takes longer than the time limit, the wrapper responds with
|
20
clair.go
20
clair.go
@ -24,9 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/clair/api"
|
"github.com/coreos/clair/api"
|
||||||
"github.com/coreos/clair/config"
|
"github.com/coreos/clair/config"
|
||||||
"github.com/coreos/clair/database"
|
"github.com/coreos/clair/database/pgsql"
|
||||||
"github.com/coreos/clair/notifier"
|
|
||||||
"github.com/coreos/clair/updater"
|
|
||||||
"github.com/coreos/clair/utils"
|
"github.com/coreos/clair/utils"
|
||||||
"github.com/coreos/pkg/capnslog"
|
"github.com/coreos/pkg/capnslog"
|
||||||
)
|
)
|
||||||
@ -40,25 +38,25 @@ func Boot(config *config.Config) {
|
|||||||
st := utils.NewStopper()
|
st := utils.NewStopper()
|
||||||
|
|
||||||
// Open database
|
// Open database
|
||||||
err := database.Open(config.Database)
|
db, err := pgsql.Open(config.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer database.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Start notifier
|
// Start notifier
|
||||||
st.Begin()
|
// st.Begin()
|
||||||
go notifier.Run(config.Notifier, st)
|
// go notifier.Run(config.Notifier, st)
|
||||||
|
|
||||||
// Start API
|
// Start API
|
||||||
st.Begin()
|
st.Begin()
|
||||||
go api.Run(config.API, st)
|
go api.Run(config.API, &api.Env{Datastore: db}, st)
|
||||||
st.Begin()
|
st.Begin()
|
||||||
go api.RunHealth(config.API, st)
|
go api.RunHealth(config.API, &api.Env{Datastore: db}, st)
|
||||||
|
|
||||||
// Start updater
|
// Start updater
|
||||||
st.Begin()
|
// st.Begin()
|
||||||
go updater.Run(config.Updater, st)
|
// go updater.Run(config.Updater, st)
|
||||||
|
|
||||||
// Wait for interruption and shutdown gracefully.
|
// Wait for interruption and shutdown gracefully.
|
||||||
waitForSignals(os.Interrupt)
|
waitForSignals(os.Interrupt)
|
||||||
|
@ -26,11 +26,12 @@ import (
|
|||||||
"github.com/coreos/pkg/capnslog"
|
"github.com/coreos/pkg/capnslog"
|
||||||
|
|
||||||
// Register components
|
// Register components
|
||||||
_ "github.com/coreos/clair/notifier/notifiers"
|
_ "github.com/coreos/clair/worker/detectors/feature/dpkg"
|
||||||
_ "github.com/coreos/clair/updater/fetchers"
|
_ "github.com/coreos/clair/worker/detectors/feature/rpm"
|
||||||
_ "github.com/coreos/clair/worker/detectors/data"
|
_ "github.com/coreos/clair/worker/detectors/namespace/aptsources"
|
||||||
_ "github.com/coreos/clair/worker/detectors/os"
|
_ "github.com/coreos/clair/worker/detectors/namespace/lsbrelease"
|
||||||
_ "github.com/coreos/clair/worker/detectors/packages"
|
_ "github.com/coreos/clair/worker/detectors/namespace/osrelease"
|
||||||
|
_ "github.com/coreos/clair/worker/detectors/namespace/redhatrelease"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clair", "main")
|
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clair", "main")
|
||||||
|
@ -33,8 +33,8 @@ type Config struct {
|
|||||||
// DatabaseConfig is the configuration used to specify how Clair connects
|
// DatabaseConfig is the configuration used to specify how Clair connects
|
||||||
// to a database.
|
// to a database.
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Type string
|
Source string
|
||||||
Path string
|
CacheSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdaterConfig is the configuration for the Updater service.
|
// UpdaterConfig is the configuration for the Updater service.
|
||||||
@ -59,7 +59,7 @@ type APIConfig struct {
|
|||||||
// DefaultConfig is a configuration that can be used as a fallback value.
|
// DefaultConfig is a configuration that can be used as a fallback value.
|
||||||
var DefaultConfig = Config{
|
var DefaultConfig = Config{
|
||||||
Database: &DatabaseConfig{
|
Database: &DatabaseConfig{
|
||||||
Type: "memstore",
|
CacheSize: 16384,
|
||||||
},
|
},
|
||||||
Updater: &UpdaterConfig{
|
Updater: &UpdaterConfig{
|
||||||
Interval: 1 * time.Hour,
|
Interval: 1 * time.Hour,
|
||||||
|
@ -1,50 +1,8 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Package database implements every database models and the functions that
|
|
||||||
// manipulate them.
|
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import "errors"
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/barakmich/glog"
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/coreos/clair/health"
|
|
||||||
"github.com/coreos/clair/utils"
|
|
||||||
"github.com/coreos/pkg/capnslog"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/google/cayley/graph"
|
|
||||||
"github.com/google/cayley/graph/path"
|
|
||||||
|
|
||||||
// Load all supported backends.
|
|
||||||
_ "github.com/google/cayley/graph/bolt"
|
|
||||||
_ "github.com/google/cayley/graph/leveldb"
|
|
||||||
_ "github.com/google/cayley/graph/memstore"
|
|
||||||
_ "github.com/google/cayley/graph/mongo"
|
|
||||||
_ "github.com/google/cayley/graph/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// fieldIs is the graph predicate defining the type of an entity.
|
|
||||||
fieldIs = "is"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = capnslog.NewPackageLogger("github.com/coreos/clair", "database")
|
|
||||||
|
|
||||||
// ErrTransaction is an error that occurs when a database transaction fails.
|
// ErrTransaction is an error that occurs when a database transaction fails.
|
||||||
ErrTransaction = errors.New("database: transaction failed (concurrent modification?)")
|
ErrTransaction = errors.New("database: transaction failed (concurrent modification?)")
|
||||||
// ErrBackendException is an error that occurs when the database backend does
|
// ErrBackendException is an error that occurs when the database backend does
|
||||||
@ -55,141 +13,32 @@ var (
|
|||||||
ErrInconsistent = errors.New("database: inconsistent database")
|
ErrInconsistent = errors.New("database: inconsistent database")
|
||||||
// ErrCantOpen is an error that occurs when the database could not be opened
|
// ErrCantOpen is an error that occurs when the database could not be opened
|
||||||
ErrCantOpen = errors.New("database: could not open database")
|
ErrCantOpen = errors.New("database: could not open database")
|
||||||
|
|
||||||
store *cayley.Handle
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
type Datastore interface {
|
||||||
health.RegisterHealthchecker("database", Healthcheck)
|
// Layer
|
||||||
}
|
InsertLayer(Layer) error
|
||||||
|
FindLayer(name string, withFeatures, withVulnerabilities bool) (layer Layer, err error)
|
||||||
// Open opens a Cayley database, creating it if necessary and return its handle
|
DeleteLayer(name string) error
|
||||||
func Open(config *config.DatabaseConfig) error {
|
|
||||||
if store != nil {
|
// Vulnerability
|
||||||
log.Errorf("could not open database at %s : a database is already opened", config.Path)
|
// InsertVulnerabilities([]*Vulnerability)
|
||||||
return ErrCantOpen
|
// DeleteVulnerability(id string)
|
||||||
}
|
|
||||||
if config.Type != "memstore" && config.Path == "" {
|
// Notifications
|
||||||
log.Errorf("could not open database : no path provided.")
|
// InsertNotifications([]*Notification) error
|
||||||
return ErrCantOpen
|
// FindNotificationToSend() (*Notification, error)
|
||||||
}
|
// CountNotificationsToSend() (int, error)
|
||||||
|
// MarkNotificationAsSent(id string)
|
||||||
var err error
|
|
||||||
options := make(graph.Options)
|
// Key/Value
|
||||||
|
InsertKeyValue(key, value string) error
|
||||||
switch config.Type {
|
GetKeyValue(key string) (string, error)
|
||||||
case "bolt", "leveldb":
|
|
||||||
if _, err := os.Stat(config.Path); os.IsNotExist(err) {
|
// Lock
|
||||||
log.Infof("database at %s does not exist yet, creating it", config.Path)
|
// Lock(name string, duration time.Duration, owner string) (bool, time.Time)
|
||||||
|
// Unlock(name, owner string)
|
||||||
err = graph.InitQuadStore(config.Type, config.Path, options)
|
// LockInfo(name string) (string, time.Time, error)
|
||||||
if err != nil && err != graph.ErrDatabaseExists {
|
|
||||||
log.Errorf("could not create database at %s : %s", config.Path, err)
|
Close()
|
||||||
return ErrCantOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "sql":
|
|
||||||
// Replaces the PostgreSQL's slow COUNT query with a fast estimator.
|
|
||||||
// Ref: https://wiki.postgresql.org/wiki/Count_estimate
|
|
||||||
options["use_estimates"] = true
|
|
||||||
|
|
||||||
err := graph.InitQuadStore(config.Type, config.Path, options)
|
|
||||||
if err != nil && err != graph.ErrDatabaseExists {
|
|
||||||
log.Errorf("could not create database at %s : %s", config.Path, err)
|
|
||||||
return ErrCantOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
store, err = cayley.NewGraph(config.Type, config.Path, options)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("could not open database at %s : %s", config.Path, err)
|
|
||||||
return ErrCantOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes a Cayley database
|
|
||||||
func Close() {
|
|
||||||
if store != nil {
|
|
||||||
store.Close()
|
|
||||||
store = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Healthcheck simply adds and then remove a quad in Cayley to ensure it is working
|
|
||||||
// It returns true when everything is ok
|
|
||||||
func Healthcheck() health.Status {
|
|
||||||
var err error
|
|
||||||
if store != nil {
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
q := cayley.Triple("cayley", "is", "healthy")
|
|
||||||
t.AddQuad(q)
|
|
||||||
t.RemoveQuad(q)
|
|
||||||
glog.SetStderrThreshold("FATAL") // TODO REMOVE ME
|
|
||||||
err = store.ApplyTransaction(t)
|
|
||||||
glog.SetStderrThreshold("ERROR") // TODO REMOVE ME
|
|
||||||
}
|
|
||||||
|
|
||||||
return health.Status{IsEssential: true, IsHealthy: err == nil, Details: nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toValue returns a single value from a path
|
|
||||||
// If the path does not lead to a value, an empty string is returned
|
|
||||||
// If the path leads to multiple values or if a database error occurs, an empty string and an error are returned
|
|
||||||
func toValue(p *path.Path) (string, error) {
|
|
||||||
var value string
|
|
||||||
found := false
|
|
||||||
|
|
||||||
it, _ := p.BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
if found {
|
|
||||||
log.Error("failed query in toValue: used on an iterator containing multiple values")
|
|
||||||
return "", ErrInconsistent
|
|
||||||
}
|
|
||||||
|
|
||||||
if it.Result() != nil {
|
|
||||||
value = store.NameOf(it.Result())
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in toValue: %s", it.Err())
|
|
||||||
return "", ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// toValues returns multiple values from a path
|
|
||||||
// If the path does not lead to any value, an empty array is returned
|
|
||||||
// If a database error occurs, an empty array and an error are returned
|
|
||||||
func toValues(p *path.Path) ([]string, error) {
|
|
||||||
var values []string
|
|
||||||
|
|
||||||
it, _ := p.BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
if it.Result() != nil {
|
|
||||||
values = append(values, store.NameOf(it.Result()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in toValues: %s", it.Err())
|
|
||||||
return []string{}, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return values, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveFields appends cayley's Save method to a path for each field in
|
|
||||||
// selectedFields, except the ones that appears also in exceptFields
|
|
||||||
func saveFields(p *path.Path, selectedFields []string, exceptFields []string) {
|
|
||||||
for _, selectedField := range selectedFields {
|
|
||||||
if utils.Contains(selectedField, exceptFields) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p = p.Save(selectedField, selectedField)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHealthcheck(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
b := Healthcheck()
|
|
||||||
assert.True(t, b.IsHealthy, "Healthcheck failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToValue(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
// toValue()
|
|
||||||
v, err := toValue(cayley.StartPath(store, "tests").Out("are"))
|
|
||||||
assert.Nil(t, err, "toValue should work even if the requested path leads to nothing")
|
|
||||||
assert.Equal(t, "", v, "toValue should return an empty string if the requested path leads to nothing")
|
|
||||||
|
|
||||||
store.AddQuad(cayley.Triple("tests", "are", "awesome"))
|
|
||||||
v, err = toValue(cayley.StartPath(store, "tests").Out("are"))
|
|
||||||
assert.Nil(t, err, "toValue should have worked")
|
|
||||||
assert.Equal(t, "awesome", v, "toValue did not return the expected value")
|
|
||||||
|
|
||||||
store.AddQuad(cayley.Triple("tests", "are", "running"))
|
|
||||||
v, err = toValue(cayley.StartPath(store, "tests").Out("are"))
|
|
||||||
assert.NotNil(t, err, "toValue should return an error and an empty string if the path leads to multiple values")
|
|
||||||
assert.Equal(t, "", v, "toValue should return an error and an empty string if the path leads to multiple values")
|
|
||||||
|
|
||||||
// toValues()
|
|
||||||
vs, err := toValues(cayley.StartPath(store, "CoreOS").Out(fieldIs))
|
|
||||||
assert.Nil(t, err, "toValues should work even if the requested path leads to nothing")
|
|
||||||
assert.Len(t, vs, 0, "toValue should return an empty array if the requested path leads to nothing")
|
|
||||||
words := []string{"powerful", "lightweight"}
|
|
||||||
for i, word := range words {
|
|
||||||
store.AddQuad(cayley.Triple("CoreOS", fieldIs, word))
|
|
||||||
v, err := toValues(cayley.StartPath(store, "CoreOS").Out(fieldIs))
|
|
||||||
assert.Nil(t, err, "toValues should have worked")
|
|
||||||
assert.Len(t, v, i+1, "toValues did not return the right amount of values")
|
|
||||||
for _, e := range words[:i+1] {
|
|
||||||
assert.Contains(t, v, e, "toValues did not return the values we expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toValue(s)() and empty values
|
|
||||||
store.AddQuad(cayley.Triple("bob", "likes", ""))
|
|
||||||
v, err = toValue(cayley.StartPath(store, "bob").Out("likes"))
|
|
||||||
assert.Nil(t, err, "toValue should work even if the requested path leads to nothing")
|
|
||||||
assert.Equal(t, "", v, "toValue should return an empty string if the requested path leads to nothing")
|
|
||||||
|
|
||||||
store.AddQuad(cayley.Triple("bob", "likes", "running"))
|
|
||||||
v, err = toValue(cayley.StartPath(store, "bob").Out("likes"))
|
|
||||||
assert.NotNil(t, err, "toValue should return an error and an empty string if the path leads to multiple values")
|
|
||||||
assert.Equal(t, "", v, "toValue should return an error and an empty string if the path leads to multiple values")
|
|
||||||
|
|
||||||
store.AddQuad(cayley.Triple("bob", "likes", "swimming"))
|
|
||||||
va, err := toValues(cayley.StartPath(store, "bob").Out("likes"))
|
|
||||||
assert.Nil(t, err, "toValues should have worked")
|
|
||||||
if assert.Len(t, va, 3, "toValues should have returned 2 values") {
|
|
||||||
assert.Contains(t, va, "running")
|
|
||||||
assert.Contains(t, va, "swimming")
|
|
||||||
assert.Contains(t, va, "")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fieldFlagValue = "value"
|
|
||||||
flagNodePrefix = "flag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UpdateFlag creates a flag or update an existing flag's value
|
|
||||||
func UpdateFlag(name, value string) error {
|
|
||||||
if name == "" || value == "" {
|
|
||||||
log.Warning("could not insert a flag which has an empty name or value")
|
|
||||||
return cerrors.NewBadRequestError("could not insert a flag which has an empty name or value")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize transaction
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
|
|
||||||
// Get current flag value
|
|
||||||
currentValue, err := GetFlagValue(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build transaction
|
|
||||||
name = flagNodePrefix + ":" + name
|
|
||||||
if currentValue != "" {
|
|
||||||
t.RemoveQuad(cayley.Triple(name, fieldFlagValue, currentValue))
|
|
||||||
}
|
|
||||||
t.AddQuad(cayley.Triple(name, fieldFlagValue, value))
|
|
||||||
|
|
||||||
// Apply transaction
|
|
||||||
if err = store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (UpdateFlag): %s", err)
|
|
||||||
return ErrTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFlagValue returns the value of the flag given by its name (or an empty string if the flag does not exist)
|
|
||||||
func GetFlagValue(name string) (string, error) {
|
|
||||||
return toValue(cayley.StartPath(store, flagNodePrefix+":"+name).Out(fieldFlagValue))
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFlag(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
// Get non existing flag
|
|
||||||
f, err := GetFlagValue("test")
|
|
||||||
assert.Nil(t, err, "GetFlagValue should have worked")
|
|
||||||
assert.Empty(t, "", f, "Getting a non-existing flag should return an empty string")
|
|
||||||
|
|
||||||
// Try to insert invalid flags
|
|
||||||
assert.Error(t, UpdateFlag("test", ""), "It should not accept a flag with an empty name or value")
|
|
||||||
assert.Error(t, UpdateFlag("", "test"), "It should not accept a flag with an empty name or value")
|
|
||||||
assert.Error(t, UpdateFlag("", ""), "It should not accept a flag with an empty name or value")
|
|
||||||
|
|
||||||
// Insert a flag and verify its value
|
|
||||||
assert.Nil(t, UpdateFlag("test", "test1"))
|
|
||||||
f, err = GetFlagValue("test")
|
|
||||||
assert.Nil(t, err, "GetFlagValue should have worked")
|
|
||||||
assert.Equal(t, "test1", f, "GetFlagValue did not return the expected value")
|
|
||||||
|
|
||||||
// Update a flag and verify its value
|
|
||||||
assert.Nil(t, UpdateFlag("test", "test2"))
|
|
||||||
f, err = GetFlagValue("test")
|
|
||||||
assert.Nil(t, err, "GetFlagValue should have worked")
|
|
||||||
assert.Equal(t, "test2", f, "GetFlagValue did not return the expected value")
|
|
||||||
}
|
|
@ -1,432 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/utils"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/google/cayley/graph"
|
|
||||||
"github.com/google/cayley/graph/path"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FieldLayerID = "id"
|
|
||||||
FieldLayerParent = "parent"
|
|
||||||
FieldLayerSuccessors = "successors"
|
|
||||||
FieldLayerOS = "os"
|
|
||||||
FieldLayerEngineVersion = "engineVersion"
|
|
||||||
FieldLayerPackages = "adds/removes"
|
|
||||||
|
|
||||||
// These fields are not selectable and are for internal use only.
|
|
||||||
fieldLayerIsValue = "layer"
|
|
||||||
fieldLayerInstalledPackages = "adds"
|
|
||||||
fieldLayerRemovedPackages = "removes"
|
|
||||||
)
|
|
||||||
|
|
||||||
var FieldLayerAll = []string{FieldLayerID, FieldLayerParent, FieldLayerSuccessors, FieldLayerOS, FieldLayerPackages, FieldLayerEngineVersion}
|
|
||||||
|
|
||||||
// Layer represents an unique container layer
|
|
||||||
type Layer struct {
|
|
||||||
Node string `json:"-"`
|
|
||||||
ID string
|
|
||||||
ParentNode string `json:"-"`
|
|
||||||
SuccessorsNodes []string `json:"-"`
|
|
||||||
OS string
|
|
||||||
InstalledPackagesNodes []string `json:"-"`
|
|
||||||
RemovedPackagesNodes []string `json:"-"`
|
|
||||||
EngineVersion int
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode returns the node name of a Layer
|
|
||||||
// Requires the key field: ID
|
|
||||||
func (l *Layer) GetNode() string {
|
|
||||||
return fieldLayerIsValue + ":" + utils.Hash(l.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertLayer insert a single layer in the database
|
|
||||||
//
|
|
||||||
// ID, and EngineVersion fields are required.
|
|
||||||
// ParentNode, OS, InstalledPackagesNodes and RemovedPackagesNodes are optional,
|
|
||||||
// SuccessorsNodes is unnecessary.
|
|
||||||
//
|
|
||||||
// The ID MUST be unique for two different layers.
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// If the Layer already exists, nothing is done, except if the provided engine
|
|
||||||
// version is higher than the existing one, in which case, the OS,
|
|
||||||
// InstalledPackagesNodes and RemovedPackagesNodes fields will be replaced.
|
|
||||||
//
|
|
||||||
// The layer should only contains the newly installed/removed packages
|
|
||||||
// There is no safeguard that prevents from marking a package as newly installed
|
|
||||||
// while it has already been installed in one of its parent.
|
|
||||||
func InsertLayer(layer *Layer) error {
|
|
||||||
// Verify parameters
|
|
||||||
if layer.ID == "" {
|
|
||||||
log.Warning("could not insert a layer which has an empty ID")
|
|
||||||
return cerrors.NewBadRequestError("could not insert a layer which has an empty ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create required data structures
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
layer.Node = layer.GetNode()
|
|
||||||
|
|
||||||
// Try to find an existing layer
|
|
||||||
existingLayer, err := FindOneLayerByNode(layer.Node, FieldLayerAll)
|
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingLayer != nil && existingLayer.EngineVersion >= layer.EngineVersion {
|
|
||||||
// The layer exists and has an equal or higher engine verison, do nothing
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingLayer == nil {
|
|
||||||
// Create case: add permanent nodes
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, fieldIs, fieldLayerIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, FieldLayerID, layer.ID))
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, FieldLayerParent, layer.ParentNode))
|
|
||||||
} else {
|
|
||||||
// Update case: remove everything before we add updated data
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, FieldLayerOS, existingLayer.OS))
|
|
||||||
for _, pkg := range existingLayer.InstalledPackagesNodes {
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, fieldLayerInstalledPackages, pkg))
|
|
||||||
}
|
|
||||||
for _, pkg := range existingLayer.RemovedPackagesNodes {
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, fieldLayerRemovedPackages, pkg))
|
|
||||||
}
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, FieldLayerEngineVersion, strconv.Itoa(existingLayer.EngineVersion)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add OS/Packages
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, FieldLayerOS, layer.OS))
|
|
||||||
for _, pkg := range layer.InstalledPackagesNodes {
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, fieldLayerInstalledPackages, pkg))
|
|
||||||
}
|
|
||||||
for _, pkg := range layer.RemovedPackagesNodes {
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, fieldLayerRemovedPackages, pkg))
|
|
||||||
}
|
|
||||||
t.AddQuad(cayley.Triple(layer.Node, FieldLayerEngineVersion, strconv.Itoa(layer.EngineVersion)))
|
|
||||||
|
|
||||||
// Apply transaction
|
|
||||||
if err = store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (InsertLayer): %s", err)
|
|
||||||
return ErrTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteLayer deletes the specified layer and any child layers that are
|
|
||||||
// dependent on the specified layer.
|
|
||||||
func DeleteLayer(ID string) error {
|
|
||||||
layer, err := FindOneLayerByID(ID, []string{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return deleteLayerTreeFrom(layer.Node, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteLayerTreeFrom(node string, t *graph.Transaction) error {
|
|
||||||
// Determine if that function call is the root call of the recursivity
|
|
||||||
// And create transaction if its the case.
|
|
||||||
root := (t == nil)
|
|
||||||
if root {
|
|
||||||
t = cayley.NewTransaction()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find layer.
|
|
||||||
layer, err := FindOneLayerByNode(node, FieldLayerAll)
|
|
||||||
if err != nil {
|
|
||||||
// Ignore missing layer.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all successor layers.
|
|
||||||
for _, succNode := range layer.SuccessorsNodes {
|
|
||||||
deleteLayerTreeFrom(succNode, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove layer.
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, fieldIs, fieldLayerIsValue))
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, FieldLayerID, layer.ID))
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, FieldLayerParent, layer.ParentNode))
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, FieldLayerOS, layer.OS))
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, FieldLayerEngineVersion, strconv.Itoa(layer.EngineVersion)))
|
|
||||||
for _, pkg := range layer.InstalledPackagesNodes {
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, fieldLayerInstalledPackages, pkg))
|
|
||||||
}
|
|
||||||
for _, pkg := range layer.RemovedPackagesNodes {
|
|
||||||
t.RemoveQuad(cayley.Triple(layer.Node, fieldLayerRemovedPackages, pkg))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply transaction if root call.
|
|
||||||
if root {
|
|
||||||
if err = store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (deleteLayerTreeFrom): %s", err)
|
|
||||||
return ErrTransaction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindOneLayerByID finds and returns a single layer having the given ID,
|
|
||||||
// selecting the specified fields and hardcoding its ID
|
|
||||||
func FindOneLayerByID(ID string, selectedFields []string) (*Layer, error) {
|
|
||||||
t := &Layer{ID: ID}
|
|
||||||
l, err := FindOneLayerByNode(t.GetNode(), selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l.ID = ID
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindOneLayerByNode finds and returns a single package by its node, selecting the specified fields
|
|
||||||
func FindOneLayerByNode(node string, selectedFields []string) (*Layer, error) {
|
|
||||||
l, err := toLayers(cayley.StartPath(store, node).Has(fieldIs, fieldLayerIsValue), selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(l) == 1 {
|
|
||||||
return l[0], nil
|
|
||||||
}
|
|
||||||
if len(l) > 1 {
|
|
||||||
log.Errorf("found multiple layers with identical node [Node: %s]", node)
|
|
||||||
return nil, ErrInconsistent
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, cerrors.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAllLayersByAddedPackageNodes finds and returns all layers that add the
|
|
||||||
// given packages (by their nodes), selecting the specified fields
|
|
||||||
func FindAllLayersByAddedPackageNodes(nodes []string, selectedFields []string) ([]*Layer, error) {
|
|
||||||
layers, err := toLayers(cayley.StartPath(store, nodes...).In(fieldLayerInstalledPackages), selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return []*Layer{}, err
|
|
||||||
}
|
|
||||||
return layers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAllLayersByPackageNode finds and returns all layers that have the given package (by its node), selecting the specified fields
|
|
||||||
// func FindAllLayersByPackageNode(node string, only map[string]struct{}) ([]*Layer, error) {
|
|
||||||
// var layers []*Layer
|
|
||||||
//
|
|
||||||
// // We need the successors field
|
|
||||||
// if only != nil {
|
|
||||||
// only[FieldLayerSuccessors] = struct{}{}
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Get all the layers which remove the package
|
|
||||||
// layersNodesRemoving, err := toValues(cayley.StartPath(store, node).In(fieldLayerRemovedPackages).Has(fieldIs, fieldLayerIsValue))
|
|
||||||
// if err != nil {
|
|
||||||
// return []*Layer{}, err
|
|
||||||
// }
|
|
||||||
// layersNodesRemovingMap := make(map[string]struct{})
|
|
||||||
// for _, l := range layersNodesRemoving {
|
|
||||||
// layersNodesRemovingMap[l] = struct{}{}
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// layersToBrowse, err := toLayers(cayley.StartPath(store, node).In(fieldLayerInstalledPackages).Has(fieldIs, fieldLayerIsValue), only)
|
|
||||||
// if err != nil {
|
|
||||||
// return []*Layer{}, err
|
|
||||||
// }
|
|
||||||
// for len(layersToBrowse) > 0 {
|
|
||||||
// var newLayersToBrowse []*Layer
|
|
||||||
// for _, layerToBrowse := range layersToBrowse {
|
|
||||||
// if _, layerRemovesPackage := layersNodesRemovingMap[layerToBrowse.Node]; !layerRemovesPackage {
|
|
||||||
// layers = append(layers, layerToBrowse)
|
|
||||||
// successors, err := layerToBrowse.Successors(only)
|
|
||||||
// if err != nil {
|
|
||||||
// return []*Layer{}, err
|
|
||||||
// }
|
|
||||||
// newLayersToBrowse = append(newLayersToBrowse, successors...)
|
|
||||||
// }
|
|
||||||
// layersToBrowse = newLayersToBrowse
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return layers, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// toLayers converts a path leading to one or multiple layers to Layer structs,
|
|
||||||
// selecting the specified fields
|
|
||||||
func toLayers(path *path.Path, selectedFields []string) ([]*Layer, error) {
|
|
||||||
var layers []*Layer
|
|
||||||
|
|
||||||
saveFields(path, selectedFields, []string{FieldLayerSuccessors, FieldLayerPackages, fieldLayerInstalledPackages, fieldLayerRemovedPackages})
|
|
||||||
it, _ := path.BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
layer := Layer{Node: store.NameOf(it.Result())}
|
|
||||||
for _, selectedField := range selectedFields {
|
|
||||||
switch selectedField {
|
|
||||||
case FieldLayerID:
|
|
||||||
layer.ID = store.NameOf(tags[FieldLayerID])
|
|
||||||
case FieldLayerParent:
|
|
||||||
layer.ParentNode = store.NameOf(tags[FieldLayerParent])
|
|
||||||
case FieldLayerSuccessors:
|
|
||||||
var err error
|
|
||||||
layer.SuccessorsNodes, err = toValues(cayley.StartPath(store, layer.Node).In(FieldLayerParent))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("could not get successors of layer %s: %s.", layer.Node, err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FieldLayerOS:
|
|
||||||
layer.OS = store.NameOf(tags[FieldLayerOS])
|
|
||||||
case FieldLayerPackages:
|
|
||||||
var err error
|
|
||||||
it, _ := cayley.StartPath(store, layer.Node).OutWithTags([]string{"predicate"}, fieldLayerInstalledPackages, fieldLayerRemovedPackages).BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
predicate := store.NameOf(tags["predicate"])
|
|
||||||
if predicate == fieldLayerInstalledPackages {
|
|
||||||
layer.InstalledPackagesNodes = append(layer.InstalledPackagesNodes, store.NameOf(it.Result()))
|
|
||||||
} else if predicate == fieldLayerRemovedPackages {
|
|
||||||
layer.RemovedPackagesNodes = append(layer.RemovedPackagesNodes, store.NameOf(it.Result()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("could not get installed/removed packages of layer %s: %s.", layer.Node, it.Err())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FieldLayerEngineVersion:
|
|
||||||
layer.EngineVersion, _ = strconv.Atoi(store.NameOf(tags[FieldLayerEngineVersion]))
|
|
||||||
default:
|
|
||||||
panic("unknown selectedField")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
layers = append(layers, &layer)
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in toLayers: %s", it.Err())
|
|
||||||
return []*Layer{}, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return layers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successors find and returns all layers that define l as their parent,
|
|
||||||
// selecting the specified fields
|
|
||||||
// It requires that FieldLayerSuccessors field has been selected on l
|
|
||||||
// func (l *Layer) Successors(selectedFields []string) ([]*Layer, error) {
|
|
||||||
// if len(l.SuccessorsNodes) == 0 {
|
|
||||||
// return []*Layer{}, nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return toLayers(cayley.StartPath(store, l.SuccessorsNodes...), only)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Parent find and returns the parent layer of l, selecting the specified fields
|
|
||||||
// It requires that FieldLayerParent field has been selected on l
|
|
||||||
func (l *Layer) Parent(selectedFields []string) (*Layer, error) {
|
|
||||||
if l.ParentNode == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parent, err := toLayers(cayley.StartPath(store, l.ParentNode), selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(parent) == 1 {
|
|
||||||
return parent[0], nil
|
|
||||||
}
|
|
||||||
if len(parent) > 1 {
|
|
||||||
log.Errorf("found multiple layers when getting parent layer of layer %s", l.ParentNode)
|
|
||||||
return nil, ErrInconsistent
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sublayers find and returns all layers that compose l, selecting the specified
|
|
||||||
// fields
|
|
||||||
// It requires that FieldLayerParent field has been selected on l
|
|
||||||
// The base image comes first, and l is last
|
|
||||||
// func (l *Layer) Sublayers(selectedFields []string) ([]*Layer, error) {
|
|
||||||
// var sublayers []*Layer
|
|
||||||
//
|
|
||||||
// // We need the parent field
|
|
||||||
// if only != nil {
|
|
||||||
// only[FieldLayerParent] = struct{}{}
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// parent, err := l.Parent(only)
|
|
||||||
// if err != nil {
|
|
||||||
// return []*Layer{}, err
|
|
||||||
// }
|
|
||||||
// if parent != nil {
|
|
||||||
// parentSublayers, err := parent.Sublayers(only)
|
|
||||||
// if err != nil {
|
|
||||||
// return []*Layer{}, err
|
|
||||||
// }
|
|
||||||
// sublayers = append(sublayers, parentSublayers...)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// sublayers = append(sublayers, l)
|
|
||||||
//
|
|
||||||
// return sublayers, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// AllPackages computes the full list of packages that l has and return them as
|
|
||||||
// nodes.
|
|
||||||
// It requires that FieldLayerParent, FieldLayerContentInstalledPackages,
|
|
||||||
// FieldLayerContentRemovedPackages fields has been selected on l
|
|
||||||
func (l *Layer) AllPackages() ([]string, error) {
|
|
||||||
var allPackages []string
|
|
||||||
|
|
||||||
parent, err := l.Parent([]string{FieldLayerParent, FieldLayerPackages})
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
if parent != nil {
|
|
||||||
allPackages, err = parent.AllPackages()
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(utils.CompareStringLists(allPackages, l.RemovedPackagesNodes), l.InstalledPackagesNodes...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OperatingSystem tries to find the Operating System of a layer using its
|
|
||||||
// parents.
|
|
||||||
// It requires that FieldLayerParent and FieldLayerOS fields has been
|
|
||||||
// selected on l
|
|
||||||
func (l *Layer) OperatingSystem() (string, error) {
|
|
||||||
if l.OS != "" {
|
|
||||||
return l.OS, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try from the parent
|
|
||||||
parent, err := l.Parent([]string{FieldLayerParent, FieldLayerOS})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if parent != nil {
|
|
||||||
return parent.OperatingSystem()
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
@ -1,178 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/coreos/clair/utils"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestInvalidLayers tries to insert invalid layers
|
|
||||||
func TestInvalidLayers(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
assert.Error(t, InsertLayer(&Layer{ID: ""})) // No ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLayerSimple inserts a single layer and ensures it can be retrieved and
|
|
||||||
// that methods works
|
|
||||||
func TestLayerSimple(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
// Insert a layer and find it back
|
|
||||||
l1 := &Layer{ID: "l1", OS: "os1", InstalledPackagesNodes: []string{"p1", "p2"}, EngineVersion: 1}
|
|
||||||
if assert.Nil(t, InsertLayer(l1)) {
|
|
||||||
fl1, err := FindOneLayerByID(l1.ID, FieldLayerAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, fl1) {
|
|
||||||
// Saved = found
|
|
||||||
assert.True(t, layerEqual(l1, fl1), "layers are not equal, expected %v, have %s", l1, fl1)
|
|
||||||
|
|
||||||
// No parent
|
|
||||||
p, err := fl1.Parent(FieldLayerAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Nil(t, p)
|
|
||||||
|
|
||||||
// AllPackages()
|
|
||||||
pk, err := fl1.AllPackages()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
if assert.Len(t, pk, 2) {
|
|
||||||
assert.Contains(t, pk, l1.InstalledPackagesNodes[0])
|
|
||||||
assert.Contains(t, pk, l1.InstalledPackagesNodes[1])
|
|
||||||
}
|
|
||||||
// OS()
|
|
||||||
o, err := fl1.OperatingSystem()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, l1.OS, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAllLayersByAddedPackageNodes
|
|
||||||
al1, err := FindAllLayersByAddedPackageNodes([]string{"p1", "p3"}, FieldLayerAll)
|
|
||||||
if assert.Nil(t, err) && assert.Len(t, al1, 1) {
|
|
||||||
assert.Equal(t, al1[0].Node, l1.Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
if assert.Nil(t, DeleteLayer(l1.ID)) {
|
|
||||||
_, err := FindOneLayerByID(l1.ID, FieldLayerAll)
|
|
||||||
assert.Equal(t, cerrors.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLayerTree inserts a tree of layers and ensure that the tree lgoic works
|
|
||||||
func TestLayerTree(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
var layers []*Layer
|
|
||||||
layers = append(layers, &Layer{ID: "l1"})
|
|
||||||
layers = append(layers, &Layer{ID: "l2", ParentNode: layers[0].GetNode(), OS: "os2", InstalledPackagesNodes: []string{"p1", "p2"}})
|
|
||||||
layers = append(layers, &Layer{ID: "l3", ParentNode: layers[1].GetNode()}) // Repeat an empty layer archive (l1)
|
|
||||||
layers = append(layers, &Layer{ID: "l4a", ParentNode: layers[2].GetNode(), InstalledPackagesNodes: []string{"p3"}, RemovedPackagesNodes: []string{"p1", "p4"}}) // p4 does not exists and thu can't actually be removed
|
|
||||||
layers = append(layers, &Layer{ID: "l4b", ParentNode: layers[2].GetNode(), InstalledPackagesNodes: []string{}, RemovedPackagesNodes: []string{"p2", "p1"}})
|
|
||||||
|
|
||||||
var flayers []*Layer
|
|
||||||
ok := true
|
|
||||||
for _, l := range layers {
|
|
||||||
ok = ok && assert.Nil(t, InsertLayer(l))
|
|
||||||
|
|
||||||
fl, err := FindOneLayerByID(l.ID, FieldLayerAll)
|
|
||||||
ok = ok && assert.Nil(t, err)
|
|
||||||
ok = ok && assert.NotNil(t, fl)
|
|
||||||
flayers = append(flayers, fl)
|
|
||||||
}
|
|
||||||
if assert.True(t, ok) {
|
|
||||||
// Start testing
|
|
||||||
|
|
||||||
// l4a
|
|
||||||
// Parent()
|
|
||||||
fl4ap, err := flayers[3].Parent(FieldLayerAll)
|
|
||||||
assert.Nil(t, err, "l4a should has l3 as parent")
|
|
||||||
if assert.NotNil(t, fl4ap, "l4a should has l3 as parent") {
|
|
||||||
assert.Equal(t, "l3", fl4ap.ID, "l4a should has l3 as parent")
|
|
||||||
}
|
|
||||||
|
|
||||||
// OS()
|
|
||||||
fl4ao, err := flayers[3].OperatingSystem()
|
|
||||||
assert.Nil(t, err, "l4a should inherits its OS from l2")
|
|
||||||
assert.Equal(t, "os2", fl4ao, "l4a should inherits its OS from l2")
|
|
||||||
// AllPackages()
|
|
||||||
fl4apkg, err := flayers[3].AllPackages()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
if assert.Len(t, fl4apkg, 2) {
|
|
||||||
assert.Contains(t, fl4apkg, "p2")
|
|
||||||
assert.Contains(t, fl4apkg, "p3")
|
|
||||||
}
|
|
||||||
|
|
||||||
// l4b
|
|
||||||
// AllPackages()
|
|
||||||
fl4bpkg, err := flayers[4].AllPackages()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, fl4bpkg, 0)
|
|
||||||
|
|
||||||
// Delete a layer in the middle of the tree.
|
|
||||||
if assert.Nil(t, DeleteLayer(flayers[1].ID)) {
|
|
||||||
for _, l := range layers[1:] {
|
|
||||||
_, err := FindOneLayerByID(l.ID, FieldLayerAll)
|
|
||||||
assert.Equal(t, cerrors.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLayerUpdate(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
l1 := &Layer{ID: "l1", OS: "os1", InstalledPackagesNodes: []string{"p1", "p2"}, RemovedPackagesNodes: []string{"p3", "p4"}, EngineVersion: 1}
|
|
||||||
if assert.Nil(t, InsertLayer(l1)) {
|
|
||||||
// Do not update layer content if the engine versions are equals
|
|
||||||
l1b := &Layer{ID: "l1", OS: "os2", InstalledPackagesNodes: []string{"p1"}, RemovedPackagesNodes: []string{""}, EngineVersion: 1}
|
|
||||||
if assert.Nil(t, InsertLayer(l1b)) {
|
|
||||||
fl1b, err := FindOneLayerByID(l1.ID, FieldLayerAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, fl1b) {
|
|
||||||
assert.True(t, layerEqual(l1, fl1b), "layer contents are not equal, expected %v, have %s", l1, fl1b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the layer content with new data and a higher engine version
|
|
||||||
l1c := &Layer{ID: "l1", OS: "os2", InstalledPackagesNodes: []string{"p1", "p5"}, RemovedPackagesNodes: []string{"p6", "p7"}, EngineVersion: 2}
|
|
||||||
if assert.Nil(t, InsertLayer(l1c)) {
|
|
||||||
fl1c, err := FindOneLayerByID(l1c.ID, FieldLayerAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, fl1c) {
|
|
||||||
assert.True(t, layerEqual(l1c, fl1c), "layer contents are not equal, expected %v, have %s", l1c, fl1c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func layerEqual(expected, actual *Layer) bool {
|
|
||||||
eq := true
|
|
||||||
eq = eq && expected.Node == actual.Node
|
|
||||||
eq = eq && expected.ID == actual.ID
|
|
||||||
eq = eq && expected.ParentNode == actual.ParentNode
|
|
||||||
eq = eq && expected.OS == actual.OS
|
|
||||||
eq = eq && expected.EngineVersion == actual.EngineVersion
|
|
||||||
eq = eq && len(utils.CompareStringLists(actual.SuccessorsNodes, expected.SuccessorsNodes)) == 0 && len(utils.CompareStringLists(expected.SuccessorsNodes, actual.SuccessorsNodes)) == 0
|
|
||||||
eq = eq && len(utils.CompareStringLists(actual.RemovedPackagesNodes, expected.RemovedPackagesNodes)) == 0 && len(utils.CompareStringLists(expected.RemovedPackagesNodes, actual.RemovedPackagesNodes)) == 0
|
|
||||||
eq = eq && len(utils.CompareStringLists(actual.InstalledPackagesNodes, expected.InstalledPackagesNodes)) == 0 && len(utils.CompareStringLists(expected.InstalledPackagesNodes, actual.InstalledPackagesNodes)) == 0
|
|
||||||
return eq
|
|
||||||
}
|
|
163
database/lock.go
163
database/lock.go
@ -1,163 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/barakmich/glog"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/google/cayley/graph"
|
|
||||||
"github.com/google/cayley/graph/path"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fieldLockLocked = "locked"
|
|
||||||
fieldLockLockedValue = "locked"
|
|
||||||
fieldLockLockedBy = "locked_by"
|
|
||||||
fieldLockLockedUntil = "locked_until"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Lock tries to set a temporary lock in the database.
|
|
||||||
// If a lock already exists with the given name/owner, then the lock is renewed
|
|
||||||
//
|
|
||||||
// Lock does not block, instead, it returns true and its expiration time
|
|
||||||
// is the lock has been successfully acquired or false otherwise
|
|
||||||
func Lock(name string, duration time.Duration, owner string) (bool, time.Time) {
|
|
||||||
pruneLocks()
|
|
||||||
|
|
||||||
until := time.Now().Add(duration)
|
|
||||||
untilString := strconv.FormatInt(until.Unix(), 10)
|
|
||||||
|
|
||||||
// Try to get the expiration time of a lock with the same name/owner
|
|
||||||
currentExpiration, err := toValue(cayley.StartPath(store, name).Has(fieldLockLockedBy, owner).Out(fieldLockLockedUntil))
|
|
||||||
if err == nil && currentExpiration != "" {
|
|
||||||
// Renew our lock
|
|
||||||
if currentExpiration == untilString {
|
|
||||||
return true, until
|
|
||||||
}
|
|
||||||
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
t.RemoveQuad(cayley.Triple(name, fieldLockLockedUntil, currentExpiration))
|
|
||||||
t.AddQuad(cayley.Triple(name, fieldLockLockedUntil, untilString))
|
|
||||||
// It is not necessary to verify if the lock is ours again in the transaction
|
|
||||||
// because if someone took it, the lock's current expiration probably changed and the transaction will fail
|
|
||||||
return store.ApplyTransaction(t) == nil, until
|
|
||||||
}
|
|
||||||
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
t.AddQuad(cayley.Triple(name, fieldLockLocked, fieldLockLockedValue)) // Necessary to make the transaction fails if the lock already exists (and has not been pruned)
|
|
||||||
t.AddQuad(cayley.Triple(name, fieldLockLockedUntil, untilString))
|
|
||||||
t.AddQuad(cayley.Triple(name, fieldLockLockedBy, owner))
|
|
||||||
|
|
||||||
glog.SetStderrThreshold("FATAL")
|
|
||||||
success := store.ApplyTransaction(t) == nil
|
|
||||||
glog.SetStderrThreshold("ERROR")
|
|
||||||
|
|
||||||
return success, until
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock unlocks a lock specified by its name if I own it
|
|
||||||
func Unlock(name, owner string) {
|
|
||||||
unlocked := 0
|
|
||||||
it, _ := cayley.StartPath(store, name).Has(fieldLockLocked, fieldLockLockedValue).Has(fieldLockLockedBy, owner).Save(fieldLockLockedUntil, fieldLockLockedUntil).BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
t.RemoveQuad(cayley.Triple(name, fieldLockLocked, fieldLockLockedValue))
|
|
||||||
t.RemoveQuad(cayley.Triple(name, fieldLockLockedUntil, store.NameOf(tags[fieldLockLockedUntil])))
|
|
||||||
t.RemoveQuad(cayley.Triple(name, fieldLockLockedBy, owner))
|
|
||||||
err := store.ApplyTransaction(t)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed transaction (Unlock): %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unlocked++
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in Unlock: %s", it.Err())
|
|
||||||
}
|
|
||||||
if unlocked > 1 {
|
|
||||||
// We should never see this, it would mean that our database doesn't ensure quad uniqueness
|
|
||||||
// and that the entire lock system is jeopardized.
|
|
||||||
log.Errorf("found inconsistency in Unlock: matched %d times a locked named: %s", unlocked, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LockInfo returns the owner of a lock specified by its name and its
|
|
||||||
// expiration time
|
|
||||||
func LockInfo(name string) (string, time.Time, error) {
|
|
||||||
it, _ := cayley.StartPath(store, name).Has(fieldLockLocked, fieldLockLockedValue).Save(fieldLockLockedUntil, fieldLockLockedUntil).Save(fieldLockLockedBy, fieldLockLockedBy).BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
tt, _ := strconv.ParseInt(store.NameOf(tags[fieldLockLockedUntil]), 10, 64)
|
|
||||||
return store.NameOf(tags[fieldLockLockedBy]), time.Unix(tt, 0), nil
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in LockInfo: %s", it.Err())
|
|
||||||
return "", time.Time{}, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", time.Time{}, cerrors.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// pruneLocks removes every expired locks from the database
|
|
||||||
func pruneLocks() {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
// Delete every expired locks
|
|
||||||
it, _ := cayley.StartPath(store, "locked").In("locked").Save(fieldLockLockedUntil, fieldLockLockedUntil).Save(fieldLockLockedBy, fieldLockLockedBy).BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
n := store.NameOf(it.Result())
|
|
||||||
t := store.NameOf(tags[fieldLockLockedUntil])
|
|
||||||
o := store.NameOf(tags[fieldLockLockedBy])
|
|
||||||
tt, _ := strconv.ParseInt(t, 10, 64)
|
|
||||||
|
|
||||||
if now.Unix() > tt {
|
|
||||||
log.Debugf("lock %s owned by %s has expired.", n, o)
|
|
||||||
|
|
||||||
tr := cayley.NewTransaction()
|
|
||||||
tr.RemoveQuad(cayley.Triple(n, fieldLockLocked, fieldLockLockedValue))
|
|
||||||
tr.RemoveQuad(cayley.Triple(n, fieldLockLockedUntil, t))
|
|
||||||
tr.RemoveQuad(cayley.Triple(n, fieldLockLockedBy, o))
|
|
||||||
err := store.ApplyTransaction(tr)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed transaction (pruneLocks): %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Debugf("lock %s has been successfully pruned.", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in Unlock: %s", it.Err())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLockedNodes returns every nodes that are currently locked
|
|
||||||
func getLockedNodes() *path.Path {
|
|
||||||
return cayley.StartPath(store, "locked").In("locked")
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLock(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
var l bool
|
|
||||||
var et time.Time
|
|
||||||
|
|
||||||
// Create a first lock
|
|
||||||
l, _ = Lock("test1", time.Minute, "owner1")
|
|
||||||
assert.True(t, l)
|
|
||||||
// Try to lock the same lock with another owner
|
|
||||||
l, _ = Lock("test1", time.Minute, "owner2")
|
|
||||||
assert.False(t, l)
|
|
||||||
// Renew the lock
|
|
||||||
l, _ = Lock("test1", 2*time.Minute, "owner1")
|
|
||||||
assert.True(t, l)
|
|
||||||
// Unlock and then relock by someone else
|
|
||||||
Unlock("test1", "owner1")
|
|
||||||
l, et = Lock("test1", time.Minute, "owner2")
|
|
||||||
assert.True(t, l)
|
|
||||||
// LockInfo
|
|
||||||
o, et2, err := LockInfo("test1")
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, "owner2", o)
|
|
||||||
assert.Equal(t, et.Second(), et2.Second())
|
|
||||||
|
|
||||||
// Create a second lock which is actually already expired ...
|
|
||||||
l, _ = Lock("test2", -time.Minute, "owner1")
|
|
||||||
assert.True(t, l)
|
|
||||||
// Take over the lock
|
|
||||||
l, _ = Lock("test2", time.Minute, "owner2")
|
|
||||||
assert.True(t, l)
|
|
||||||
}
|
|
55
database/models.go
Normal file
55
database/models.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "github.com/coreos/clair/utils/types"
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Layer struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Name string
|
||||||
|
EngineVersion int
|
||||||
|
Parent *Layer
|
||||||
|
Namespace *Namespace
|
||||||
|
Features []FeatureVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
type Namespace struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feature struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Namespace Namespace
|
||||||
|
// FixedBy map[types.Version]Vulnerability // <<-- WRONG.
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureVersion struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Feature Feature
|
||||||
|
Version types.Version
|
||||||
|
AffectedBy []Vulnerability
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vulnerability struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Namespace Namespace
|
||||||
|
Description string
|
||||||
|
Link string
|
||||||
|
Severity types.Priority
|
||||||
|
// FixedIn map[types.Version]Feature // <<-- WRONG.
|
||||||
|
Affects []FeatureVersion
|
||||||
|
|
||||||
|
// For output purposes. Only make sense when the vulnerability
|
||||||
|
// is already about a specific Feature/FeatureVersion.
|
||||||
|
FixedBy types.Version
|
||||||
|
}
|
@ -15,7 +15,6 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
// DebianReleasesMapping translates Debian code names and class names to version numbers
|
// DebianReleasesMapping translates Debian code names and class names to version numbers
|
||||||
// TODO That should probably be stored in the database or in a file
|
|
||||||
var DebianReleasesMapping = map[string]string{
|
var DebianReleasesMapping = map[string]string{
|
||||||
// Code names
|
// Code names
|
||||||
"squeeze": "6",
|
"squeeze": "6",
|
||||||
@ -32,7 +31,6 @@ var DebianReleasesMapping = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UbuntuReleasesMapping translates Ubuntu code names to version numbers
|
// UbuntuReleasesMapping translates Ubuntu code names to version numbers
|
||||||
// TODO That should probably be stored in the database or in a file
|
|
||||||
var UbuntuReleasesMapping = map[string]string{
|
var UbuntuReleasesMapping = map[string]string{
|
||||||
"precise": "12.04",
|
"precise": "12.04",
|
||||||
"quantal": "12.10",
|
"quantal": "12.10",
|
@ -1,409 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/utils"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/coreos/clair/utils/types"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/google/cayley/graph"
|
|
||||||
"github.com/pborman/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maxNotifications is the number of notifications that InsertNotifications
|
|
||||||
// will accept at the same time. Above this number, notifications are ignored.
|
|
||||||
maxNotifications = 100
|
|
||||||
|
|
||||||
fieldNotificationIsValue = "notification"
|
|
||||||
fieldNotificationType = "type"
|
|
||||||
fieldNotificationData = "data"
|
|
||||||
fieldNotificationIsSent = "isSent"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Notification defines an interface to a message that can be sent by a
|
|
||||||
// notifier.Notifier.
|
|
||||||
// A NotificationWrapper has to be used to convert it into a NotificationWrap,
|
|
||||||
// which can be stored in the database.
|
|
||||||
type Notification interface {
|
|
||||||
// GetName returns the explicit (humanly meaningful) name of a notification.
|
|
||||||
GetName() string
|
|
||||||
// GetType returns the type of a notification, which is used by a
|
|
||||||
// NotificationWrapper to determine the concrete type of a Notification.
|
|
||||||
GetType() string
|
|
||||||
// GetContent returns the content of the notification.
|
|
||||||
GetContent() (interface{}, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotificationWrapper is an interface defined how to convert a Notification to
|
|
||||||
// a NotificationWrap object and vice-versa.
|
|
||||||
type NotificationWrapper interface {
|
|
||||||
// Wrap packs a Notification instance into a new NotificationWrap.
|
|
||||||
Wrap(n Notification) (*NotificationWrap, error)
|
|
||||||
// Unwrap unpacks an instance of NotificationWrap into a new Notification.
|
|
||||||
Unwrap(nw *NotificationWrap) (Notification, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A NotificationWrap wraps a Notification into something that can be stored in
|
|
||||||
// the database. A NotificationWrapper has to be used to convert it into a
|
|
||||||
// Notification.
|
|
||||||
type NotificationWrap struct {
|
|
||||||
Type string
|
|
||||||
Data string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultWrapper is an implementation of NotificationWrapper that supports
|
|
||||||
// NewVulnerabilityNotification notifications.
|
|
||||||
type DefaultWrapper struct{}
|
|
||||||
|
|
||||||
func (w *DefaultWrapper) Wrap(n Notification) (*NotificationWrap, error) {
|
|
||||||
data, err := json.Marshal(n)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("could not marshal notification [ID: %s, Type: %s]: %s", n.GetName(), n.GetType(), err)
|
|
||||||
return nil, cerrors.NewBadRequestError("could not marshal notification with DefaultWrapper")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &NotificationWrap{Type: n.GetType(), Data: string(data)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *DefaultWrapper) Unwrap(nw *NotificationWrap) (Notification, error) {
|
|
||||||
var v Notification
|
|
||||||
|
|
||||||
// Create struct depending on the type
|
|
||||||
switch nw.Type {
|
|
||||||
case "NewVulnerabilityNotification":
|
|
||||||
v = &NewVulnerabilityNotification{}
|
|
||||||
case "VulnerabilityPriorityIncreasedNotification":
|
|
||||||
v = &VulnerabilityPriorityIncreasedNotification{}
|
|
||||||
case "VulnerabilityPackageChangedNotification":
|
|
||||||
v = &VulnerabilityPackageChangedNotification{}
|
|
||||||
default:
|
|
||||||
log.Warningf("could not unwrap notification [Type: %s]: unknown type for DefaultWrapper", nw.Type)
|
|
||||||
return nil, cerrors.NewBadRequestError("could not unwrap notification")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal notification
|
|
||||||
err := json.Unmarshal([]byte(nw.Data), v)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("could not unmarshal notification with DefaultWrapper [Type: %s]: %s", nw.Type, err)
|
|
||||||
return nil, cerrors.NewBadRequestError("could not unmarshal notification")
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefaultNotificationWrapper returns the default wrapper
|
|
||||||
func GetDefaultNotificationWrapper() NotificationWrapper {
|
|
||||||
return &DefaultWrapper{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A NewVulnerabilityNotification is a notification that informs about a new
|
|
||||||
// vulnerability and contains all the layers that introduce that vulnerability
|
|
||||||
type NewVulnerabilityNotification struct {
|
|
||||||
VulnerabilityID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NewVulnerabilityNotification) GetName() string {
|
|
||||||
return n.VulnerabilityID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NewVulnerabilityNotification) GetType() string {
|
|
||||||
return "NewVulnerabilityNotification"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NewVulnerabilityNotification) GetContent() (interface{}, error) {
|
|
||||||
// This notification is about a new vulnerability
|
|
||||||
// Returns the list of layers that introduce this vulnerability
|
|
||||||
|
|
||||||
// Find vulnerability.
|
|
||||||
vulnerability, err := FindOneVulnerability(n.VulnerabilityID, []string{FieldVulnerabilityID, FieldVulnerabilityLink, FieldVulnerabilityPriority, FieldVulnerabilityDescription, FieldVulnerabilityFixedIn})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
abstractVulnerability, err := vulnerability.ToAbstractVulnerability()
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
layers, err := FindAllLayersIntroducingVulnerability(n.VulnerabilityID, []string{FieldLayerID})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
layersIDs := []string{} // empty slice, not null
|
|
||||||
for _, l := range layers {
|
|
||||||
layersIDs = append(layersIDs, l.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
Vulnerability *AbstractVulnerability
|
|
||||||
IntroducingLayersIDs []string
|
|
||||||
}{
|
|
||||||
Vulnerability: abstractVulnerability,
|
|
||||||
IntroducingLayersIDs: layersIDs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// A VulnerabilityPriorityIncreasedNotification is a notification that informs
|
|
||||||
// about the fact that the priority of a vulnerability increased
|
|
||||||
// vulnerability and contains all the layers that introduce that vulnerability.
|
|
||||||
type VulnerabilityPriorityIncreasedNotification struct {
|
|
||||||
VulnerabilityID string
|
|
||||||
OldPriority, NewPriority types.Priority
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *VulnerabilityPriorityIncreasedNotification) GetName() string {
|
|
||||||
return n.VulnerabilityID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *VulnerabilityPriorityIncreasedNotification) GetType() string {
|
|
||||||
return "VulnerabilityPriorityIncreasedNotification"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *VulnerabilityPriorityIncreasedNotification) GetContent() (interface{}, error) {
|
|
||||||
// Returns the list of layers that introduce this vulnerability
|
|
||||||
// And both the old and new priorities
|
|
||||||
|
|
||||||
// Find vulnerability.
|
|
||||||
vulnerability, err := FindOneVulnerability(n.VulnerabilityID, []string{FieldVulnerabilityID, FieldVulnerabilityLink, FieldVulnerabilityPriority, FieldVulnerabilityDescription, FieldVulnerabilityFixedIn})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
abstractVulnerability, err := vulnerability.ToAbstractVulnerability()
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
layers, err := FindAllLayersIntroducingVulnerability(n.VulnerabilityID, []string{FieldLayerID})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
layersIDs := []string{} // empty slice, not null
|
|
||||||
for _, l := range layers {
|
|
||||||
layersIDs = append(layersIDs, l.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
Vulnerability *AbstractVulnerability
|
|
||||||
OldPriority, NewPriority types.Priority
|
|
||||||
IntroducingLayersIDs []string
|
|
||||||
}{
|
|
||||||
Vulnerability: abstractVulnerability,
|
|
||||||
OldPriority: n.OldPriority,
|
|
||||||
NewPriority: n.NewPriority,
|
|
||||||
IntroducingLayersIDs: layersIDs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// A VulnerabilityPackageChangedNotification is a notification that informs that
|
|
||||||
// an existing vulnerability's fixed package list has been updated and may not
|
|
||||||
// affect some layers anymore or may affect new layers.
|
|
||||||
type VulnerabilityPackageChangedNotification struct {
|
|
||||||
VulnerabilityID string
|
|
||||||
AddedFixedInNodes, RemovedFixedInNodes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *VulnerabilityPackageChangedNotification) GetName() string {
|
|
||||||
return n.VulnerabilityID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *VulnerabilityPackageChangedNotification) GetType() string {
|
|
||||||
return "VulnerabilityPackageChangedNotification"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *VulnerabilityPackageChangedNotification) GetContent() (interface{}, error) {
|
|
||||||
// Returns the removed and added packages as well as the layers that
|
|
||||||
// introduced the vulnerability in the past but don't anymore because of the
|
|
||||||
// removed packages and the layers that now introduce the vulnerability
|
|
||||||
// because of the added packages
|
|
||||||
|
|
||||||
// Find vulnerability.
|
|
||||||
vulnerability, err := FindOneVulnerability(n.VulnerabilityID, []string{FieldVulnerabilityID, FieldVulnerabilityLink, FieldVulnerabilityPriority, FieldVulnerabilityDescription, FieldVulnerabilityFixedIn})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
abstractVulnerability, err := vulnerability.ToAbstractVulnerability()
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// First part of the answer : added/removed packages
|
|
||||||
addedPackages, err := FindAllPackagesByNodes(n.AddedFixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion, FieldPackagePreviousVersion})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
removedPackages, err := FindAllPackagesByNodes(n.RemovedFixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion, FieldPackagePreviousVersion})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second part of the answer
|
|
||||||
var addedPackagesPreviousVersions []string
|
|
||||||
for _, pkg := range addedPackages {
|
|
||||||
previousVersions, err := pkg.PreviousVersions([]string{})
|
|
||||||
if err != nil {
|
|
||||||
return []*Layer{}, err
|
|
||||||
}
|
|
||||||
for _, version := range previousVersions {
|
|
||||||
addedPackagesPreviousVersions = append(addedPackagesPreviousVersions, version.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var removedPackagesPreviousVersions []string
|
|
||||||
for _, pkg := range removedPackages {
|
|
||||||
previousVersions, err := pkg.PreviousVersions([]string{})
|
|
||||||
if err != nil {
|
|
||||||
return []*Layer{}, err
|
|
||||||
}
|
|
||||||
for _, version := range previousVersions {
|
|
||||||
removedPackagesPreviousVersions = append(removedPackagesPreviousVersions, version.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newIntroducingLayers, err := FindAllLayersByAddedPackageNodes(addedPackagesPreviousVersions, []string{FieldLayerID})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
formerIntroducingLayers, err := FindAllLayersByAddedPackageNodes(removedPackagesPreviousVersions, []string{FieldLayerID})
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
newIntroducingLayersIDs := []string{} // empty slice, not null
|
|
||||||
for _, l := range newIntroducingLayers {
|
|
||||||
newIntroducingLayersIDs = append(newIntroducingLayersIDs, l.ID)
|
|
||||||
}
|
|
||||||
formerIntroducingLayersIDs := []string{} // empty slice, not null
|
|
||||||
for _, l := range formerIntroducingLayers {
|
|
||||||
formerIntroducingLayersIDs = append(formerIntroducingLayersIDs, l.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove layers which appears both in new and former lists (eg. case of updated packages but still vulnerable)
|
|
||||||
filteredNewIntroducingLayersIDs := utils.CompareStringLists(newIntroducingLayersIDs, formerIntroducingLayersIDs)
|
|
||||||
filteredFormerIntroducingLayersIDs := utils.CompareStringLists(formerIntroducingLayersIDs, newIntroducingLayersIDs)
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
Vulnerability *AbstractVulnerability
|
|
||||||
AddedAffectedPackages, RemovedAffectedPackages []*AbstractPackage
|
|
||||||
NewIntroducingLayersIDs, FormerIntroducingLayerIDs []string
|
|
||||||
}{
|
|
||||||
Vulnerability: abstractVulnerability,
|
|
||||||
AddedAffectedPackages: PackagesToAbstractPackages(addedPackages),
|
|
||||||
RemovedAffectedPackages: PackagesToAbstractPackages(removedPackages),
|
|
||||||
NewIntroducingLayersIDs: filteredNewIntroducingLayersIDs,
|
|
||||||
FormerIntroducingLayerIDs: filteredFormerIntroducingLayersIDs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertNotifications stores multiple Notification in the database
|
|
||||||
// It uses the given NotificationWrapper to convert these notifications to
|
|
||||||
// something that can be stored in the database.
|
|
||||||
func InsertNotifications(notifications []Notification, wrapper NotificationWrapper) error {
|
|
||||||
if len(notifications) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not send notifications if there are too many of them (first update for example)
|
|
||||||
if len(notifications) > maxNotifications {
|
|
||||||
log.Noticef("Ignoring %d notifications", len(notifications))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize transaction
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
|
|
||||||
// Iterate over all the vulnerabilities we need to insert
|
|
||||||
for _, notification := range notifications {
|
|
||||||
// Wrap notification
|
|
||||||
wrappedNotification, err := wrapper.Wrap(notification)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
node := fieldNotificationIsValue + ":" + uuid.New()
|
|
||||||
t.AddQuad(cayley.Triple(node, fieldIs, fieldNotificationIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(node, fieldNotificationType, wrappedNotification.Type))
|
|
||||||
t.AddQuad(cayley.Triple(node, fieldNotificationData, wrappedNotification.Data))
|
|
||||||
t.AddQuad(cayley.Triple(node, fieldNotificationIsSent, strconv.FormatBool(false)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply transaction
|
|
||||||
if err := store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (InsertNotifications): %s", err)
|
|
||||||
return ErrTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindOneNotificationToSend finds and returns a notification that is not sent
|
|
||||||
// yet and not locked. Returns nil if there is none.
|
|
||||||
func FindOneNotificationToSend(wrapper NotificationWrapper) (string, Notification, error) {
|
|
||||||
it, _ := cayley.StartPath(store, fieldNotificationIsValue).In(fieldIs).Has(fieldNotificationIsSent, strconv.FormatBool(false)).Except(getLockedNodes()).Save(fieldNotificationType, fieldNotificationType).Save(fieldNotificationData, fieldNotificationData).BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
notification, err := wrapper.Unwrap(&NotificationWrap{Type: store.NameOf(tags[fieldNotificationType]), Data: store.NameOf(tags[fieldNotificationData])})
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return store.NameOf(it.Result()), notification, nil
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in FindOneNotificationToSend: %s", it.Err())
|
|
||||||
return "", nil, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountNotificationsToSend returns the number of pending notifications
|
|
||||||
// Note that it also count the locked notifications.
|
|
||||||
func CountNotificationsToSend() (int, error) {
|
|
||||||
c := 0
|
|
||||||
|
|
||||||
it, _ := cayley.StartPath(store, fieldNotificationIsValue).In(fieldIs).Has(fieldNotificationIsSent, strconv.FormatBool(false)).BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
c = c + 1
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in CountNotificationsToSend: %s", it.Err())
|
|
||||||
return 0, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkNotificationAsSent marks a notification as sent.
|
|
||||||
func MarkNotificationAsSent(node string) {
|
|
||||||
// Initialize transaction
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
|
|
||||||
t.RemoveQuad(cayley.Triple(node, fieldNotificationIsSent, strconv.FormatBool(false)))
|
|
||||||
t.AddQuad(cayley.Triple(node, fieldNotificationIsSent, strconv.FormatBool(true)))
|
|
||||||
|
|
||||||
// Apply transaction
|
|
||||||
store.ApplyTransaction(t)
|
|
||||||
}
|
|
@ -1,145 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestWrapper struct{}
|
|
||||||
|
|
||||||
func (w *TestWrapper) Wrap(n Notification) (*NotificationWrap, error) {
|
|
||||||
data, err := json.Marshal(n)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &NotificationWrap{Type: n.GetType(), Data: string(data)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TestWrapper) Unwrap(nw *NotificationWrap) (Notification, error) {
|
|
||||||
var v Notification
|
|
||||||
|
|
||||||
switch nw.Type {
|
|
||||||
case "ntest1":
|
|
||||||
v = &NotificationTest1{}
|
|
||||||
case "ntest2":
|
|
||||||
v = &NotificationTest2{}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Could not Unwrap NotificationWrapper [Type: %s, Data: %s]: Unknown notification type.", nw.Type, nw.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(nw.Data), v)
|
|
||||||
return v, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationTest1 struct {
|
|
||||||
Test1 string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NotificationTest1) GetName() string {
|
|
||||||
return n.Test1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NotificationTest1) GetType() string {
|
|
||||||
return "ntest1"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NotificationTest1) GetContent() (interface{}, error) {
|
|
||||||
return struct{ Test1 string }{Test1: n.Test1}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationTest2 struct {
|
|
||||||
Test2 string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NotificationTest2) GetName() string {
|
|
||||||
return n.Test2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NotificationTest2) GetType() string {
|
|
||||||
return "ntest2"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NotificationTest2) GetContent() (interface{}, error) {
|
|
||||||
return struct{ Test2 string }{Test2: n.Test2}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotification(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
wrapper := &TestWrapper{}
|
|
||||||
|
|
||||||
// Insert two notifications of different types
|
|
||||||
n1 := &NotificationTest1{Test1: "test1"}
|
|
||||||
n2 := &NotificationTest2{Test2: "test2"}
|
|
||||||
err := InsertNotifications([]Notification{n1, n2}, &TestWrapper{})
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
// Count notifications to send
|
|
||||||
c, err := CountNotificationsToSend()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, 2, c)
|
|
||||||
|
|
||||||
foundN1 := false
|
|
||||||
foundN2 := false
|
|
||||||
|
|
||||||
// Select the first one
|
|
||||||
node, n, err := FindOneNotificationToSend(wrapper)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
if assert.NotNil(t, n) {
|
|
||||||
if reflect.DeepEqual(n1, n) {
|
|
||||||
foundN1 = true
|
|
||||||
} else if reflect.DeepEqual(n2, n) {
|
|
||||||
foundN2 = true
|
|
||||||
} else {
|
|
||||||
assert.Fail(t, "did not find any expected notification")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the first one as sent
|
|
||||||
MarkNotificationAsSent(node)
|
|
||||||
|
|
||||||
// Count notifications to send
|
|
||||||
c, err = CountNotificationsToSend()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, 1, c)
|
|
||||||
|
|
||||||
// Select again
|
|
||||||
node, n, err = FindOneNotificationToSend(wrapper)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
if foundN1 {
|
|
||||||
assert.Equal(t, n2, n)
|
|
||||||
} else if foundN2 {
|
|
||||||
assert.Equal(t, n1, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock the second one
|
|
||||||
Lock(node, time.Minute, "TestNotification")
|
|
||||||
|
|
||||||
// Select again
|
|
||||||
_, n, err = FindOneNotificationToSend(wrapper)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, nil, n)
|
|
||||||
}
|
|
@ -1,448 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/utils"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/coreos/clair/utils/types"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/google/cayley/graph"
|
|
||||||
"github.com/google/cayley/graph/path"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FieldPackageOS = "os"
|
|
||||||
FieldPackageName = "name"
|
|
||||||
FieldPackageVersion = "version"
|
|
||||||
FieldPackageNextVersion = "nextVersion"
|
|
||||||
FieldPackagePreviousVersion = "previousVersion"
|
|
||||||
|
|
||||||
// This field is not selectable and is for internal use only.
|
|
||||||
fieldPackageIsValue = "package"
|
|
||||||
)
|
|
||||||
|
|
||||||
var FieldPackageAll = []string{FieldPackageOS, FieldPackageName, FieldPackageVersion, FieldPackageNextVersion, FieldPackagePreviousVersion}
|
|
||||||
|
|
||||||
// Package represents a package
|
|
||||||
type Package struct {
|
|
||||||
Node string `json:"-"`
|
|
||||||
OS string
|
|
||||||
Name string
|
|
||||||
Version types.Version
|
|
||||||
NextVersionNode string `json:"-"`
|
|
||||||
PreviousVersionNode string `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode returns an unique identifier for the graph node
|
|
||||||
// Requires the key fields: OS, Name, Version
|
|
||||||
func (p *Package) GetNode() string {
|
|
||||||
return fieldPackageIsValue + ":" + utils.Hash(p.Key())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key returns an unique string defining p
|
|
||||||
// Requires the key fields: OS, Name, Version
|
|
||||||
func (p *Package) Key() string {
|
|
||||||
return p.OS + ":" + p.Name + ":" + p.Version.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Branch returns an unique string defined the Branch of p (os, name)
|
|
||||||
// Requires the key fields: OS, Name
|
|
||||||
func (p *Package) Branch() string {
|
|
||||||
return p.OS + ":" + p.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractPackage is a package that abstract types.MaxVersion by modifying
|
|
||||||
// using a AllVersion boolean field and renaming Version to BeforeVersion
|
|
||||||
// which makes more sense for an usage with a Vulnerability
|
|
||||||
type AbstractPackage struct {
|
|
||||||
OS string
|
|
||||||
Name string
|
|
||||||
|
|
||||||
AllVersions bool
|
|
||||||
BeforeVersion types.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
// PackagesToAbstractPackages converts several Packages to AbstractPackages
|
|
||||||
func PackagesToAbstractPackages(packages []*Package) (abstractPackages []*AbstractPackage) {
|
|
||||||
for _, p := range packages {
|
|
||||||
ap := &AbstractPackage{OS: p.OS, Name: p.Name}
|
|
||||||
if p.Version != types.MaxVersion {
|
|
||||||
ap.BeforeVersion = p.Version
|
|
||||||
} else {
|
|
||||||
ap.AllVersions = true
|
|
||||||
}
|
|
||||||
abstractPackages = append(abstractPackages, ap)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractPackagesToPackages converts several AbstractPackages to Packages
|
|
||||||
func AbstractPackagesToPackages(abstractPackages []*AbstractPackage) (packages []*Package) {
|
|
||||||
for _, ap := range abstractPackages {
|
|
||||||
p := &Package{OS: ap.OS, Name: ap.Name}
|
|
||||||
if ap.AllVersions {
|
|
||||||
p.Version = types.MaxVersion
|
|
||||||
} else {
|
|
||||||
p.Version = ap.BeforeVersion
|
|
||||||
}
|
|
||||||
packages = append(packages, p)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertPackages inserts several packages in the database in one transaction
|
|
||||||
// Packages are stored in linked lists, one per Branch. Each linked list has a start package and an end package defined with types.MinVersion/types.MaxVersion versions
|
|
||||||
//
|
|
||||||
// OS, Name and Version fields have to be specified.
|
|
||||||
// If the insertion is successfull, the Node field is filled and represents the graph node identifier.
|
|
||||||
func InsertPackages(packageParameters []*Package) error {
|
|
||||||
if len(packageParameters) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify parameters
|
|
||||||
for _, pkg := range packageParameters {
|
|
||||||
if pkg.OS == "" || pkg.Name == "" || pkg.Version.String() == "" {
|
|
||||||
log.Warningf("could not insert an incomplete package [OS: %s, Name: %s, Version: %s]", pkg.OS, pkg.Name, pkg.Version)
|
|
||||||
return cerrors.NewBadRequestError("could not insert an incomplete package")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over all the packages we need to insert
|
|
||||||
for _, packageParameter := range packageParameters {
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
|
|
||||||
// Is the package already existing ?
|
|
||||||
pkg, err := FindOnePackage(packageParameter.OS, packageParameter.Name, packageParameter.Version, []string{})
|
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if pkg != nil {
|
|
||||||
packageParameter.Node = pkg.Node
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all packages of the same branch (both from local cache and database)
|
|
||||||
branchPackages, err := FindAllPackagesByBranch(packageParameter.OS, packageParameter.Name, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion, FieldPackageNextVersion})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(branchPackages) == 0 {
|
|
||||||
// The branch does not exist yet
|
|
||||||
insertingStartPackage := packageParameter.Version == types.MinVersion
|
|
||||||
insertingEndPackage := packageParameter.Version == types.MaxVersion
|
|
||||||
|
|
||||||
// Create and insert a end package
|
|
||||||
endPackage := &Package{
|
|
||||||
OS: packageParameter.OS,
|
|
||||||
Name: packageParameter.Name,
|
|
||||||
Version: types.MaxVersion,
|
|
||||||
}
|
|
||||||
endPackage.Node = endPackage.GetNode()
|
|
||||||
|
|
||||||
t.AddQuad(cayley.Triple(endPackage.Node, fieldIs, fieldPackageIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(endPackage.Node, FieldPackageOS, endPackage.OS))
|
|
||||||
t.AddQuad(cayley.Triple(endPackage.Node, FieldPackageName, endPackage.Name))
|
|
||||||
t.AddQuad(cayley.Triple(endPackage.Node, FieldPackageVersion, endPackage.Version.String()))
|
|
||||||
t.AddQuad(cayley.Triple(endPackage.Node, FieldPackageNextVersion, ""))
|
|
||||||
|
|
||||||
// Create the inserted package if it is different than a start/end package
|
|
||||||
var newPackage *Package
|
|
||||||
if !insertingStartPackage && !insertingEndPackage {
|
|
||||||
newPackage = &Package{
|
|
||||||
OS: packageParameter.OS,
|
|
||||||
Name: packageParameter.Name,
|
|
||||||
Version: packageParameter.Version,
|
|
||||||
}
|
|
||||||
newPackage.Node = newPackage.GetNode()
|
|
||||||
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, fieldIs, fieldPackageIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageOS, newPackage.OS))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageName, newPackage.Name))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageVersion, newPackage.Version.String()))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageNextVersion, endPackage.Node))
|
|
||||||
|
|
||||||
packageParameter.Node = newPackage.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and insert a start package
|
|
||||||
startPackage := &Package{
|
|
||||||
OS: packageParameter.OS,
|
|
||||||
Name: packageParameter.Name,
|
|
||||||
Version: types.MinVersion,
|
|
||||||
}
|
|
||||||
startPackage.Node = startPackage.GetNode()
|
|
||||||
|
|
||||||
t.AddQuad(cayley.Triple(startPackage.Node, fieldIs, fieldPackageIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(startPackage.Node, FieldPackageOS, startPackage.OS))
|
|
||||||
t.AddQuad(cayley.Triple(startPackage.Node, FieldPackageName, startPackage.Name))
|
|
||||||
t.AddQuad(cayley.Triple(startPackage.Node, FieldPackageVersion, startPackage.Version.String()))
|
|
||||||
if !insertingStartPackage && !insertingEndPackage {
|
|
||||||
t.AddQuad(cayley.Triple(startPackage.Node, FieldPackageNextVersion, newPackage.Node))
|
|
||||||
} else {
|
|
||||||
t.AddQuad(cayley.Triple(startPackage.Node, FieldPackageNextVersion, endPackage.Node))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set package node
|
|
||||||
if insertingEndPackage {
|
|
||||||
packageParameter.Node = endPackage.Node
|
|
||||||
} else if insertingStartPackage {
|
|
||||||
packageParameter.Node = startPackage.Node
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The branch already exists
|
|
||||||
|
|
||||||
// Create the package
|
|
||||||
newPackage := &Package{OS: packageParameter.OS, Name: packageParameter.Name, Version: packageParameter.Version}
|
|
||||||
newPackage.Node = "package:" + utils.Hash(newPackage.Key())
|
|
||||||
packageParameter.Node = newPackage.Node
|
|
||||||
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, fieldIs, fieldPackageIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageOS, newPackage.OS))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageName, newPackage.Name))
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageVersion, newPackage.Version.String()))
|
|
||||||
|
|
||||||
// Sort branchPackages by version (including the new package)
|
|
||||||
branchPackages = append(branchPackages, newPackage)
|
|
||||||
sort.Sort(ByVersion(branchPackages))
|
|
||||||
|
|
||||||
// Find my prec/succ GraphID in the sorted slice now
|
|
||||||
newPackageKey := newPackage.Key()
|
|
||||||
var pred, succ *Package
|
|
||||||
var found bool
|
|
||||||
for _, p := range branchPackages {
|
|
||||||
equal := p.Key() == newPackageKey
|
|
||||||
if !equal && !found {
|
|
||||||
pred = p
|
|
||||||
} else if found {
|
|
||||||
succ = p
|
|
||||||
break
|
|
||||||
} else if equal {
|
|
||||||
found = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pred == nil || succ == nil {
|
|
||||||
log.Warningf("could not find any package predecessor/successor of: [OS: %s, Name: %s, Version: %s].", packageParameter.OS, packageParameter.Name, packageParameter.Version)
|
|
||||||
return cerrors.NewBadRequestError("could not find package predecessor/successor")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link the new packages with the branch
|
|
||||||
t.RemoveQuad(cayley.Triple(pred.Node, FieldPackageNextVersion, succ.Node))
|
|
||||||
|
|
||||||
pred.NextVersionNode = newPackage.Node
|
|
||||||
t.AddQuad(cayley.Triple(pred.Node, FieldPackageNextVersion, newPackage.Node))
|
|
||||||
|
|
||||||
newPackage.NextVersionNode = succ.Node
|
|
||||||
t.AddQuad(cayley.Triple(newPackage.Node, FieldPackageNextVersion, succ.Node))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply transaction
|
|
||||||
if err := store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (InsertPackages): %s", err)
|
|
||||||
return ErrTransaction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindOnePackage finds and returns a single package having the given OS, name and version, selecting the specified fields
|
|
||||||
func FindOnePackage(OS, name string, version types.Version, selectedFields []string) (*Package, error) {
|
|
||||||
packageParameter := Package{OS: OS, Name: name, Version: version}
|
|
||||||
p, err := toPackages(cayley.StartPath(store, packageParameter.GetNode()).Has(fieldIs, fieldPackageIsValue), selectedFields)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(p) == 1 {
|
|
||||||
return p[0], nil
|
|
||||||
}
|
|
||||||
if len(p) > 1 {
|
|
||||||
log.Errorf("found multiple packages with identical data [OS: %s, Name: %s, Version: %s]", OS, name, version)
|
|
||||||
return nil, ErrInconsistent
|
|
||||||
}
|
|
||||||
return nil, cerrors.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAllPackagesByNodes finds and returns all packages given by their nodes, selecting the specified fields
|
|
||||||
func FindAllPackagesByNodes(nodes []string, selectedFields []string) ([]*Package, error) {
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return []*Package{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return toPackages(cayley.StartPath(store, nodes...).Has(fieldIs, fieldPackageIsValue), selectedFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAllPackagesByBranch finds and returns all packages that belong to the given Branch, selecting the specified fields
|
|
||||||
func FindAllPackagesByBranch(OS, name string, selectedFields []string) ([]*Package, error) {
|
|
||||||
return toPackages(cayley.StartPath(store, name).In(FieldPackageName).Has(FieldPackageOS, OS), selectedFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// toPackages converts a path leading to one or multiple packages to Package structs, selecting the specified fields
|
|
||||||
func toPackages(path *path.Path, selectedFields []string) ([]*Package, error) {
|
|
||||||
var packages []*Package
|
|
||||||
var err error
|
|
||||||
|
|
||||||
saveFields(path, selectedFields, []string{FieldPackagePreviousVersion})
|
|
||||||
it, _ := path.BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
pkg := Package{Node: store.NameOf(it.Result())}
|
|
||||||
for _, selectedField := range selectedFields {
|
|
||||||
switch selectedField {
|
|
||||||
case FieldPackageOS:
|
|
||||||
pkg.OS = store.NameOf(tags[FieldPackageOS])
|
|
||||||
case FieldPackageName:
|
|
||||||
pkg.Name = store.NameOf(tags[FieldPackageName])
|
|
||||||
case FieldPackageVersion:
|
|
||||||
pkg.Version, err = types.NewVersion(store.NameOf(tags[FieldPackageVersion]))
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("could not parse version of package %s: %s", pkg.Node, err.Error())
|
|
||||||
}
|
|
||||||
case FieldPackageNextVersion:
|
|
||||||
pkg.NextVersionNode = store.NameOf(tags[FieldPackageNextVersion])
|
|
||||||
case FieldPackagePreviousVersion:
|
|
||||||
pkg.PreviousVersionNode, err = toValue(cayley.StartPath(store, pkg.Node).In(FieldPackageNextVersion))
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("could not get previousVersion on package %s: %s.", pkg.Node, err.Error())
|
|
||||||
return []*Package{}, ErrInconsistent
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("unknown selectedField")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
packages = append(packages, &pkg)
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in toPackages: %s", it.Err())
|
|
||||||
return []*Package{}, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return packages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextVersion find and returns the package of the same branch that has a higher version number, selecting the specified fields
|
|
||||||
// It requires that FieldPackageNextVersion field has been selected on p
|
|
||||||
func (p *Package) NextVersion(selectedFields []string) (*Package, error) {
|
|
||||||
if p.NextVersionNode == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := FindAllPackagesByNodes([]string{p.NextVersionNode}, selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(v) != 1 {
|
|
||||||
log.Errorf("found multiple packages when getting next version of package %s", p.Node)
|
|
||||||
return nil, ErrInconsistent
|
|
||||||
}
|
|
||||||
return v[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextVersions find and returns all the packages of the same branch that have
|
|
||||||
// a higher version number, selecting the specified fields
|
|
||||||
// It requires that FieldPackageNextVersion field has been selected on p
|
|
||||||
// The immediate higher version is listed first, and the special end-of-Branch package is last, p is not listed
|
|
||||||
func (p *Package) NextVersions(selectedFields []string) ([]*Package, error) {
|
|
||||||
var nextVersions []*Package
|
|
||||||
|
|
||||||
if !utils.Contains(FieldPackageNextVersion, selectedFields) {
|
|
||||||
selectedFields = append(selectedFields, FieldPackageNextVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
nextVersion, err := p.NextVersion(selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return []*Package{}, err
|
|
||||||
}
|
|
||||||
if nextVersion != nil {
|
|
||||||
nextVersions = append(nextVersions, nextVersion)
|
|
||||||
|
|
||||||
nextNextVersions, err := nextVersion.NextVersions(selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return []*Package{}, err
|
|
||||||
}
|
|
||||||
nextVersions = append(nextVersions, nextNextVersions...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextVersions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreviousVersion find and returns the package of the same branch that has an
|
|
||||||
// immediate lower version number, selecting the specified fields
|
|
||||||
// It requires that FieldPackagePreviousVersion field has been selected on p
|
|
||||||
func (p *Package) PreviousVersion(selectedFields []string) (*Package, error) {
|
|
||||||
if p.PreviousVersionNode == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := FindAllPackagesByNodes([]string{p.PreviousVersionNode}, selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(v) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if len(v) != 1 {
|
|
||||||
log.Errorf("found multiple packages when getting previous version of package %s", p.Node)
|
|
||||||
return nil, ErrInconsistent
|
|
||||||
}
|
|
||||||
return v[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreviousVersions find and returns all the packages of the same branch that
|
|
||||||
// have a lower version number, selecting the specified fields
|
|
||||||
// It requires that FieldPackageNextVersion field has been selected on p
|
|
||||||
// The immediate lower version is listed first, and the special start-of-Branch
|
|
||||||
// package is last, p is not listed
|
|
||||||
func (p *Package) PreviousVersions(selectedFields []string) ([]*Package, error) {
|
|
||||||
var previousVersions []*Package
|
|
||||||
|
|
||||||
if !utils.Contains(FieldPackagePreviousVersion, selectedFields) {
|
|
||||||
selectedFields = append(selectedFields, FieldPackagePreviousVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
previousVersion, err := p.PreviousVersion(selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return []*Package{}, err
|
|
||||||
}
|
|
||||||
if previousVersion != nil {
|
|
||||||
previousVersions = append(previousVersions, previousVersion)
|
|
||||||
|
|
||||||
previousPreviousVersions, err := previousVersion.PreviousVersions(selectedFields)
|
|
||||||
if err != nil {
|
|
||||||
return []*Package{}, err
|
|
||||||
}
|
|
||||||
previousVersions = append(previousVersions, previousPreviousVersions...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return previousVersions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByVersion implements sort.Interface for []*Package based on the Version field
|
|
||||||
// It uses github.com/quentin-m/dpkgcomp internally and makes use of types.MinVersion/types.MaxVersion
|
|
||||||
type ByVersion []*Package
|
|
||||||
|
|
||||||
func (p ByVersion) Len() int { return len(p) }
|
|
||||||
func (p ByVersion) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
||||||
func (p ByVersion) Less(i, j int) bool { return p[i].Version.Compare(p[j].Version) < 0 }
|
|
@ -1,194 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
"github.com/coreos/clair/utils/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPackage(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
// Try to insert invalid packages
|
|
||||||
for _, invalidPkg := range []*Package{
|
|
||||||
&Package{OS: "", Name: "testpkg1", Version: types.NewVersionUnsafe("1.0")},
|
|
||||||
&Package{OS: "testOS", Name: "", Version: types.NewVersionUnsafe("1.0")},
|
|
||||||
&Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("")},
|
|
||||||
&Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("bad version")},
|
|
||||||
&Package{OS: "", Name: "", Version: types.NewVersionUnsafe("")},
|
|
||||||
} {
|
|
||||||
err := InsertPackages([]*Package{invalidPkg})
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert a package
|
|
||||||
pkg1 := &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
err := InsertPackages([]*Package{pkg1})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
// Find the inserted package and verify its content
|
|
||||||
pkg1b, err := FindOnePackage(pkg1.OS, pkg1.Name, pkg1.Version, FieldPackageAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, pkg1b) {
|
|
||||||
assert.Equal(t, pkg1.Node, pkg1b.Node)
|
|
||||||
assert.Equal(t, pkg1.OS, pkg1b.OS)
|
|
||||||
assert.Equal(t, pkg1.Name, pkg1b.Name)
|
|
||||||
assert.Equal(t, pkg1.Version, pkg1b.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find packages from the inserted branch and verify their content
|
|
||||||
// (the first one should be a start package, the second one the inserted one and the third one the end package)
|
|
||||||
pkgs1c, err := FindAllPackagesByBranch(pkg1.OS, pkg1.Name, FieldPackageAll)
|
|
||||||
if assert.Nil(t, err) && assert.Equal(t, 3, len(pkgs1c)) {
|
|
||||||
sort.Sort(ByVersion(pkgs1c))
|
|
||||||
|
|
||||||
assert.Equal(t, pkg1.OS, pkgs1c[0].OS)
|
|
||||||
assert.Equal(t, pkg1.Name, pkgs1c[0].Name)
|
|
||||||
assert.Equal(t, types.MinVersion, pkgs1c[0].Version)
|
|
||||||
|
|
||||||
assert.Equal(t, pkg1.OS, pkgs1c[1].OS)
|
|
||||||
assert.Equal(t, pkg1.Name, pkgs1c[1].Name)
|
|
||||||
assert.Equal(t, pkg1.Version, pkgs1c[1].Version)
|
|
||||||
|
|
||||||
assert.Equal(t, pkg1.OS, pkgs1c[2].OS)
|
|
||||||
assert.Equal(t, pkg1.Name, pkgs1c[2].Name)
|
|
||||||
assert.Equal(t, types.MaxVersion, pkgs1c[2].Version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert multiple packages in the same branch, one in another branch, insert local duplicates and database duplicates as well
|
|
||||||
pkg2 := []*Package{
|
|
||||||
&Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("0.8")},
|
|
||||||
&Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("0.9")},
|
|
||||||
&Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.0")}, // Already present in the database
|
|
||||||
&Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.1")},
|
|
||||||
&Package{OS: "testOS", Name: "testpkg2", Version: types.NewVersionUnsafe("1.0")}, // Another branch
|
|
||||||
&Package{OS: "testOS", Name: "testpkg2", Version: types.NewVersionUnsafe("1.0")}, // Local duplicates
|
|
||||||
}
|
|
||||||
nbInSameBranch := 4 + 2 // (start/end packages)
|
|
||||||
|
|
||||||
err = InsertPackages(shuffle(pkg2))
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
// Find packages from the inserted branch, verify their order and NextVersion / PreviousVersion
|
|
||||||
pkgs2b, err := FindAllPackagesByBranch("testOS", "testpkg1", FieldPackageAll)
|
|
||||||
if assert.Nil(t, err) && assert.Equal(t, nbInSameBranch, len(pkgs2b)) {
|
|
||||||
sort.Sort(ByVersion(pkgs2b))
|
|
||||||
|
|
||||||
for i := 0; i < nbInSameBranch; i = i + 1 {
|
|
||||||
if i == 0 {
|
|
||||||
assert.Equal(t, types.MinVersion, pkgs2b[0].Version)
|
|
||||||
} else if i < nbInSameBranch-2 {
|
|
||||||
assert.Equal(t, pkg2[i].Version, pkgs2b[i+1].Version)
|
|
||||||
|
|
||||||
nv, err := pkgs2b[i+1].NextVersion(FieldPackageAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, pkgs2b[i+2], nv)
|
|
||||||
|
|
||||||
if i > 0 {
|
|
||||||
pv, err := pkgs2b[i].PreviousVersion(FieldPackageAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, pkgs2b[i-1], pv)
|
|
||||||
} else {
|
|
||||||
pv, err := pkgs2b[i].PreviousVersion(FieldPackageAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Nil(t, pv)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, types.MaxVersion, pkgs2b[nbInSameBranch-1].Version)
|
|
||||||
|
|
||||||
nv, err := pkgs2b[nbInSameBranch-1].NextVersion(FieldPackageAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Nil(t, nv)
|
|
||||||
|
|
||||||
pv, err := pkgs2b[i].PreviousVersion(FieldPackageAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, pkgs2b[i-1], pv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextVersions
|
|
||||||
nv, err := pkgs2b[0].NextVersions(FieldPackageAll)
|
|
||||||
if assert.Nil(t, err) && assert.Len(t, nv, nbInSameBranch-1) {
|
|
||||||
for i := 0; i < nbInSameBranch-1; i = i + 1 {
|
|
||||||
if i < nbInSameBranch-2 {
|
|
||||||
assert.Equal(t, pkg2[i].Version, nv[i].Version)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, types.MaxVersion, nv[i].Version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreviousVersions
|
|
||||||
pv, err := pkgs2b[nbInSameBranch-1].PreviousVersions(FieldPackageAll)
|
|
||||||
if assert.Nil(t, err) && assert.Len(t, pv, nbInSameBranch-1) {
|
|
||||||
for i := 0; i < len(pv); i = i + 1 {
|
|
||||||
assert.Equal(t, pkgs2b[len(pkgs2b)-i-2], pv[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the one we added which was already present in the database has the same node value (meaning that we just fetched it actually)
|
|
||||||
assert.Contains(t, pkg2, pkg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert duplicated latest packages directly, ensure only one is actually inserted. Then insert another package in the branch and ensure that its next version is the latest one
|
|
||||||
pkg3a := &Package{OS: "testOS", Name: "testpkg3", Version: types.MaxVersion}
|
|
||||||
pkg3b := &Package{OS: "testOS", Name: "testpkg3", Version: types.MaxVersion}
|
|
||||||
pkg3c := &Package{OS: "testOS", Name: "testpkg3", Version: types.MaxVersion}
|
|
||||||
err1 := InsertPackages([]*Package{pkg3a, pkg3b})
|
|
||||||
err2 := InsertPackages([]*Package{pkg3c})
|
|
||||||
if assert.Nil(t, err1) && assert.Nil(t, err2) {
|
|
||||||
assert.Equal(t, pkg3a, pkg3b)
|
|
||||||
assert.Equal(t, pkg3b, pkg3c)
|
|
||||||
}
|
|
||||||
pkg4 := Package{OS: "testOS", Name: "testpkg3", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
InsertPackages([]*Package{&pkg4})
|
|
||||||
pkgs34, _ := FindAllPackagesByBranch("testOS", "testpkg3", FieldPackageAll)
|
|
||||||
if assert.Len(t, pkgs34, 3) {
|
|
||||||
sort.Sort(ByVersion(pkgs34))
|
|
||||||
assert.Equal(t, pkg4.Node, pkgs34[1].Node)
|
|
||||||
assert.Equal(t, pkg3a.Node, pkgs34[2].Node)
|
|
||||||
assert.Equal(t, pkg3a.Node, pkgs34[1].NextVersionNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert two identical packages but with "different" versions
|
|
||||||
// The second version should be simplified to the first one
|
|
||||||
// Therefore, we should just have three packages (the inserted one and the start/end packages of the branch)
|
|
||||||
InsertPackages([]*Package{&Package{OS: "testOS", Name: "testdirtypkg", Version: types.NewVersionUnsafe("0.1")}})
|
|
||||||
InsertPackages([]*Package{&Package{OS: "testOS", Name: "testdirtypkg", Version: types.NewVersionUnsafe("0:0.1")}})
|
|
||||||
dirtypkgs, err := FindAllPackagesByBranch("testOS", "testdirtypkg", FieldPackageAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, dirtypkgs, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shuffle(packageParameters []*Package) []*Package {
|
|
||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
|
||||||
|
|
||||||
sPackage := make([]*Package, len(packageParameters))
|
|
||||||
copy(sPackage, packageParameters)
|
|
||||||
|
|
||||||
for i := len(sPackage) - 1; i > 0; i-- {
|
|
||||||
j := rand.Intn(i)
|
|
||||||
sPackage[i], sPackage[j] = sPackage[j], sPackage[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return sPackage
|
|
||||||
}
|
|
127
database/pgsql/feature.go
Normal file
127
database/pgsql/feature.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
"github.com/coreos/clair/utils/types"
|
||||||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) insertFeature(feature database.Feature) (id int, err error) {
|
||||||
|
if feature.Name == "" {
|
||||||
|
return 0, cerrors.NewBadRequestError("could not find/insert invalid Feature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pgSQL.cache != nil {
|
||||||
|
if id, found := pgSQL.cache.Get("feature:" + feature.Name); found {
|
||||||
|
return id.(int), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create Namespace.
|
||||||
|
namespaceID, err := pgSQL.insertNamespace(feature.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create Feature.
|
||||||
|
err = pgSQL.QueryRow(getQuery("soi_feature"), feature.Name, namespaceID).Scan(&id)
|
||||||
|
|
||||||
|
if pgSQL.cache != nil {
|
||||||
|
pgSQL.cache.Add("feature:"+feature.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) (id int, err error) {
|
||||||
|
if featureVersion.Version.String() == "" {
|
||||||
|
return 0, cerrors.NewBadRequestError("could not find/insert invalid FeatureVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pgSQL.cache != nil {
|
||||||
|
if id, found := pgSQL.cache.Get("featureversion:" + featureVersion.Feature.Name + ":" +
|
||||||
|
featureVersion.Version.String()); found {
|
||||||
|
return id.(int), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create Feature first.
|
||||||
|
featureID, err := pgSQL.insertFeature(featureVersion.Feature)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction.
|
||||||
|
tx, err := pgSQL.Begin()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create FeatureVersion.
|
||||||
|
var newOrExisting string
|
||||||
|
err = tx.QueryRow(getQuery("soi_featureversion"), featureID, featureVersion.Version).
|
||||||
|
Scan(&newOrExisting, &featureVersion.ID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
if newOrExisting == "exi" {
|
||||||
|
// That featureVersion already exists, return its id.
|
||||||
|
return featureVersion.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link the new FeatureVersion with every vulnerabilities that affect it, by inserting in
|
||||||
|
// Vulnerability_Affects_FeatureVersion.
|
||||||
|
|
||||||
|
// Lock Vulnerability_FixedIn_Feature because we can't let it to be modified while we modify
|
||||||
|
// Vulnerability_Affects_FeatureVersion.
|
||||||
|
_, err = tx.Exec(getQuery("l_share_vulnerability_fixedin_feature"))
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select every vulnerability and the fixed version that affect this Feature.
|
||||||
|
rows, err := tx.Query(getQuery("s_vulnerability_fixedin_feature"), featureID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var fixedInID, vulnerabilityID int
|
||||||
|
var fixedInVersion types.Version
|
||||||
|
for rows.Next() {
|
||||||
|
err := rows.Scan(&fixedInID, &vulnerabilityID, &fixedInVersion)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if featureVersion.Version.Compare(fixedInVersion) < 0 {
|
||||||
|
// The version of the FeatureVersion we are inserting is lower than the fixed version on this
|
||||||
|
// Vulnerability, thus, this FeatureVersion is affected by it.
|
||||||
|
_, err := tx.Exec(getQuery("i_vulnerability_affects_featureversion"), vulnerabilityID,
|
||||||
|
featureVersion.ID, fixedInID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction.
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pgSQL.cache != nil {
|
||||||
|
pgSQL.cache.Add("featureversion:"+featureVersion.Feature.Name+":"+
|
||||||
|
featureVersion.Version.String(), featureVersion.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
88
database/pgsql/feature_test.go
Normal file
88
database/pgsql/feature_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
"github.com/coreos/clair/utils/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInsertFeature(t *testing.T) {
|
||||||
|
datastore, err := OpenForTest("InsertFeature", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer datastore.Close()
|
||||||
|
|
||||||
|
// Invalid Feature.
|
||||||
|
id0, err := datastore.insertFeature(database.Feature{})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Zero(t, id0)
|
||||||
|
|
||||||
|
id0, err = datastore.insertFeature(database.Feature{
|
||||||
|
Namespace: database.Namespace{},
|
||||||
|
Name: "TestInsertFeature0",
|
||||||
|
})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Zero(t, id0)
|
||||||
|
|
||||||
|
// Insert Feature and ensure we can find it.
|
||||||
|
feature := database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace1"},
|
||||||
|
Name: "TestInsertFeature1",
|
||||||
|
}
|
||||||
|
id1, err := datastore.insertFeature(feature)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
id2, err := datastore.insertFeature(feature)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, id1, id2)
|
||||||
|
|
||||||
|
// Insert invalid FeatureVersion.
|
||||||
|
for _, invalidFeatureVersion := range []database.FeatureVersion{
|
||||||
|
database.FeatureVersion{
|
||||||
|
Feature: database.Feature{},
|
||||||
|
Version: types.NewVersionUnsafe("1.0"),
|
||||||
|
},
|
||||||
|
database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{},
|
||||||
|
Name: "TestInsertFeature2",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("1.0"),
|
||||||
|
},
|
||||||
|
database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace2"},
|
||||||
|
Name: "TestInsertFeature2",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe(""),
|
||||||
|
},
|
||||||
|
database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace2"},
|
||||||
|
Name: "TestInsertFeature2",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("bad version"),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
id3, err := datastore.insertFeatureVersion(invalidFeatureVersion)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Zero(t, id3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert FeatureVersion and ensure we can find it.
|
||||||
|
featureVersion := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace1"},
|
||||||
|
Name: "TestInsertFeature1",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("2:3.0-imba"),
|
||||||
|
}
|
||||||
|
id4, err := datastore.insertFeatureVersion(featureVersion)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
id5, err := datastore.insertFeatureVersion(featureVersion)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, id4, id5)
|
||||||
|
}
|
58
database/pgsql/keyvalue.go
Normal file
58
database/pgsql/keyvalue.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InsertKeyValue stores (or updates) a single key / value tuple.
|
||||||
|
func (pgSQL *pgSQL) InsertKeyValue(key, value string) (err error) {
|
||||||
|
if key == "" || value == "" {
|
||||||
|
log.Warning("could not insert a flag which has an empty name or value")
|
||||||
|
return cerrors.NewBadRequestError("could not insert a flag which has an empty name or value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert.
|
||||||
|
//
|
||||||
|
// Note: UPSERT works only on >= PostgreSQL 9.5 which is not yet supported by AWS RDS.
|
||||||
|
// The best solution is currently the use of http://dba.stackexchange.com/a/13477
|
||||||
|
// but the key/value storage doesn't need to be super-efficient and super-safe at the
|
||||||
|
// moment so we can just use a client-side solution with transactions, based on
|
||||||
|
// http://postgresql.org/docs/current/static/plpgsql-control-structures.html.
|
||||||
|
// TODO(Quentin-M): Enable Upsert as soon as 9.5 is stable.
|
||||||
|
|
||||||
|
for {
|
||||||
|
// First, try to update.
|
||||||
|
r, err := pgSQL.Exec(getQuery("u_keyvalue"), value, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n, _ := r.RowsAffected(); n > 0 {
|
||||||
|
// Updated successfully.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to insert the key.
|
||||||
|
// If someone else inserts the same key concurrently, we could get a unique-key violation error.
|
||||||
|
_, err = pgSQL.Exec(getQuery("i_keyvalue"), key, value)
|
||||||
|
if err != nil {
|
||||||
|
if isErrUniqueViolation(err) {
|
||||||
|
// Got unique constraint violation, retry.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue reads a single key / value tuple and returns an empty string if the key doesn't exist.
|
||||||
|
func (pgSQL *pgSQL) GetKeyValue(key string) (value string, err error) {
|
||||||
|
err = pgSQL.QueryRow(getQuery("s_keyvalue"), key).Scan(&value)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
38
database/pgsql/keyvalue_test.go
Normal file
38
database/pgsql/keyvalue_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeyValue(t *testing.T) {
|
||||||
|
datastore, err := OpenForTest("KeyValue", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer datastore.Close()
|
||||||
|
|
||||||
|
// Get non-existing key/value
|
||||||
|
f, err := datastore.GetKeyValue("test")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Empty(t, "", f)
|
||||||
|
|
||||||
|
// Try to insert invalid key/value.
|
||||||
|
assert.Error(t, datastore.InsertKeyValue("test", ""))
|
||||||
|
assert.Error(t, datastore.InsertKeyValue("", "test"))
|
||||||
|
assert.Error(t, datastore.InsertKeyValue("", ""))
|
||||||
|
|
||||||
|
// Insert and verify.
|
||||||
|
assert.Nil(t, datastore.InsertKeyValue("test", "test1"))
|
||||||
|
f, err = datastore.GetKeyValue("test")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "test1", f)
|
||||||
|
|
||||||
|
// Update and verify.
|
||||||
|
assert.Nil(t, datastore.InsertKeyValue("test", "test2"))
|
||||||
|
f, err = datastore.GetKeyValue("test")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "test2", f)
|
||||||
|
}
|
283
database/pgsql/layer.go
Normal file
283
database/pgsql/layer.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||||||
|
"github.com/guregu/null/zero"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities bool) (database.Layer, error) {
|
||||||
|
// Find the layer
|
||||||
|
var layer database.Layer
|
||||||
|
var parentName sql.NullString
|
||||||
|
var namespaceName sql.NullString
|
||||||
|
|
||||||
|
err := pgSQL.QueryRow(getQuery("s_layer"), name).
|
||||||
|
Scan(&layer.ID, &layer.Name, &layer.EngineVersion, &parentName, &namespaceName)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return layer, cerrors.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return layer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentName.Valid {
|
||||||
|
layer.Parent = &database.Layer{Name: parentName.String}
|
||||||
|
}
|
||||||
|
if namespaceName.Valid {
|
||||||
|
layer.Namespace = &database.Namespace{Name: namespaceName.String}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find its features
|
||||||
|
if withFeatures || withVulnerabilities {
|
||||||
|
featureVersions, err := pgSQL.getLayerFeatureVersions(layer.ID, !withFeatures)
|
||||||
|
if err != nil {
|
||||||
|
return layer, err
|
||||||
|
}
|
||||||
|
layer.Features = featureVersions
|
||||||
|
|
||||||
|
if withVulnerabilities {
|
||||||
|
// Load the vulnerabilities that affect the FeatureVersions.
|
||||||
|
err := pgSQL.loadAffectedBy(layer.Features)
|
||||||
|
if err != nil {
|
||||||
|
return layer, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLayerFeatureVersions returns list of database.FeatureVersion that a database.Layer has.
|
||||||
|
// if idOnly is specified, the returned structs will only have their ID filled. Otherwise,
|
||||||
|
// it also gets their versions, feature's names, feature's namespace's names.
|
||||||
|
func (pgSQL *pgSQL) getLayerFeatureVersions(layerID int, idOnly bool) ([]database.FeatureVersion, error) {
|
||||||
|
var featureVersions []database.FeatureVersion
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
var query string
|
||||||
|
if idOnly {
|
||||||
|
query = getQuery("s_layer_featureversion_id_only")
|
||||||
|
} else {
|
||||||
|
query = getQuery("s_layer_featureversion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
rows, err := pgSQL.Query(query, layerID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return featureVersions, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Scan query
|
||||||
|
var modification string
|
||||||
|
mapFeatureVersions := make(map[int]database.FeatureVersion)
|
||||||
|
for rows.Next() {
|
||||||
|
var featureVersion database.FeatureVersion
|
||||||
|
|
||||||
|
if idOnly {
|
||||||
|
err = rows.Scan(&featureVersion.ID, &modification)
|
||||||
|
if err != nil {
|
||||||
|
return featureVersions, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = rows.Scan(&featureVersion.ID, &modification, &featureVersion.Feature.Namespace.ID,
|
||||||
|
&featureVersion.Feature.Namespace.Name, &featureVersion.Feature.ID,
|
||||||
|
&featureVersion.Feature.Name, &featureVersion.ID, &featureVersion.Version)
|
||||||
|
if err != nil {
|
||||||
|
return featureVersions, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do transitive closure
|
||||||
|
switch modification {
|
||||||
|
case "add":
|
||||||
|
mapFeatureVersions[featureVersion.ID] = featureVersion
|
||||||
|
case "del":
|
||||||
|
delete(mapFeatureVersions, featureVersion.ID)
|
||||||
|
default:
|
||||||
|
log.Warningf("unknown Layer_diff_FeatureVersion's modification: %s", modification)
|
||||||
|
return featureVersions, database.ErrInconsistent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return featureVersions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result by converting our map to a slice
|
||||||
|
for _, featureVersion := range mapFeatureVersions {
|
||||||
|
featureVersions = append(featureVersions, featureVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return featureVersions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAffectedBy returns the list of database.Vulnerability that affect the given
|
||||||
|
// FeatureVersion.
|
||||||
|
func (pgSQL *pgSQL) loadAffectedBy(featureVersions []database.FeatureVersion) error {
|
||||||
|
if len(featureVersions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct list of FeatureVersion IDs, we will do a single query
|
||||||
|
featureVersionIDs := make([]int, 0, len(featureVersions))
|
||||||
|
for i := 0; i < len(featureVersions); i++ {
|
||||||
|
featureVersionIDs = append(featureVersionIDs, featureVersions[i].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := pgSQL.Query(getQuery("s_featureversions_vulnerabilities"),
|
||||||
|
buildInputArray(featureVersionIDs))
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
vulnerabilities := make(map[int][]database.Vulnerability, len(featureVersions))
|
||||||
|
var featureversionID int
|
||||||
|
for rows.Next() {
|
||||||
|
var vulnerability database.Vulnerability
|
||||||
|
err := rows.Scan(&featureversionID, &vulnerability.ID, &vulnerability.Name,
|
||||||
|
&vulnerability.Description, &vulnerability.Link, &vulnerability.Severity,
|
||||||
|
&vulnerability.Namespace.Name, &vulnerability.FixedBy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vulnerabilities[featureversionID] = append(vulnerabilities[featureversionID], vulnerability)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign vulnerabilities to every FeatureVersions
|
||||||
|
for i := 0; i < len(featureVersions); i++ {
|
||||||
|
featureVersions[i].AffectedBy = vulnerabilities[featureVersions[i].ID]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertLayer insert a single layer in the database
|
||||||
|
//
|
||||||
|
// The Name and EngineVersion fields are required.
|
||||||
|
// The Parent, Namespace, Features are optional.
|
||||||
|
// However, please note that the Parent field, if provided, is expected to have been retrieved
|
||||||
|
// using FindLayer with its Features.
|
||||||
|
//
|
||||||
|
// The Name must be unique for two different layers.
|
||||||
|
//
|
||||||
|
// If the Layer already exists and the EngineVersion value of the inserted layer is higher than the
|
||||||
|
// stored value, the EngineVersion, the Namespace and the Feature list will be updated.
|
||||||
|
//
|
||||||
|
// Internally, only Feature additions/removals are stored for each layer. If a layer has a parent,
|
||||||
|
// the Feature list will be compared to the parent's Feature list and the difference will be stored.
|
||||||
|
// Note that when the Namespace of a layer differs from its parent, it is expected that several
|
||||||
|
// Feature that were already included a parent will have their Namespace updated as well
|
||||||
|
// (happens when Feature detectors relies on the detected layer Namespace). However, if the listed
|
||||||
|
// Feature has the same Name/Version as its parent, InsertLayer considers that the Feature hasn't
|
||||||
|
// been modified.
|
||||||
|
// TODO(Quentin-M): This behavior should be implemented at the Feature detectors level.
|
||||||
|
func (pgSQL *pgSQL) InsertLayer(layer database.Layer) error {
|
||||||
|
// Verify parameters
|
||||||
|
if layer.Name == "" {
|
||||||
|
log.Warning("could not insert a layer which has an empty Name")
|
||||||
|
return cerrors.NewBadRequestError("could not insert a layer which has an empty Name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a potentially existing layer.
|
||||||
|
existingLayer, err := pgSQL.FindLayer(layer.Name, true, false)
|
||||||
|
if err != nil && err != cerrors.ErrNotFound {
|
||||||
|
return err
|
||||||
|
} else if err == nil {
|
||||||
|
layer.ID = existingLayer.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction.
|
||||||
|
tx, err := pgSQL.Begin()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or insert namespace if provided.
|
||||||
|
var namespaceID zero.Int
|
||||||
|
if layer.Namespace != nil {
|
||||||
|
n, err := pgSQL.insertNamespace(*layer.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
namespaceID = zero.IntFrom(int64(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
if layer.ID == 0 {
|
||||||
|
// Insert a new layer.
|
||||||
|
var parentID zero.Int
|
||||||
|
if layer.Parent != nil {
|
||||||
|
if layer.Parent.ID == 0 {
|
||||||
|
log.Warning("Parent is expected to be retrieved from database when inserting a layer.")
|
||||||
|
return cerrors.NewBadRequestError("Parent is expected to be retrieved from database when inserting a layer.")
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID = zero.IntFrom(int64(layer.Parent.ID))
|
||||||
|
|
||||||
|
// Import the Namespace from the parent is this layer doesn't specify one.
|
||||||
|
if zero.IsNull(namespaceID) {
|
||||||
|
namespaceID = zero.IntFrom(int64(layer.Parent.Namespace.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.QueryRow(getQuery("i_layer"), layer.Name, layer.EngineVersion, parentID, namespaceID).
|
||||||
|
Scan(&layer.ID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if existingLayer.EngineVersion >= layer.EngineVersion {
|
||||||
|
// The layer exists and has an equal or higher engine verison, do nothing.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing layer.
|
||||||
|
_, err = tx.Exec(getQuery("u_layer"), layer.ID, layer.EngineVersion, namespaceID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Layer_diff_FeatureVersion now.
|
||||||
|
updateDiffFeatureVersions(tx, &layer, &existingLayer)
|
||||||
|
|
||||||
|
// Commit transaction.
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDiffFeatureVersions(tx *sql.Tx, layer, existingLayer *database.Layer) {
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
if existingLayer != nil {
|
||||||
|
// We are updating a layer, we need to diff the Features with the existing Layer.
|
||||||
|
|
||||||
|
} else if layer.Parent == nil {
|
||||||
|
// There is no parent, every Features are added.
|
||||||
|
|
||||||
|
} else if layer.Parent != nil {
|
||||||
|
// There is a parent, we need to diff the Features with it.
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) DeleteLayer(name string) error {
|
||||||
|
// TODO
|
||||||
|
return nil
|
||||||
|
}
|
246
database/pgsql/layer_test.go
Normal file
246
database/pgsql/layer_test.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
"github.com/coreos/clair/utils/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindLayer(t *testing.T) {
|
||||||
|
datastore, err := OpenForTest("FindLayer", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer datastore.Close()
|
||||||
|
|
||||||
|
// Layer-0: no parent, no namespace, no feature, no vulnerability
|
||||||
|
layer, err := datastore.FindLayer("layer-0", false, false)
|
||||||
|
if assert.Nil(t, err) && assert.NotNil(t, layer) {
|
||||||
|
assert.Equal(t, "layer-0", layer.Name)
|
||||||
|
assert.Nil(t, layer.Namespace)
|
||||||
|
assert.Nil(t, layer.Parent)
|
||||||
|
assert.Equal(t, 1, layer.EngineVersion)
|
||||||
|
assert.Len(t, layer.Features, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer, err = datastore.FindLayer("layer-0", true, false)
|
||||||
|
if assert.Nil(t, err) && assert.NotNil(t, layer) {
|
||||||
|
assert.Len(t, layer.Features, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer-1: one parent, adds two features, one vulnerability
|
||||||
|
layer, err = datastore.FindLayer("layer-1", false, false)
|
||||||
|
if assert.Nil(t, err) && assert.NotNil(t, layer) {
|
||||||
|
assert.Equal(t, layer.Name, "layer-1")
|
||||||
|
assert.Equal(t, "debian:7", layer.Namespace.Name)
|
||||||
|
if assert.NotNil(t, layer.Parent) {
|
||||||
|
assert.Equal(t, "layer-0", layer.Parent.Name)
|
||||||
|
}
|
||||||
|
assert.Equal(t, 1, layer.EngineVersion)
|
||||||
|
assert.Len(t, layer.Features, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer, err = datastore.FindLayer("layer-1", true, false)
|
||||||
|
if assert.Nil(t, err) && assert.NotNil(t, layer) && assert.Len(t, layer.Features, 2) {
|
||||||
|
for _, featureVersion := range layer.Features {
|
||||||
|
assert.Equal(t, "debian:7", featureVersion.Feature.Namespace.Name)
|
||||||
|
|
||||||
|
switch featureVersion.Feature.Name {
|
||||||
|
case "wechat":
|
||||||
|
assert.Equal(t, types.NewVersionUnsafe("0.5"), featureVersion.Version)
|
||||||
|
case "openssl":
|
||||||
|
assert.Equal(t, types.NewVersionUnsafe("1.0"), featureVersion.Version)
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer, err = datastore.FindLayer("layer-1", true, true)
|
||||||
|
if assert.Nil(t, err) && assert.NotNil(t, layer) && assert.Len(t, layer.Features, 2) {
|
||||||
|
for _, featureVersion := range layer.Features {
|
||||||
|
assert.Equal(t, "debian:7", featureVersion.Feature.Namespace.Name)
|
||||||
|
|
||||||
|
switch featureVersion.Feature.Name {
|
||||||
|
case "wechat":
|
||||||
|
assert.Equal(t, types.NewVersionUnsafe("0.5"), featureVersion.Version)
|
||||||
|
case "openssl":
|
||||||
|
assert.Equal(t, types.NewVersionUnsafe("1.0"), featureVersion.Version)
|
||||||
|
|
||||||
|
if assert.Len(t, featureVersion.AffectedBy, 1) {
|
||||||
|
assert.Equal(t, "debian:7", featureVersion.AffectedBy[0].Namespace.Name)
|
||||||
|
assert.Equal(t, "CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Name)
|
||||||
|
assert.Equal(t, types.High, featureVersion.AffectedBy[0].Severity)
|
||||||
|
assert.Equal(t, "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", featureVersion.AffectedBy[0].Description)
|
||||||
|
assert.Equal(t, "http://google.com/#q=CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Link)
|
||||||
|
assert.Equal(t, types.NewVersionUnsafe("2.0"), featureVersion.AffectedBy[0].FixedBy)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertLayer(t *testing.T) {
|
||||||
|
datastore, err := OpenForTest("InsertLayer", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer datastore.Close()
|
||||||
|
|
||||||
|
// Insert invalid layer.
|
||||||
|
testInsertLayerInvalid(t, datastore)
|
||||||
|
|
||||||
|
// Insert a layer tree.
|
||||||
|
testInsertLayerTree(t, datastore)
|
||||||
|
|
||||||
|
// Update layer.
|
||||||
|
// TODO(Quentin-M)
|
||||||
|
|
||||||
|
// Delete layer.
|
||||||
|
// TODO(Quentin-M)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInsertLayerInvalid(t *testing.T, datastore database.Datastore) {
|
||||||
|
invalidLayers := []database.Layer{
|
||||||
|
database.Layer{},
|
||||||
|
database.Layer{Name: "layer0", Parent: &database.Layer{}},
|
||||||
|
database.Layer{Name: "layer0", Parent: &database.Layer{Name: "UnknownLayer"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, invalidLayer := range invalidLayers {
|
||||||
|
err := datastore.InsertLayer(invalidLayer)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
|
||||||
|
f1 := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"},
|
||||||
|
Name: "TestInsertLayerFeature1",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("1.0"),
|
||||||
|
}
|
||||||
|
f2 := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"},
|
||||||
|
Name: "TestInsertLayerFeature2",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("0.34"),
|
||||||
|
}
|
||||||
|
f3 := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"},
|
||||||
|
Name: "TestInsertLayerFeature3",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("0.56"),
|
||||||
|
}
|
||||||
|
f4 := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"},
|
||||||
|
Name: "TestInsertLayerFeature2",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("0.34"),
|
||||||
|
}
|
||||||
|
f5 := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"},
|
||||||
|
Name: "TestInsertLayerFeature2",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("0.57"),
|
||||||
|
}
|
||||||
|
f6 := database.FeatureVersion{
|
||||||
|
Feature: database.Feature{
|
||||||
|
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"},
|
||||||
|
Name: "TestInsertLayerFeature4",
|
||||||
|
},
|
||||||
|
Version: types.NewVersionUnsafe("0.666"),
|
||||||
|
}
|
||||||
|
|
||||||
|
layers := []database.Layer{
|
||||||
|
database.Layer{
|
||||||
|
Name: "TestInsertLayer1",
|
||||||
|
},
|
||||||
|
database.Layer{
|
||||||
|
Name: "TestInsertLayer2",
|
||||||
|
Parent: &database.Layer{Name: "TestInsertLayer1"},
|
||||||
|
Namespace: &database.Namespace{Name: "TestInsertLayerNamespace1"},
|
||||||
|
},
|
||||||
|
// This layer changes the namespace and adds Features.
|
||||||
|
database.Layer{
|
||||||
|
Name: "TestInsertLayer3",
|
||||||
|
Parent: &database.Layer{Name: "TestInsertLayer2"},
|
||||||
|
Namespace: &database.Namespace{Name: "TestInsertLayerNamespace2"},
|
||||||
|
Features: []database.FeatureVersion{f1, f2, f3},
|
||||||
|
},
|
||||||
|
// This layer covers the case where the last layer doesn't provide any Feature.
|
||||||
|
database.Layer{
|
||||||
|
Name: "TestInsertLayer4a",
|
||||||
|
Parent: &database.Layer{Name: "TestInsertLayer3"},
|
||||||
|
},
|
||||||
|
// This layer covers the case where the last layer provides Features.
|
||||||
|
// It also modifies the Namespace ("upgrade") but keeps some Features not upgraded, their
|
||||||
|
// Namespaces should then remain unchanged.
|
||||||
|
database.Layer{
|
||||||
|
Name: "TestInsertLayer4b",
|
||||||
|
Parent: &database.Layer{Name: "TestInsertLayer3"},
|
||||||
|
Namespace: &database.Namespace{Name: "TestInsertLayerNamespace3"},
|
||||||
|
Features: []database.FeatureVersion{
|
||||||
|
// Deletes TestInsertLayerFeature1.
|
||||||
|
// Keep TestInsertLayerFeature2 (old Namespace should be kept):
|
||||||
|
f4,
|
||||||
|
// Upgrades TestInsertLayerFeature3 (with new Namespace):
|
||||||
|
f5,
|
||||||
|
// Adds TestInsertLayerFeature4:
|
||||||
|
f6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
retrievedLayers := make(map[string]database.Layer)
|
||||||
|
for _, layer := range layers {
|
||||||
|
if layer.Parent != nil {
|
||||||
|
// Retrieve from database its parent and assign.
|
||||||
|
parent := retrievedLayers[layer.Parent.Name]
|
||||||
|
layer.Parent = &parent
|
||||||
|
}
|
||||||
|
|
||||||
|
err = datastore.InsertLayer(layer)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
retrievedLayers[layer.Name], err = datastore.FindLayer(layer.Name, true, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l4a := retrievedLayers["TestInsertLayer4a"]
|
||||||
|
assert.Equal(t, "TestInsertLayerNamespace2", l4a.Namespace.Name)
|
||||||
|
assert.Len(t, l4a.Features, 3)
|
||||||
|
for _, featureVersion := range l4a.Features {
|
||||||
|
if cmpFV(featureVersion, f1) && cmpFV(featureVersion, f2) && cmpFV(featureVersion, f3) {
|
||||||
|
assert.Error(t, fmt.Errorf("TestInsertLayer4a contains an unexpected package: %#v. Should contain %#v and %#v and %#v.", featureVersion, f1, f2, f3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l4b := retrievedLayers["TestInsertLayer4b"]
|
||||||
|
assert.Equal(t, "TestInsertLayerNamespace3", l4a.Namespace.Name)
|
||||||
|
assert.Len(t, l4a.Features, 3)
|
||||||
|
for _, featureVersion := range l4a.Features {
|
||||||
|
if cmpFV(featureVersion, f2) && cmpFV(featureVersion, f5) && cmpFV(featureVersion, f6) {
|
||||||
|
assert.Error(t, fmt.Errorf("TestInsertLayer4a contains an unexpected package: %#v. Should contain %#v and %#v and %#v.", featureVersion, f2, f4, f6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmpFV(a, b database.FeatureVersion) bool {
|
||||||
|
return a.Feature.Name == b.Feature.Name &&
|
||||||
|
a.Feature.Namespace.Name == b.Feature.Namespace.Name &&
|
||||||
|
a.Version.String() == b.Version.String()
|
||||||
|
}
|
127
database/pgsql/migrations/20151222113213_Initial.sql
Normal file
127
database/pgsql/migrations/20151222113213_Initial.sql
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Namespace
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS Namespace (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(128) NULL);
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Layer
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS Layer (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
engineversion SMALLINT NOT NULL,
|
||||||
|
parent_id INT NULL REFERENCES Layer,
|
||||||
|
namespace_id INT NULL REFERENCES Namespace);
|
||||||
|
|
||||||
|
CREATE INDEX ON Layer (parent_id);
|
||||||
|
CREATE INDEX ON Layer (namespace_id);
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Feature
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS Feature (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
namespace_id INT NOT NULL REFERENCES Namespace,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (namespace_id, name));
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table FeatureVersion
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS FeatureVersion (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
feature_id INT NOT NULL REFERENCES Feature,
|
||||||
|
version VARCHAR(128) NOT NULL);
|
||||||
|
|
||||||
|
CREATE INDEX ON FeatureVersion (feature_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Layer_diff_FeatureVersion
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TYPE modification AS ENUM ('add', 'del');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS Layer_diff_FeatureVersion (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
layer_id INT NOT NULL REFERENCES Layer ON DELETE CASCADE,
|
||||||
|
featureversion_id INT NOT NULL REFERENCES FeatureVersion,
|
||||||
|
modification modification NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (layer_id, featureversion_id));
|
||||||
|
|
||||||
|
CREATE INDEX ON Layer_diff_FeatureVersion (layer_id);
|
||||||
|
CREATE INDEX ON Layer_diff_FeatureVersion (featureversion_id);
|
||||||
|
CREATE INDEX ON Layer_diff_FeatureVersion (featureversion_id, layer_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Vulnerability
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TYPE severity AS ENUM ('Unknown', 'Negligible', 'Low', 'Medium', 'High', 'Critical', 'Defcon1');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS Vulnerability (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
namespace_id INT NOT NULL REFERENCES Namespace,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
link VARCHAR(128) NULL,
|
||||||
|
severity severity NULL,
|
||||||
|
|
||||||
|
UNIQUE (namespace_id, name));
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Vulnerability_FixedIn_Feature
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS Vulnerability_FixedIn_Feature (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
vulnerability_id INT NOT NULL REFERENCES Vulnerability ON DELETE CASCADE,
|
||||||
|
feature_id INT NOT NULL REFERENCES Feature,
|
||||||
|
version VARCHAR(128) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (vulnerability_id, feature_id));
|
||||||
|
|
||||||
|
CREATE INDEX ON Vulnerability_FixedIn_Feature (feature_id, vulnerability_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table Vulnerability_Affects_FeatureVersion
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS Vulnerability_Affects_FeatureVersion (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
vulnerability_id INT NOT NULL REFERENCES Vulnerability ON DELETE CASCADE,
|
||||||
|
featureversion_id INT NOT NULL REFERENCES FeatureVersion,
|
||||||
|
fixedin_id INT NOT NULL REFERENCES Vulnerability_FixedIn_Feature,
|
||||||
|
|
||||||
|
UNIQUE (vulnerability_id, featureversion_id));
|
||||||
|
|
||||||
|
CREATE INDEX ON Vulnerability_Affects_FeatureVersion (fixedin_id);
|
||||||
|
CREATE INDEX ON Vulnerability_Affects_FeatureVersion (featureversion_id, vulnerability_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table KeyValue
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS KeyValue (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
key VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
value TEXT);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS Namespace,
|
||||||
|
Layer,
|
||||||
|
Feature,
|
||||||
|
FeatureVersion,
|
||||||
|
Layer_diff_FeatureVersion,
|
||||||
|
Vulnerability,
|
||||||
|
Vulnerability_FixedIn_Feature,
|
||||||
|
Vulnerability_Affects_FeatureVersion,
|
||||||
|
KeyValue
|
||||||
|
CASCADE;
|
26
database/pgsql/namespace.go
Normal file
26
database/pgsql/namespace.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) insertNamespace(namespace database.Namespace) (id int, err error) {
|
||||||
|
if namespace.Name == "" {
|
||||||
|
return 0, cerrors.NewBadRequestError("could not find/insert invalid Namespace")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pgSQL.cache != nil {
|
||||||
|
if id, found := pgSQL.cache.Get("namespace:" + namespace.Name); found {
|
||||||
|
return id.(int), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pgSQL.QueryRow(getQuery("soi_namespace"), namespace.Name).Scan(&id)
|
||||||
|
|
||||||
|
if pgSQL.cache != nil {
|
||||||
|
pgSQL.cache.Add("namespace:"+namespace.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
29
database/pgsql/namespace_test.go
Normal file
29
database/pgsql/namespace_test.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInsertNamespace(t *testing.T) {
|
||||||
|
datastore, err := OpenForTest("InsertNamespace", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer datastore.Close()
|
||||||
|
|
||||||
|
// Invalid Namespace.
|
||||||
|
id0, err := datastore.insertNamespace(database.Namespace{})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Zero(t, id0)
|
||||||
|
|
||||||
|
// Insert Namespace and ensure we can find it.
|
||||||
|
id1, err := datastore.insertNamespace(database.Namespace{Name: "TestInsertNamespace1"})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
id2, err := datastore.insertNamespace(database.Namespace{Name: "TestInsertNamespace1"})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, id1, id2)
|
||||||
|
}
|
190
database/pgsql/pgsql.go
Normal file
190
database/pgsql/pgsql.go
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"bitbucket.org/liamstask/goose/lib/goose"
|
||||||
|
"github.com/coreos/clair/config"
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
"github.com/coreos/pkg/capnslog"
|
||||||
|
"github.com/hashicorp/golang-lru"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "pgsql")
|
||||||
|
|
||||||
|
type pgSQL struct {
|
||||||
|
*sql.DB
|
||||||
|
cache *lru.ARCCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQL) Close() {
|
||||||
|
pgSQL.DB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open creates a Datastore backed by a PostgreSQL database.
|
||||||
|
//
|
||||||
|
// It will run immediately every necessary migration on the database.
|
||||||
|
func Open(config *config.DatabaseConfig) (database.Datastore, error) {
|
||||||
|
// Run migrations.
|
||||||
|
if err := Migrate(config.Source); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not run database migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database.
|
||||||
|
db, err := sql.Open("postgres", config.Source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open database (Open): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize cache.
|
||||||
|
// TODO(Quentin-M): Benchmark with a simple LRU Cache.
|
||||||
|
var cache *lru.ARCCache
|
||||||
|
if config.CacheSize > 0 {
|
||||||
|
cache, _ = lru.NewARC(config.CacheSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pgSQL{DB: db, cache: cache}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate runs all available migrations on a pgSQL database.
|
||||||
|
func Migrate(dataSource string) error {
|
||||||
|
log.Info("running database migrations")
|
||||||
|
|
||||||
|
_, filename, _, _ := runtime.Caller(1)
|
||||||
|
migrationDir := path.Join(path.Dir(filename), "/migrations/")
|
||||||
|
conf := &goose.DBConf{
|
||||||
|
MigrationsDir: migrationDir,
|
||||||
|
Driver: goose.DBDriver{
|
||||||
|
Name: "postgres",
|
||||||
|
OpenStr: dataSource,
|
||||||
|
Import: "github.com/lib/pq",
|
||||||
|
Dialect: &goose.PostgresDialect{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the most recent revision available from the migrations folder.
|
||||||
|
target, err := goose.GetMostRecentDBVersion(conf.MigrationsDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
err = goose.RunMigrations(conf, conf.MigrationsDir, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("database migration ran successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDatabase creates a new database.
|
||||||
|
// The dataSource parameter should not contain a dbname.
|
||||||
|
func CreateDatabase(dataSource, databaseName string) error {
|
||||||
|
// Open database.
|
||||||
|
db, err := sql.Open("postgres", dataSource)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open database (CreateDatabase): %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create database.
|
||||||
|
_, err = db.Exec("CREATE DATABASE " + databaseName + ";")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropDatabase drops an existing database.
|
||||||
|
// The dataSource parameter should not contain a dbname.
|
||||||
|
func DropDatabase(dataSource, databaseName string) error {
|
||||||
|
// Open database.
|
||||||
|
db, err := sql.Open("postgres", dataSource)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open database (DropDatabase): %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Drop database.
|
||||||
|
_, err = db.Exec("DROP DATABASE " + databaseName + ";")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pgSQLTest wraps pgSQL for testing purposes.
|
||||||
|
// Its Close() method drops the database.
|
||||||
|
type pgSQLTest struct {
|
||||||
|
*pgSQL
|
||||||
|
dataSource string
|
||||||
|
dbName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pgSQL *pgSQLTest) Close() {
|
||||||
|
pgSQL.DB.Close()
|
||||||
|
DropDatabase(pgSQL.dataSource+"dbname=postgres", pgSQL.dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenForTest creates a test Datastore backed by a new PostgreSQL database.
|
||||||
|
// It creates a new unique and prefixed ("test_") database.
|
||||||
|
// Using Close() will drop the database.
|
||||||
|
func OpenForTest(name string, withTestData bool) (*pgSQLTest, error) {
|
||||||
|
dataSource := "host=127.0.0.1 sslmode=disable "
|
||||||
|
dbName := "test_" + strings.ToLower(name) + "_" + strings.Replace(uuid.New(), "-", "_", -1)
|
||||||
|
|
||||||
|
// Create database.
|
||||||
|
err := CreateDatabase(dataSource+"dbname=postgres", dbName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database.
|
||||||
|
db, err := Open(&config.DatabaseConfig{Source: dataSource + "dbname=" + dbName, CacheSize: 0})
|
||||||
|
if err != nil {
|
||||||
|
DropDatabase(dataSource, dbName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load test data if specified.
|
||||||
|
if withTestData {
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
d, _ := ioutil.ReadFile(path.Join(path.Dir(filename)) + "/testdata/data.sql")
|
||||||
|
_, err = db.(*pgSQL).Exec(string(d))
|
||||||
|
if err != nil {
|
||||||
|
DropDatabase(dataSource, dbName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pgSQLTest{pgSQL: db.(*pgSQL), dataSource: dataSource, dbName: dbName}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildInputArray constructs a PostgreSQL input array from the specified integers.
|
||||||
|
// Useful to use the `= ANY($1::integer[])` syntax that let us use a IN clause while using
|
||||||
|
// a single placeholder.
|
||||||
|
func buildInputArray(ints []int) string {
|
||||||
|
str := "{"
|
||||||
|
for i := 0; i < len(ints)-1; i++ {
|
||||||
|
str = str + strconv.Itoa(ints[i]) + ","
|
||||||
|
}
|
||||||
|
str = str + strconv.Itoa(ints[len(ints)-1]) + "}"
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// isErrUniqueViolation determines is the given error is a unique contraint violation.
|
||||||
|
func isErrUniqueViolation(err error) bool {
|
||||||
|
pqErr, ok := err.(*pq.Error)
|
||||||
|
return ok && pqErr.Code == "23505"
|
||||||
|
}
|
123
database/pgsql/queries.go
Normal file
123
database/pgsql/queries.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package pgsql
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var queries map[string]string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
queries = make(map[string]string)
|
||||||
|
|
||||||
|
// keyvalue.go
|
||||||
|
queries["u_keyvalue"] = `UPDATE KeyValue SET value = $1 WHERE key = $2`
|
||||||
|
queries["i_keyvalue"] = `INSERT INTO KeyValue(key, value) VALUES($1, $2)`
|
||||||
|
queries["s_keyvalue"] = `SELECT value FROM KeyValue WHERE key = $1`
|
||||||
|
|
||||||
|
// namespace.go
|
||||||
|
queries["soi_namespace"] = `
|
||||||
|
WITH new_namespace AS (
|
||||||
|
INSERT INTO Namespace(name)
|
||||||
|
SELECT CAST($1 AS VARCHAR)
|
||||||
|
WHERE NOT EXISTS (SELECT name FROM Namespace WHERE name = $1)
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
SELECT id FROM Namespace WHERE name = $1
|
||||||
|
UNION
|
||||||
|
SELECT id FROM new_namespace`
|
||||||
|
|
||||||
|
// feature.go
|
||||||
|
queries["soi_feature"] = `
|
||||||
|
WITH new_feature AS (
|
||||||
|
INSERT INTO Feature(name, namespace_id)
|
||||||
|
SELECT CAST($1 AS VARCHAR), CAST($2 AS VARCHAR)
|
||||||
|
WHERE NOT EXISTS (SELECT id FROM Feature WHERE name = $1 AND namespace_id = $2)
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
SELECT id FROM Feature WHERE name = $1 AND namespace_id = $2
|
||||||
|
UNION
|
||||||
|
SELECT id FROM new_feature`
|
||||||
|
|
||||||
|
queries["l_share_vulnerability_fixedin_feature"] = `LOCK Vulnerability_FixedIn_Feature IN SHARE MODE`
|
||||||
|
|
||||||
|
queries["soi_featureversion"] = `
|
||||||
|
WITH new_featureversion AS (
|
||||||
|
INSERT INTO FeatureVersion(feature_id, version)
|
||||||
|
SELECT CAST($1 AS VARCHAR), CAST($2 AS VARCHAR)
|
||||||
|
WHERE NOT EXISTS (SELECT id FROM Feature WHERE feature_id = $1 AND version = $2)
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
SELECT 'exi', id FROM Feature WHERE feature_id = $1 AND version = $2
|
||||||
|
UNION
|
||||||
|
SELECT 'new', id FROM new_featureversion
|
||||||
|
`
|
||||||
|
|
||||||
|
queries["s_vulnerability_fixedin_feature"] = `
|
||||||
|
SELECT id, vulnerability_id, version FROM Vulnerability_FixedIn_Feature
|
||||||
|
WHERE feature_id = ?`
|
||||||
|
|
||||||
|
queries["i_vulnerability_affects_featureversion"] = `
|
||||||
|
INSERT INTO Vulnerability_Affects_FeatureVersion(vulnerability_id,
|
||||||
|
featureversion_id, fixedin_id) VALUES($1, $2, $3)`
|
||||||
|
|
||||||
|
// layer.go
|
||||||
|
queries["s_layer"] = `
|
||||||
|
SELECT l.id, l.name, l.engineversion, p.name, n.name
|
||||||
|
FROM Layer l
|
||||||
|
LEFT JOIN Layer p ON l.parent_id = p.id
|
||||||
|
LEFT JOIN Namespace n ON l.namespace_id = n.id
|
||||||
|
WHERE l.name = $1;`
|
||||||
|
|
||||||
|
queries["s_layer_featureversion_id_only"] = `
|
||||||
|
WITH RECURSIVE layer_tree(id, parent_id, depth, path, cycle) AS(
|
||||||
|
SELECT l.id, l.parent_id, 1, ARRAY[l.id], false
|
||||||
|
FROM Layer l
|
||||||
|
WHERE l.id = $1
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.id, l.parent_id, lt.depth + 1, path || l.id, l.id = ANY(path)
|
||||||
|
FROM Layer l, layer_tree lt
|
||||||
|
WHERE l.id = lt.parent_id
|
||||||
|
)
|
||||||
|
SELECT ldf.featureversion_id, ldf.modification
|
||||||
|
FROM Layer_diff_FeatureVersion ldf
|
||||||
|
JOIN (
|
||||||
|
SELECT row_number() over (ORDER BY depth DESC), id FROM layer_tree
|
||||||
|
) AS ltree (ordering, id) ON ldf.layer_id = ltree.id
|
||||||
|
ORDER BY ltree.ordering`
|
||||||
|
|
||||||
|
queries["s_layer_featureversion"] = `
|
||||||
|
WITH RECURSIVE layer_tree(id, parent_id, depth, path, cycle) AS(
|
||||||
|
SELECT l.id, l.parent_id, 1, ARRAY[l.id], false
|
||||||
|
FROM Layer l
|
||||||
|
WHERE l.id = $1
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.id, l.parent_id, lt.depth + 1, path || l.id, l.id = ANY(path)
|
||||||
|
FROM Layer l, layer_tree lt
|
||||||
|
WHERE l.id = lt.parent_id
|
||||||
|
)
|
||||||
|
SELECT ldf.featureversion_id, ldf.modification, fn.id, fn.name, f.id, f.name, fv.id, fv.version
|
||||||
|
FROM Layer_diff_FeatureVersion ldf
|
||||||
|
JOIN (
|
||||||
|
SELECT row_number() over (ORDER BY depth DESC), id FROM layer_tree
|
||||||
|
) AS ltree (ordering, id) ON ldf.layer_id = ltree.id, FeatureVersion fv, Feature f, Namespace fn
|
||||||
|
WHERE ldf.featureversion_id = fv.id AND fv.feature_id = f.id AND f.namespace_id = fn.id
|
||||||
|
ORDER BY ltree.ordering`
|
||||||
|
|
||||||
|
queries["s_featureversions_vulnerabilities"] = `
|
||||||
|
SELECT vafv.featureversion_id, v.id, v.name, v.description, v.link, v.severity, vn.name, vfif.version
|
||||||
|
FROM Vulnerability_Affects_FeatureVersion vafv, Vulnerability v,
|
||||||
|
Namespace vn, Vulnerability_FixedIn_Feature vfif
|
||||||
|
WHERE vafv.featureversion_id = ANY($1::integer[])
|
||||||
|
AND vafv.vulnerability_id = v.id
|
||||||
|
AND vafv.fixedin_id = vfif.id
|
||||||
|
AND v.namespace_id = vn.id`
|
||||||
|
|
||||||
|
queries["i_layer"] = `INSERT INTO Layer(name, engine_version, parent_id, namespace_id) VALUES($1, $2, $3, $4) RETURNING id`
|
||||||
|
|
||||||
|
queries["u_layer"] = `UPDATE LAYER SET engine_version = $2, namespace_id = $3) WHERE id = $1`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQuery(name string) string {
|
||||||
|
if query, ok := queries[name]; ok {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("pgsql: unknown query %v", name))
|
||||||
|
}
|
37
database/pgsql/testdata/data.sql
vendored
Normal file
37
database/pgsql/testdata/data.sql
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
INSERT INTO namespace (id, name) VALUES (1, 'debian:7');
|
||||||
|
INSERT INTO namespace (id, name) VALUES (2, 'debian:8');
|
||||||
|
|
||||||
|
INSERT INTO feature (id, namespace_id, name) VALUES (1, 1, 'wechat');
|
||||||
|
INSERT INTO feature (id, namespace_id, name) VALUES (2, 1, 'openssl');
|
||||||
|
INSERT INTO feature (id, namespace_id, name) VALUES (3, 2, 'openssl');
|
||||||
|
INSERT INTO featureversion (id, feature_id, version) VALUES (1, 1, '0.5');
|
||||||
|
INSERT INTO featureversion (id, feature_id, version) VALUES (2, 2, '1.0');
|
||||||
|
INSERT INTO featureversion (id, feature_id, version) VALUES (3, 2, '2.0');
|
||||||
|
INSERT INTO featureversion (id, feature_id, version) VALUES (4, 3, '1.0');
|
||||||
|
|
||||||
|
INSERT INTO layer (id, name, engineversion, parent_id, namespace_id) VALUES (1, 'layer-0', 1, NULL, NULL);
|
||||||
|
INSERT INTO layer (id, name, engineversion, parent_id, namespace_id) VALUES (2, 'layer-1', 1, 1, 1);
|
||||||
|
INSERT INTO layer (id, name, engineversion, parent_id, namespace_id) VALUES (3, 'layer-2', 1, 2, 1);
|
||||||
|
INSERT INTO layer (id, name, engineversion, parent_id, namespace_id) VALUES (4, 'layer-3a', 1, 3, 1);
|
||||||
|
INSERT INTO layer (id, name, engineversion, parent_id, namespace_id) VALUES (5, 'layer-3b', 1, 3, 2);
|
||||||
|
INSERT INTO layer_diff_featureversion (id, layer_id, featureversion_id, modification) VALUES (1, 2, 1, 'add');
|
||||||
|
INSERT INTO layer_diff_featureversion (id, layer_id, featureversion_id, modification) VALUES (2, 2, 2, 'add');
|
||||||
|
INSERT INTO layer_diff_featureversion (id, layer_id, featureversion_id, modification) VALUES (3, 3, 2, 'del'); -- layer-2: Update Debian:7 OpenSSL 1.0 -> 2.0
|
||||||
|
INSERT INTO layer_diff_featureversion (id, layer_id, featureversion_id, modification) VALUES (4, 3, 3, 'add'); -- ^
|
||||||
|
INSERT INTO layer_diff_featureversion (id, layer_id, featureversion_id, modification) VALUES (5, 5, 3, 'del'); -- layer-3b: Delete Debian:7 OpenSSL 2.0
|
||||||
|
INSERT INTO layer_diff_featureversion (id, layer_id, featureversion_id, modification) VALUES (6, 5, 4, 'add'); -- layer-3b: Add Debian:8 OpenSSL 1.0
|
||||||
|
|
||||||
|
INSERT INTO vulnerability (id, namespace_id, name, description, link, severity) VALUES (1, 1, 'CVE-OPENSSL-1-DEB7', 'A vulnerability affecting OpenSSL < 2.0 on Debian 7.0', 'http://google.com/#q=CVE-OPENSSL-1-DEB7', 'High');
|
||||||
|
INSERT INTO vulnerability_fixedin_feature (id, vulnerability_id, feature_id, version) VALUES (1, 1, 2, '2.0');
|
||||||
|
INSERT INTO vulnerability_affects_featureversion (id, vulnerability_id, featureversion_id, fixedin_id) VALUES (1, 1, 2, 1); -- CVE-OPENSSL-1-DEB7 affects Debian:7 OpenSSL 1.0
|
||||||
|
INSERT INTO vulnerability (id, namespace_id, name, description, link, severity) VALUES (2, 1, 'CVE-NOPE', 'A vulnerability affecting nothing', 'http://google.com/#q=NOPE', 'Negligible');
|
||||||
|
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('namespace', 'id'), (SELECT MAX(id) FROM namespace)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('feature', 'id'), (SELECT MAX(id) FROM feature)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('featureversion', 'id'), (SELECT MAX(id) FROM featureversion)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('layer', 'id'), (SELECT MAX(id) FROM layer)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('layer_diff_featureversion', 'id'), (SELECT MAX(id) FROM layer_diff_featureversion)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('vulnerability', 'id'), (SELECT MAX(id) FROM vulnerability)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('vulnerability_fixedin_feature', 'id'), (SELECT MAX(id) FROM vulnerability_fixedin_feature)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('vulnerability_affects_featureversion', 'id'), (SELECT MAX(id) FROM vulnerability_affects_featureversion)+1);
|
||||||
|
SELECT pg_catalog.setval(pg_get_serial_sequence('vulnerability', 'id'), (SELECT MAX(id) FROM vulnerability)+1);
|
@ -1,51 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
|
|
||||||
// FindAllLayersIntroducingVulnerability finds and returns the list of layers
|
|
||||||
// that introduce the given vulnerability (by its ID), selecting the specified fields
|
|
||||||
func FindAllLayersIntroducingVulnerability(vulnerabilityID string, selectedFields []string) ([]*Layer, error) {
|
|
||||||
// Find vulnerability
|
|
||||||
vulnerability, err := FindOneVulnerability(vulnerabilityID, []string{FieldVulnerabilityFixedIn})
|
|
||||||
if err != nil {
|
|
||||||
return []*Layer{}, err
|
|
||||||
}
|
|
||||||
if vulnerability == nil {
|
|
||||||
return []*Layer{}, cerrors.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find FixedIn packages
|
|
||||||
fixedInPackages, err := FindAllPackagesByNodes(vulnerability.FixedInNodes, []string{FieldPackagePreviousVersion})
|
|
||||||
if err != nil {
|
|
||||||
return []*Layer{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all FixedIn packages's ancestors packages (which are therefore vulnerable to the vulnerability)
|
|
||||||
var vulnerablePackagesNodes []string
|
|
||||||
for _, pkg := range fixedInPackages {
|
|
||||||
previousVersions, err := pkg.PreviousVersions([]string{})
|
|
||||||
if err != nil {
|
|
||||||
return []*Layer{}, err
|
|
||||||
}
|
|
||||||
for _, version := range previousVersions {
|
|
||||||
vulnerablePackagesNodes = append(vulnerablePackagesNodes, version.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all the layers that add these packages
|
|
||||||
return FindAllLayersByAddedPackageNodes(vulnerablePackagesNodes, selectedFields)
|
|
||||||
}
|
|
@ -1,377 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/coreos/clair/utils"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/coreos/clair/utils/types"
|
|
||||||
"github.com/google/cayley"
|
|
||||||
"github.com/google/cayley/graph"
|
|
||||||
"github.com/google/cayley/graph/path"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FieldVulnerabilityID = "id"
|
|
||||||
FieldVulnerabilityLink = "link"
|
|
||||||
FieldVulnerabilityPriority = "priority"
|
|
||||||
FieldVulnerabilityDescription = "description"
|
|
||||||
FieldVulnerabilityFixedIn = "fixedIn"
|
|
||||||
// FieldVulnerabilityCausedByPackage only makes sense with FindAllVulnerabilitiesByFixedIn.
|
|
||||||
FieldVulnerabilityCausedByPackage = "causedByPackage"
|
|
||||||
|
|
||||||
// This field is not selectable and is for internal use only.
|
|
||||||
fieldVulnerabilityIsValue = "vulnerability"
|
|
||||||
)
|
|
||||||
|
|
||||||
var FieldVulnerabilityAll = []string{FieldVulnerabilityID, FieldVulnerabilityLink, FieldVulnerabilityPriority, FieldVulnerabilityDescription, FieldVulnerabilityFixedIn}
|
|
||||||
|
|
||||||
// Vulnerability represents a vulnerability that is fixed in some Packages
|
|
||||||
type Vulnerability struct {
|
|
||||||
Node string `json:"-"`
|
|
||||||
ID string
|
|
||||||
Link string
|
|
||||||
Priority types.Priority
|
|
||||||
Description string `json:",omitempty"`
|
|
||||||
FixedInNodes []string `json:"-"`
|
|
||||||
|
|
||||||
CausedByPackage string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode returns an unique identifier for the graph node
|
|
||||||
// Requires the key field: ID
|
|
||||||
func (v *Vulnerability) GetNode() string {
|
|
||||||
return fieldVulnerabilityIsValue + ":" + utils.Hash(v.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToAbstractVulnerability converts a Vulnerability into an
|
|
||||||
// AbstractVulnerability.
|
|
||||||
func (v *Vulnerability) ToAbstractVulnerability() (*AbstractVulnerability, error) {
|
|
||||||
// Find FixedIn packages.
|
|
||||||
fixedInPackages, err := FindAllPackagesByNodes(v.FixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AbstractVulnerability{
|
|
||||||
ID: v.ID,
|
|
||||||
Link: v.Link,
|
|
||||||
Priority: v.Priority,
|
|
||||||
Description: v.Description,
|
|
||||||
AffectedPackages: PackagesToAbstractPackages(fixedInPackages),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbstractVulnerability represents a Vulnerability as it is defined in the database
|
|
||||||
// package but exposes directly a list of AbstractPackage instead of
|
|
||||||
// nodes to packages.
|
|
||||||
type AbstractVulnerability struct {
|
|
||||||
ID string
|
|
||||||
Link string
|
|
||||||
Priority types.Priority
|
|
||||||
Description string
|
|
||||||
AffectedPackages []*AbstractPackage
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToVulnerability converts an abstractVulnerability into
|
|
||||||
// a Vulnerability
|
|
||||||
func (av *AbstractVulnerability) ToVulnerability(fixedInNodes []string) *Vulnerability {
|
|
||||||
return &Vulnerability{
|
|
||||||
ID: av.ID,
|
|
||||||
Link: av.Link,
|
|
||||||
Priority: av.Priority,
|
|
||||||
Description: av.Description,
|
|
||||||
FixedInNodes: fixedInNodes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertVulnerabilities inserts or updates several vulnerabilities in the database in one transaction
|
|
||||||
// During an update, if the vulnerability was previously fixed by a version in a branch and a new package of that branch is specified, the previous one is deleted
|
|
||||||
// Otherwise, it simply adds the defined packages, there is currently no way to delete affected packages.
|
|
||||||
//
|
|
||||||
// ID, Link, Priority and FixedInNodes fields have to be specified. Description is optionnal.
|
|
||||||
func InsertVulnerabilities(vulnerabilities []*Vulnerability) ([]Notification, error) {
|
|
||||||
if len(vulnerabilities) == 0 {
|
|
||||||
return []Notification{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create required data structure
|
|
||||||
var err error
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
cachedVulnerabilities := make(map[string]*Vulnerability)
|
|
||||||
|
|
||||||
var notifications []Notification
|
|
||||||
newVulnerabilityNotifications := make(map[string]*NewVulnerabilityNotification)
|
|
||||||
vulnerabilityPriorityIncreasedNotifications := make(map[string]*VulnerabilityPriorityIncreasedNotification)
|
|
||||||
vulnerabilityPackageChangedNotifications := make(map[string]*VulnerabilityPackageChangedNotification)
|
|
||||||
|
|
||||||
// Iterate over all the vulnerabilities we need to insert/update
|
|
||||||
for _, vulnerability := range vulnerabilities {
|
|
||||||
// Check if the vulnerability already exists
|
|
||||||
existingVulnerability, _ := cachedVulnerabilities[vulnerability.ID]
|
|
||||||
if existingVulnerability == nil {
|
|
||||||
existingVulnerability, err = FindOneVulnerability(vulnerability.ID, FieldVulnerabilityAll)
|
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
|
||||||
return []Notification{}, err
|
|
||||||
}
|
|
||||||
if existingVulnerability != nil {
|
|
||||||
cachedVulnerabilities[vulnerability.ID] = existingVulnerability
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert/Update vulnerability
|
|
||||||
if existingVulnerability == nil {
|
|
||||||
// The vulnerability does not exist, create it
|
|
||||||
|
|
||||||
// Verify parameters
|
|
||||||
if vulnerability.ID == "" || vulnerability.Link == "" || vulnerability.Priority == "" {
|
|
||||||
log.Warningf("could not insert an incomplete vulnerability [ID: %s, Link: %s, Priority: %s]", vulnerability.ID, vulnerability.Link, vulnerability.Priority)
|
|
||||||
return []Notification{}, cerrors.NewBadRequestError("Could not insert an incomplete vulnerability")
|
|
||||||
}
|
|
||||||
if !vulnerability.Priority.IsValid() {
|
|
||||||
log.Warningf("could not insert a vulnerability which has an invalid priority [ID: %s, Link: %s, Priority: %s]. Valid priorities are: %v.", vulnerability.ID, vulnerability.Link, vulnerability.Priority, types.Priorities)
|
|
||||||
return []Notification{}, cerrors.NewBadRequestError("Could not insert a vulnerability which has an invalid priority")
|
|
||||||
}
|
|
||||||
if len(vulnerability.FixedInNodes) == 0 {
|
|
||||||
log.Warningf("could not insert a vulnerability which doesn't affect any package [ID: %s].", vulnerability.ID)
|
|
||||||
return []Notification{}, cerrors.NewBadRequestError("could not insert a vulnerability which doesn't affect any package")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert it
|
|
||||||
vulnerability.Node = vulnerability.GetNode()
|
|
||||||
|
|
||||||
t.AddQuad(cayley.Triple(vulnerability.Node, fieldIs, fieldVulnerabilityIsValue))
|
|
||||||
t.AddQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityID, vulnerability.ID))
|
|
||||||
t.AddQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityLink, vulnerability.Link))
|
|
||||||
t.AddQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityPriority, string(vulnerability.Priority)))
|
|
||||||
t.AddQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityDescription, vulnerability.Description))
|
|
||||||
for _, p := range vulnerability.FixedInNodes {
|
|
||||||
t.AddQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityFixedIn, p))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a notification
|
|
||||||
notification := &NewVulnerabilityNotification{VulnerabilityID: vulnerability.ID}
|
|
||||||
notifications = append(notifications, notification)
|
|
||||||
newVulnerabilityNotifications[vulnerability.ID] = notification
|
|
||||||
|
|
||||||
cachedVulnerabilities[vulnerability.ID] = vulnerability
|
|
||||||
} else {
|
|
||||||
// The vulnerability already exists, update it
|
|
||||||
if vulnerability.Link != "" && existingVulnerability.Link != vulnerability.Link {
|
|
||||||
t.RemoveQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityLink, existingVulnerability.Link))
|
|
||||||
t.AddQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityLink, vulnerability.Link))
|
|
||||||
existingVulnerability.Link = vulnerability.Link
|
|
||||||
}
|
|
||||||
|
|
||||||
if vulnerability.Priority != "" && vulnerability.Priority != types.Unknown && existingVulnerability.Priority != vulnerability.Priority {
|
|
||||||
if !vulnerability.Priority.IsValid() {
|
|
||||||
log.Warningf("could not update a vulnerability which has an invalid priority [ID: %s, Link: %s, Priority: %s]. Valid priorities are: %v.", vulnerability.ID, vulnerability.Link, vulnerability.Priority, types.Priorities)
|
|
||||||
return []Notification{}, cerrors.NewBadRequestError("Could not update a vulnerability which has an invalid priority")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a notification about the priority change if the new priority is higher and the vulnerability is not new
|
|
||||||
if vulnerability.Priority.Compare(existingVulnerability.Priority) > 0 {
|
|
||||||
if _, newVulnerabilityNotificationExists := newVulnerabilityNotifications[vulnerability.ID]; !newVulnerabilityNotificationExists {
|
|
||||||
// Any priorityChangeNotification already ?
|
|
||||||
if existingPriorityNotification, _ := vulnerabilityPriorityIncreasedNotifications[vulnerability.ID]; existingPriorityNotification != nil {
|
|
||||||
// There is a priority change notification, replace it but keep the old priority field
|
|
||||||
existingPriorityNotification.NewPriority = vulnerability.Priority
|
|
||||||
} else {
|
|
||||||
// No previous notification, just add a new one
|
|
||||||
notification := &VulnerabilityPriorityIncreasedNotification{OldPriority: existingVulnerability.Priority, NewPriority: vulnerability.Priority, VulnerabilityID: existingVulnerability.ID}
|
|
||||||
notifications = append(notifications, notification)
|
|
||||||
vulnerabilityPriorityIncreasedNotifications[vulnerability.ID] = notification
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.RemoveQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityPriority, string(existingVulnerability.Priority)))
|
|
||||||
t.AddQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityPriority, string(vulnerability.Priority)))
|
|
||||||
existingVulnerability.Priority = vulnerability.Priority
|
|
||||||
}
|
|
||||||
|
|
||||||
if vulnerability.Description != "" && existingVulnerability.Description != vulnerability.Description {
|
|
||||||
t.RemoveQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityDescription, existingVulnerability.Description))
|
|
||||||
t.AddQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityDescription, vulnerability.Description))
|
|
||||||
existingVulnerability.Description = vulnerability.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
newFixedInNodes := utils.CompareStringLists(vulnerability.FixedInNodes, existingVulnerability.FixedInNodes)
|
|
||||||
if len(newFixedInNodes) > 0 {
|
|
||||||
var removedNodes []string
|
|
||||||
var addedNodes []string
|
|
||||||
|
|
||||||
existingVulnerabilityFixedInPackages, err := FindAllPackagesByNodes(existingVulnerability.FixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
return []Notification{}, err
|
|
||||||
}
|
|
||||||
newFixedInPackages, err := FindAllPackagesByNodes(newFixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
return []Notification{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range newFixedInPackages {
|
|
||||||
for _, ep := range existingVulnerabilityFixedInPackages {
|
|
||||||
if p.Branch() == ep.Branch() {
|
|
||||||
// A link to this package branch already exist and is not the same version, we will delete it
|
|
||||||
t.RemoveQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityFixedIn, ep.Node))
|
|
||||||
|
|
||||||
var index int
|
|
||||||
for i, n := range existingVulnerability.FixedInNodes {
|
|
||||||
if n == ep.Node {
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existingVulnerability.FixedInNodes = append(existingVulnerability.FixedInNodes[index:], existingVulnerability.FixedInNodes[index+1:]...)
|
|
||||||
removedNodes = append(removedNodes, ep.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.AddQuad(cayley.Triple(existingVulnerability.Node, FieldVulnerabilityFixedIn, p.Node))
|
|
||||||
existingVulnerability.FixedInNodes = append(existingVulnerability.FixedInNodes, p.Node)
|
|
||||||
addedNodes = append(addedNodes, p.Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add notification about the FixedIn modification if the vulnerability is not new
|
|
||||||
if _, newVulnerabilityNotificationExists := newVulnerabilityNotifications[vulnerability.ID]; !newVulnerabilityNotificationExists {
|
|
||||||
// Any VulnerabilityPackageChangedNotification already ?
|
|
||||||
if existingPackageNotification, _ := vulnerabilityPackageChangedNotifications[vulnerability.ID]; existingPackageNotification != nil {
|
|
||||||
// There is a priority change notification, add the packages modifications to it
|
|
||||||
existingPackageNotification.AddedFixedInNodes = append(existingPackageNotification.AddedFixedInNodes, addedNodes...)
|
|
||||||
existingPackageNotification.RemovedFixedInNodes = append(existingPackageNotification.RemovedFixedInNodes, removedNodes...)
|
|
||||||
} else {
|
|
||||||
// No previous notification, just add a new one
|
|
||||||
notification := &VulnerabilityPackageChangedNotification{VulnerabilityID: vulnerability.ID, AddedFixedInNodes: addedNodes, RemovedFixedInNodes: removedNodes}
|
|
||||||
notifications = append(notifications, notification)
|
|
||||||
vulnerabilityPackageChangedNotifications[vulnerability.ID] = notification
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply transaction
|
|
||||||
if err = store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (InsertVulnerabilities): %s", err)
|
|
||||||
return []Notification{}, ErrTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
return notifications, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteVulnerability deletes the vulnerability having the given ID
|
|
||||||
func DeleteVulnerability(id string) error {
|
|
||||||
vulnerability, err := FindOneVulnerability(id, FieldVulnerabilityAll)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
t := cayley.NewTransaction()
|
|
||||||
t.RemoveQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityID, vulnerability.ID))
|
|
||||||
t.RemoveQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityLink, vulnerability.Link))
|
|
||||||
t.RemoveQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityPriority, string(vulnerability.Priority)))
|
|
||||||
t.RemoveQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityDescription, vulnerability.Description))
|
|
||||||
for _, p := range vulnerability.FixedInNodes {
|
|
||||||
t.RemoveQuad(cayley.Triple(vulnerability.Node, FieldVulnerabilityFixedIn, p))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := store.ApplyTransaction(t); err != nil {
|
|
||||||
log.Errorf("failed transaction (DeleteVulnerability): %s", err)
|
|
||||||
return ErrTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindOneVulnerability finds and returns a single vulnerability having the given ID selecting the specified fields
|
|
||||||
func FindOneVulnerability(id string, selectedFields []string) (*Vulnerability, error) {
|
|
||||||
t := &Vulnerability{ID: id}
|
|
||||||
v, err := toVulnerabilities(cayley.StartPath(store, t.GetNode()).Has(fieldIs, fieldVulnerabilityIsValue), selectedFields)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(v) == 1 {
|
|
||||||
return v[0], nil
|
|
||||||
}
|
|
||||||
if len(v) > 1 {
|
|
||||||
log.Errorf("found multiple vulnerabilities with identical ID [ID: %s]", id)
|
|
||||||
return nil, ErrInconsistent
|
|
||||||
}
|
|
||||||
return nil, cerrors.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAllVulnerabilitiesByFixedIn finds and returns all vulnerabilities that are fixed in the given packages (speficied by their nodes), selecting the specified fields
|
|
||||||
func FindAllVulnerabilitiesByFixedIn(nodes []string, selectedFields []string) ([]*Vulnerability, error) {
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
log.Warning("Could not FindAllVulnerabilitiesByFixedIn with an empty nodes array.")
|
|
||||||
return []*Vulnerability{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct path, potentially saving FieldVulnerabilityCausedByPackage
|
|
||||||
path := cayley.StartPath(store, nodes...)
|
|
||||||
if utils.Contains(FieldVulnerabilityCausedByPackage, selectedFields) {
|
|
||||||
path = path.Save(FieldPackageName, FieldVulnerabilityCausedByPackage)
|
|
||||||
}
|
|
||||||
path = path.In(FieldVulnerabilityFixedIn)
|
|
||||||
|
|
||||||
return toVulnerabilities(path, selectedFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// toVulnerabilities converts a path leading to one or multiple vulnerabilities to Vulnerability structs, selecting the specified fields
|
|
||||||
func toVulnerabilities(path *path.Path, selectedFields []string) ([]*Vulnerability, error) {
|
|
||||||
var vulnerabilities []*Vulnerability
|
|
||||||
|
|
||||||
saveFields(path, selectedFields, []string{FieldVulnerabilityFixedIn, FieldVulnerabilityCausedByPackage})
|
|
||||||
it, _ := path.BuildIterator().Optimize()
|
|
||||||
defer it.Close()
|
|
||||||
for cayley.RawNext(it) {
|
|
||||||
tags := make(map[string]graph.Value)
|
|
||||||
it.TagResults(tags)
|
|
||||||
|
|
||||||
vulnerability := Vulnerability{Node: store.NameOf(it.Result())}
|
|
||||||
for _, selectedField := range selectedFields {
|
|
||||||
switch selectedField {
|
|
||||||
case FieldVulnerabilityID:
|
|
||||||
vulnerability.ID = store.NameOf(tags[FieldVulnerabilityID])
|
|
||||||
case FieldVulnerabilityLink:
|
|
||||||
vulnerability.Link = store.NameOf(tags[FieldVulnerabilityLink])
|
|
||||||
case FieldVulnerabilityPriority:
|
|
||||||
vulnerability.Priority = types.Priority(store.NameOf(tags[FieldVulnerabilityPriority]))
|
|
||||||
case FieldVulnerabilityDescription:
|
|
||||||
vulnerability.Description = store.NameOf(tags[FieldVulnerabilityDescription])
|
|
||||||
case FieldVulnerabilityFixedIn:
|
|
||||||
var err error
|
|
||||||
vulnerability.FixedInNodes, err = toValues(cayley.StartPath(store, vulnerability.Node).Out(FieldVulnerabilityFixedIn))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("could not get fixedIn on vulnerability %s: %s.", vulnerability.Node, err.Error())
|
|
||||||
return []*Vulnerability{}, err
|
|
||||||
}
|
|
||||||
case FieldVulnerabilityCausedByPackage:
|
|
||||||
vulnerability.CausedByPackage = store.NameOf(tags[FieldVulnerabilityCausedByPackage])
|
|
||||||
default:
|
|
||||||
panic("unknown selectedField")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vulnerabilities = append(vulnerabilities, &vulnerability)
|
|
||||||
}
|
|
||||||
if it.Err() != nil {
|
|
||||||
log.Errorf("failed query in toVulnerabilities: %s", it.Err())
|
|
||||||
return []*Vulnerability{}, ErrBackendException
|
|
||||||
}
|
|
||||||
|
|
||||||
return vulnerabilities, nil
|
|
||||||
}
|
|
@ -1,238 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/config"
|
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/coreos/clair/utils/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestVulnerability(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
// Insert invalid vulnerabilities
|
|
||||||
for _, vulnerability := range []Vulnerability{
|
|
||||||
Vulnerability{ID: "", Link: "link1", Priority: types.Medium, FixedInNodes: []string{"pkg1"}},
|
|
||||||
Vulnerability{ID: "test1", Link: "", Priority: types.Medium, FixedInNodes: []string{"pkg1"}},
|
|
||||||
Vulnerability{ID: "test1", Link: "link1", Priority: "InvalidPriority", FixedInNodes: []string{"pkg1"}},
|
|
||||||
Vulnerability{ID: "test1", Link: "link1", Priority: types.Medium, FixedInNodes: []string{}},
|
|
||||||
} {
|
|
||||||
_, err := InsertVulnerabilities([]*Vulnerability{&vulnerability})
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some data
|
|
||||||
vuln1 := &Vulnerability{ID: "test1", Link: "link1", Priority: types.Medium, Description: "testDescription1", FixedInNodes: []string{"pkg1"}}
|
|
||||||
vuln2 := &Vulnerability{ID: "test2", Link: "link2", Priority: types.High, Description: "testDescription2", FixedInNodes: []string{"pkg1", "pkg2"}}
|
|
||||||
vuln3 := &Vulnerability{ID: "test3", Link: "link3", Priority: types.High, FixedInNodes: []string{"pkg3"}} // Empty description
|
|
||||||
|
|
||||||
// Insert some vulnerabilities
|
|
||||||
_, err := InsertVulnerabilities([]*Vulnerability{vuln1, vuln2, vuln3})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
// Find one of the vulnerabilities we just inserted and verify its content
|
|
||||||
v1, err := FindOneVulnerability(vuln1.ID, FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, v1) {
|
|
||||||
assert.Equal(t, vuln1.ID, v1.ID)
|
|
||||||
assert.Equal(t, vuln1.Link, v1.Link)
|
|
||||||
assert.Equal(t, vuln1.Priority, v1.Priority)
|
|
||||||
assert.Equal(t, vuln1.Description, v1.Description)
|
|
||||||
if assert.Len(t, v1.FixedInNodes, 1) {
|
|
||||||
assert.Equal(t, vuln1.FixedInNodes[0], v1.FixedInNodes[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that vulnerabilities with empty descriptions work as well
|
|
||||||
v3, err := FindOneVulnerability(vuln3.ID, FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, v3) {
|
|
||||||
assert.Equal(t, vuln3.Description, v3.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find vulnerabilities by fixed packages
|
|
||||||
vulnsFixedInPkg1AndPkg3, err := FindAllVulnerabilitiesByFixedIn([]string{"pkg2", "pkg3"}, FieldVulnerabilityAll)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, vulnsFixedInPkg1AndPkg3, 2)
|
|
||||||
|
|
||||||
// Delete vulnerability
|
|
||||||
if assert.Nil(t, DeleteVulnerability(vuln1.ID)) {
|
|
||||||
v1, err := FindOneVulnerability(vuln1.ID, FieldVulnerabilityAll)
|
|
||||||
assert.Equal(t, cerrors.ErrNotFound, err)
|
|
||||||
assert.Nil(t, v1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update a vulnerability and verify its new content
|
|
||||||
pkg1 := &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
InsertPackages([]*Package{pkg1})
|
|
||||||
vuln5 := &Vulnerability{ID: "test5", Link: "link5", Priority: types.Medium, Description: "testDescription5", FixedInNodes: []string{pkg1.Node}}
|
|
||||||
|
|
||||||
_, err = InsertVulnerabilities([]*Vulnerability{vuln5})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
// Partial updates
|
|
||||||
// # Just a field update
|
|
||||||
vuln5b := &Vulnerability{ID: "test5", Priority: types.High}
|
|
||||||
_, err := InsertVulnerabilities([]*Vulnerability{vuln5b})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
v5b, err := FindOneVulnerability(vuln5b.ID, FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, v5b) {
|
|
||||||
assert.Equal(t, vuln5b.ID, v5b.ID)
|
|
||||||
assert.Equal(t, vuln5b.Priority, v5b.Priority)
|
|
||||||
|
|
||||||
if assert.Len(t, v5b.FixedInNodes, 1) {
|
|
||||||
assert.Contains(t, v5b.FixedInNodes, pkg1.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// # Just a field update, twice in the same transaction
|
|
||||||
vuln5b1 := &Vulnerability{ID: "test5", Link: "http://foo.bar"}
|
|
||||||
vuln5b2 := &Vulnerability{ID: "test5", Link: "http://bar.foo"}
|
|
||||||
_, err = InsertVulnerabilities([]*Vulnerability{vuln5b1, vuln5b2})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
v5b2, err := FindOneVulnerability(vuln5b2.ID, FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, v5b2) {
|
|
||||||
assert.Equal(t, vuln5b2.Link, v5b2.Link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// # All fields except fixedIn update
|
|
||||||
vuln5c := &Vulnerability{ID: "test5", Link: "link5c", Priority: types.Critical, Description: "testDescription5c"}
|
|
||||||
_, err = InsertVulnerabilities([]*Vulnerability{vuln5c})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
v5c, err := FindOneVulnerability(vuln5c.ID, FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, v5c) {
|
|
||||||
assert.Equal(t, vuln5c.ID, v5c.ID)
|
|
||||||
assert.Equal(t, vuln5c.Link, v5c.Link)
|
|
||||||
assert.Equal(t, vuln5c.Priority, v5c.Priority)
|
|
||||||
assert.Equal(t, vuln5c.Description, v5c.Description)
|
|
||||||
|
|
||||||
if assert.Len(t, v5c.FixedInNodes, 1) {
|
|
||||||
assert.Contains(t, v5c.FixedInNodes, pkg1.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete update
|
|
||||||
pkg2 := &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.1")}
|
|
||||||
pkg3 := &Package{OS: "testOS", Name: "testpkg2", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
InsertPackages([]*Package{pkg2, pkg3})
|
|
||||||
vuln5d := &Vulnerability{ID: "test5", Link: "link5d", Priority: types.Low, Description: "testDescription5d", FixedInNodes: []string{pkg2.Node, pkg3.Node}}
|
|
||||||
|
|
||||||
_, err = InsertVulnerabilities([]*Vulnerability{vuln5d})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
v5d, err := FindOneVulnerability(vuln5d.ID, FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.NotNil(t, v5d) {
|
|
||||||
assert.Equal(t, vuln5d.ID, v5d.ID)
|
|
||||||
assert.Equal(t, vuln5d.Link, v5d.Link)
|
|
||||||
assert.Equal(t, vuln5d.Priority, v5d.Priority)
|
|
||||||
assert.Equal(t, vuln5d.Description, v5d.Description)
|
|
||||||
|
|
||||||
// Here, we ensure that a vulnerability can only be fixed by one package of a given branch at a given time
|
|
||||||
// And that we can add new fixed packages as well
|
|
||||||
if assert.Len(t, v5d.FixedInNodes, 2) {
|
|
||||||
assert.NotContains(t, v5d.FixedInNodes, pkg1.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and update a vulnerability's packages (and from the same branch) in the same batch
|
|
||||||
pkg1 = &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
pkg1b := &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.1")}
|
|
||||||
InsertPackages([]*Package{pkg1, pkg1b})
|
|
||||||
|
|
||||||
// # Two updates of the same vulnerability in the same batch with packages of the same branch
|
|
||||||
pkg0 := &Package{OS: "testOS", Name: "testpkg0", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
InsertPackages([]*Package{pkg0})
|
|
||||||
_, err = InsertVulnerabilities([]*Vulnerability{&Vulnerability{ID: "test7", Link: "link7", Priority: types.Medium, Description: "testDescription7", FixedInNodes: []string{pkg0.Node}}})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
vuln7b := &Vulnerability{ID: "test7", FixedInNodes: []string{pkg1.Node}}
|
|
||||||
vuln7c := &Vulnerability{ID: "test7", FixedInNodes: []string{pkg1b.Node}}
|
|
||||||
_, err = InsertVulnerabilities([]*Vulnerability{vuln7b, vuln7c})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
v7, err := FindOneVulnerability("test7", FieldVulnerabilityAll)
|
|
||||||
if assert.Nil(t, err) && assert.Len(t, v7.FixedInNodes, 2) {
|
|
||||||
assert.Contains(t, v7.FixedInNodes, pkg0.Node)
|
|
||||||
assert.NotContains(t, v7.FixedInNodes, pkg1.Node)
|
|
||||||
assert.Contains(t, v7.FixedInNodes, pkg1b.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInsertVulnerabilityNotifications(t *testing.T) {
|
|
||||||
Open(&config.DatabaseConfig{Type: "memstore"})
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
pkg1 := &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
pkg1b := &Package{OS: "testOS", Name: "testpkg1", Version: types.NewVersionUnsafe("1.2")}
|
|
||||||
pkg2 := &Package{OS: "testOS", Name: "testpkg2", Version: types.NewVersionUnsafe("1.0")}
|
|
||||||
InsertPackages([]*Package{pkg1, pkg1b, pkg2})
|
|
||||||
|
|
||||||
// NewVulnerabilityNotification
|
|
||||||
vuln1 := &Vulnerability{ID: "test1", Link: "link1", Priority: types.Medium, Description: "testDescription1", FixedInNodes: []string{pkg1.Node}}
|
|
||||||
vuln2 := &Vulnerability{ID: "test2", Link: "link2", Priority: types.High, Description: "testDescription2", FixedInNodes: []string{pkg1.Node, pkg2.Node}}
|
|
||||||
vuln1b := &Vulnerability{ID: "test1", Priority: types.High, FixedInNodes: []string{"pkg3"}}
|
|
||||||
notifications, err := InsertVulnerabilities([]*Vulnerability{vuln1, vuln2, vuln1b})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
// We should only have two NewVulnerabilityNotification notifications: one for test1 and one for test2
|
|
||||||
// We should not have a VulnerabilityPriorityIncreasedNotification or a VulnerabilityPackageChangedNotification
|
|
||||||
// for test1 because it is in the same batch
|
|
||||||
if assert.Len(t, notifications, 2) {
|
|
||||||
for _, n := range notifications {
|
|
||||||
_, ok := n.(*NewVulnerabilityNotification)
|
|
||||||
assert.True(t, ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VulnerabilityPriorityIncreasedNotification
|
|
||||||
vuln1c := &Vulnerability{ID: "test1", Priority: types.Critical}
|
|
||||||
notifications, err = InsertVulnerabilities([]*Vulnerability{vuln1c})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
if assert.Len(t, notifications, 1) {
|
|
||||||
if nn, ok := notifications[0].(*VulnerabilityPriorityIncreasedNotification); assert.True(t, ok) {
|
|
||||||
assert.Equal(t, vuln1b.Priority, nn.OldPriority)
|
|
||||||
assert.Equal(t, vuln1c.Priority, nn.NewPriority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications, err = InsertVulnerabilities([]*Vulnerability{&Vulnerability{ID: "test1", Priority: types.Low}})
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, notifications, 0)
|
|
||||||
|
|
||||||
// VulnerabilityPackageChangedNotification
|
|
||||||
vuln1e := &Vulnerability{ID: "test1", FixedInNodes: []string{pkg1b.Node}}
|
|
||||||
vuln1f := &Vulnerability{ID: "test1", FixedInNodes: []string{pkg2.Node}}
|
|
||||||
notifications, err = InsertVulnerabilities([]*Vulnerability{vuln1e, vuln1f})
|
|
||||||
if assert.Nil(t, err) {
|
|
||||||
if assert.Len(t, notifications, 1) {
|
|
||||||
if nn, ok := notifications[0].(*VulnerabilityPackageChangedNotification); assert.True(t, ok) {
|
|
||||||
// Here, we say that pkg1b fixes the vulnerability, but as pkg1b is in
|
|
||||||
// the same branch as pkg1, pkg1 should be removed and pkg1b added
|
|
||||||
// We also add pkg2 as fixed
|
|
||||||
assert.Contains(t, nn.AddedFixedInNodes, pkg1b.Node)
|
|
||||||
assert.Contains(t, nn.RemovedFixedInNodes, pkg1.Node)
|
|
||||||
|
|
||||||
assert.Contains(t, nn.AddedFixedInNodes, pkg2.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -48,7 +48,7 @@ type WebhookNotifierConfiguration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
notifier.RegisterNotifier("webhook", &WebhookNotifier{})
|
//notifier.RegisterNotifier("webhook", &WebhookNotifier{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *WebhookNotifier) Configure(config *config.NotifierConfig) (bool, error) {
|
func (h *WebhookNotifier) Configure(config *config.NotifierConfig) (bool, error) {
|
||||||
|
BIN
updater/.DS_Store
vendored
Normal file
BIN
updater/.DS_Store
vendored
Normal file
Binary file not shown.
@ -53,7 +53,7 @@ type jsonRel struct {
|
|||||||
type DebianFetcher struct{}
|
type DebianFetcher struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updater.RegisterFetcher("debian", &DebianFetcher{})
|
//updater.RegisterFetcher("debian", &DebianFetcher{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchUpdate fetches vulnerability updates from the Debian Security Tracker.
|
// FetchUpdate fetches vulnerability updates from the Debian Security Tracker.
|
||||||
|
19
updater/fetchers/nvd.go
Normal file
19
updater/fetchers/nvd.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package fetchers
|
||||||
|
|
||||||
|
import "github.com/coreos/clair/updater"
|
||||||
|
|
||||||
|
// NVDFetcher implements updater.Fetcher and gets vulnerability updates from
|
||||||
|
// the National Vulnerability Database (NVD), from the
|
||||||
|
// National Institute of Standards and Technology (NIST).
|
||||||
|
type NVDFetcher struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
//updater.RegisterFetcher("NVD", &RHELFetcher{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUpdate gets vulnerability updates from the NVD database.
|
||||||
|
func (f *NVDFetcher) FetchUpdate() (resp updater.FetcherResponse, err error) {
|
||||||
|
log.Info("fetching NVD vulneratibilities")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -81,7 +81,7 @@ type criterion struct {
|
|||||||
type RHELFetcher struct{}
|
type RHELFetcher struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updater.RegisterFetcher("Red Hat", &RHELFetcher{})
|
//updater.RegisterFetcher("Red Hat", &RHELFetcher{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchUpdate gets vulnerability updates from the Red Hat OVAL definitions.
|
// FetchUpdate gets vulnerability updates from the Red Hat OVAL definitions.
|
||||||
|
@ -76,7 +76,7 @@ var (
|
|||||||
type UbuntuFetcher struct{}
|
type UbuntuFetcher struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updater.RegisterFetcher("Ubuntu", &UbuntuFetcher{})
|
//updater.RegisterFetcher("Ubuntu", &UbuntuFetcher{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchUpdate gets vulnerability updates from the Ubuntu CVE Tracker.
|
// FetchUpdate gets vulnerability updates from the Ubuntu CVE Tracker.
|
||||||
|
211
utils/types/cvss.go
Normal file
211
utils/types/cvss.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
//
|
||||||
|
// import "fmt"
|
||||||
|
//
|
||||||
|
// // CVSSv2 represents the Common Vulnerability Scoring System (CVSS), that assesses the severity of
|
||||||
|
// // vulnerabilities.
|
||||||
|
// // It describes the CVSS score, but also a vector describing the components from which the score
|
||||||
|
// // was calculated. This provides users of the score confidence in its correctness and provides
|
||||||
|
// // insight into the nature of the vulnerability.
|
||||||
|
// //
|
||||||
|
// // Reference: https://nvd.nist.gov/CVSS/Vector-v2.aspx
|
||||||
|
// type CVSSv2 struct {
|
||||||
|
// // Base Vectors
|
||||||
|
// AccessVector CVSSValue
|
||||||
|
// AccessComplexity CVSSValue
|
||||||
|
// Authentication CVSSValue
|
||||||
|
// ConfImpact CVSSValue
|
||||||
|
// IntegImpact CVSSValue
|
||||||
|
// AvailImpact CVSSValue
|
||||||
|
// // Temporal Vectors
|
||||||
|
// Exploitability CVSSValue
|
||||||
|
// RemediationLevel CVSSValue
|
||||||
|
// ReportConfidence CVSSValue
|
||||||
|
// // Environmental Vectors
|
||||||
|
// CollateralDamagePotential CVSSValue
|
||||||
|
// TargetDistribution CVSSValue
|
||||||
|
// SystemConfidentialityRequirement CVSSValue
|
||||||
|
// SystemIntegrityRequirement CVSSValue
|
||||||
|
// SystemAvailabilityRequirement CVSSValue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func NewCVSSv2(value string) (*CVSSv2, error) {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // CVSSValue is the comprehensible value for a CVSS metric.
|
||||||
|
// type CVSSValue string
|
||||||
|
//
|
||||||
|
// // Metric acronym + Value abbreviation -> Comprehensible metric value.
|
||||||
|
// var toValue map[string]func(string) (CVSSValue, error)
|
||||||
|
//
|
||||||
|
// func init() {
|
||||||
|
// parsers = make(map[string]func(string) (CVSSValue, error), 14)
|
||||||
|
// toValue["AV"] = av
|
||||||
|
// toValue["AC"] = ac
|
||||||
|
// toValue["Au"] = au
|
||||||
|
// toValue["C"] = cAndIAndA
|
||||||
|
// toValue["I"] = cAndIAndA
|
||||||
|
// toValue["A"] = cAndIAndA
|
||||||
|
// toValue["E"] = e
|
||||||
|
// toValue["RL"] = rl
|
||||||
|
// toValue["RC"] = rc
|
||||||
|
// toValue["CDP"] = cdp
|
||||||
|
// toValue["TD"] = td
|
||||||
|
// toValue["CR"] = crAndIrAndAr
|
||||||
|
// toValue["IR"] = crAndIrAndAr
|
||||||
|
// toValue["AR"] = crAndIrAndAr
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func av(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "L":
|
||||||
|
// return CVSSValue("Local access"), nil
|
||||||
|
// case "A":
|
||||||
|
// return CVSSValue("Adjacent Network"), nil
|
||||||
|
// case "N":
|
||||||
|
// return CVSSValue("Network"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for AV", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func ac(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "H":
|
||||||
|
// return CVSSValue("High"), nil
|
||||||
|
// case "M":
|
||||||
|
// return CVSSValue("Medium"), nil
|
||||||
|
// case "L":
|
||||||
|
// return CVSSValue("Low"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for AC", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func au(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "N":
|
||||||
|
// return CVSSValue("None required"), nil
|
||||||
|
// case "S":
|
||||||
|
// return CVSSValue("Requires single instance"), nil
|
||||||
|
// case "M":
|
||||||
|
// return CVSSValue("Requires multiple instances"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for Au", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func cAndIAndA(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "N":
|
||||||
|
// return CVSSValue("None"), nil
|
||||||
|
// case "P":
|
||||||
|
// return CVSSValue("Partial"), nil
|
||||||
|
// case "C":
|
||||||
|
// return CVSSValue("Complete"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for C/I/A", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func e(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "U":
|
||||||
|
// return CVSSValue("Unproven"), nil
|
||||||
|
// case "POC":
|
||||||
|
// return CVSSValue("Proof-of-concept"), nil
|
||||||
|
// case "F":
|
||||||
|
// return CVSSValue("Functional"), nil
|
||||||
|
// case "H":
|
||||||
|
// return CVSSValue("High"), nil
|
||||||
|
// case "ND":
|
||||||
|
// return CVSSValue("Not Defined"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for E", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func rl(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "OF":
|
||||||
|
// return CVSSValue("Official-fix"), nil
|
||||||
|
// case "T":
|
||||||
|
// return CVSSValue("Temporary-fix"), nil
|
||||||
|
// case "W":
|
||||||
|
// return CVSSValue("Workaround"), nil
|
||||||
|
// case "U":
|
||||||
|
// return CVSSValue("Unavailable"), nil
|
||||||
|
// case "ND":
|
||||||
|
// return CVSSValue("Not Defined"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for RL", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func rc(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "UC":
|
||||||
|
// return CVSSValue("Unconfirmed"), nil
|
||||||
|
// case "UR":
|
||||||
|
// return CVSSValue("Uncorroborated"), nil
|
||||||
|
// case "C":
|
||||||
|
// return CVSSValue("Confirmed"), nil
|
||||||
|
// case "ND":
|
||||||
|
// return CVSSValue("Not Defined"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for RC", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func cdp(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "N":
|
||||||
|
// return CVSSValue("None"), nil
|
||||||
|
// case "L":
|
||||||
|
// return CVSSValue("Low"), nil
|
||||||
|
// case "LM":
|
||||||
|
// return CVSSValue("Low-Medium"), nil
|
||||||
|
// case "MH":
|
||||||
|
// return CVSSValue("Medium-High"), nil
|
||||||
|
// case "H":
|
||||||
|
// return CVSSValue("High"), nil
|
||||||
|
// case "ND":
|
||||||
|
// return CVSSValue("Not Defined"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for CDP", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func td(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "N":
|
||||||
|
// return CVSSValue("None (0%)"), nil
|
||||||
|
// case "L":
|
||||||
|
// return CVSSValue("Low (1-25%)"), nil
|
||||||
|
// case "M":
|
||||||
|
// return CVSSValue("Medium (26-75%)"), nil
|
||||||
|
// case "H":
|
||||||
|
// return CVSSValue("High (76-100%)"), nil
|
||||||
|
// case "ND":
|
||||||
|
// return CVSSValue("Not Defined"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for TD", v)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func crAndIrAndAr(v string) (CVSSValue, error) {
|
||||||
|
// switch v {
|
||||||
|
// case "L":
|
||||||
|
// return CVSSValue("Low"), nil
|
||||||
|
// case "M":
|
||||||
|
// return CVSSValue("Medium"), nil
|
||||||
|
// case "H":
|
||||||
|
// return CVSSValue("High"), nil
|
||||||
|
// case "ND":
|
||||||
|
// return CVSSValue("Not Defined"), nil
|
||||||
|
// default:
|
||||||
|
// return "", fmt.Errorf("%v is not a valid value for CR/IR/AR", v)
|
||||||
|
// }
|
||||||
|
// }
|
@ -15,6 +15,12 @@
|
|||||||
// Package types defines useful types that are used in database models.
|
// Package types defines useful types that are used in database models.
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// Priority defines a vulnerability priority
|
// Priority defines a vulnerability priority
|
||||||
type Priority string
|
type Priority string
|
||||||
|
|
||||||
@ -86,3 +92,19 @@ func (p Priority) Compare(p2 Priority) int {
|
|||||||
|
|
||||||
return i1 - i2
|
return i1 - i2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Priority) Scan(value interface{}) error {
|
||||||
|
val, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("could not scan a Priority from a non-string input")
|
||||||
|
}
|
||||||
|
*p = Priority(string(val))
|
||||||
|
if !p.IsValid() {
|
||||||
|
return fmt.Errorf("could not scan an invalid Priority (%v)", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Priority) Value() (driver.Value, error) {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -183,6 +184,19 @@ func (v *Version) UnmarshalJSON(b []byte) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Version) Scan(value interface{}) (err error) {
|
||||||
|
val, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("could not scan a Version from a non-string input")
|
||||||
|
}
|
||||||
|
*v, err = NewVersion(string(val))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Version) Value() (driver.Value, error) {
|
||||||
|
return v.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func verrevcmp(t1, t2 string) int {
|
func verrevcmp(t1, t2 string) int {
|
||||||
t1, rt1 := nextRune(t1)
|
t1, rt1 := nextRune(t1)
|
||||||
t2, rt2 := nextRune(t2)
|
t2, rt2 := nextRune(t2)
|
||||||
|
@ -12,18 +12,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// Package packages defines PackagesDetector for several sources.
|
package dpkg
|
||||||
package packages
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/pkg/capnslog"
|
|
||||||
"github.com/coreos/clair/database"
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/utils/types"
|
"github.com/coreos/clair/utils/types"
|
||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
|
"github.com/coreos/pkg/capnslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -33,24 +32,24 @@ var (
|
|||||||
dpkgSrcCaptureRegexpNames = dpkgSrcCaptureRegexp.SubexpNames()
|
dpkgSrcCaptureRegexpNames = dpkgSrcCaptureRegexp.SubexpNames()
|
||||||
)
|
)
|
||||||
|
|
||||||
// DpkgPackagesDetector implements PackagesDetector and detects dpkg packages
|
// DpkgFeaturesDetector implements FeaturesDetector and detects dpkg packages
|
||||||
type DpkgPackagesDetector struct{}
|
type DpkgFeaturesDetector struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
detectors.RegisterPackagesDetector("dpkg", &DpkgPackagesDetector{})
|
detectors.RegisterFeaturesDetector("dpkg", &DpkgFeaturesDetector{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect detects packages using var/lib/dpkg/status from the input data
|
// Detect detects packages using var/lib/dpkg/status from the input data
|
||||||
func (detector *DpkgPackagesDetector) Detect(data map[string][]byte) ([]*database.Package, error) {
|
func (detector *DpkgFeaturesDetector) Detect(data map[string][]byte) ([]database.FeatureVersion, error) {
|
||||||
f, hasFile := data["var/lib/dpkg/status"]
|
f, hasFile := data["var/lib/dpkg/status"]
|
||||||
if !hasFile {
|
if !hasFile {
|
||||||
return []*database.Package{}, nil
|
return []database.FeatureVersion{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map to store packages and ensure their uniqueness
|
// Create a map to store packages and ensure their uniqueness
|
||||||
packagesMap := make(map[string]*database.Package)
|
packagesMap := make(map[string]database.FeatureVersion)
|
||||||
|
|
||||||
var pkg *database.Package
|
var pkg database.FeatureVersion
|
||||||
var err error
|
var err error
|
||||||
scanner := bufio.NewScanner(strings.NewReader(string(f)))
|
scanner := bufio.NewScanner(strings.NewReader(string(f)))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@ -60,10 +59,8 @@ func (detector *DpkgPackagesDetector) Detect(data map[string][]byte) ([]*databas
|
|||||||
// Package line
|
// Package line
|
||||||
// Defines the name of the package
|
// Defines the name of the package
|
||||||
|
|
||||||
pkg = &database.Package{
|
pkg.Feature.Name = strings.TrimSpace(strings.TrimPrefix(line, "Package: "))
|
||||||
Name: strings.TrimSpace(strings.TrimPrefix(line, "Package: ")),
|
} else if strings.HasPrefix(line, "Source: ") {
|
||||||
}
|
|
||||||
} else if pkg != nil && strings.HasPrefix(line, "Source: ") {
|
|
||||||
// Source line (Optionnal)
|
// Source line (Optionnal)
|
||||||
// Gives the name of the source package
|
// Gives the name of the source package
|
||||||
// May also specifies a version
|
// May also specifies a version
|
||||||
@ -74,14 +71,14 @@ func (detector *DpkgPackagesDetector) Detect(data map[string][]byte) ([]*databas
|
|||||||
md[dpkgSrcCaptureRegexpNames[i]] = strings.TrimSpace(n)
|
md[dpkgSrcCaptureRegexpNames[i]] = strings.TrimSpace(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
pkg.Name = md["name"]
|
pkg.Feature.Name = md["name"]
|
||||||
if md["version"] != "" {
|
if md["version"] != "" {
|
||||||
pkg.Version, err = types.NewVersion(md["version"])
|
pkg.Version, err = types.NewVersion(md["version"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error())
|
log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if pkg != nil && strings.HasPrefix(line, "Version: ") && pkg.Version.String() == "" {
|
} else if strings.HasPrefix(line, "Version: ") && pkg.Version.String() == "" {
|
||||||
// Version line
|
// Version line
|
||||||
// Defines the version of the package
|
// Defines the version of the package
|
||||||
// This version is less important than a version retrieved from a Source line
|
// This version is less important than a version retrieved from a Source line
|
||||||
@ -94,14 +91,15 @@ func (detector *DpkgPackagesDetector) Detect(data map[string][]byte) ([]*databas
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the package to the result array if we have all the informations
|
// Add the package to the result array if we have all the informations
|
||||||
if pkg != nil && pkg.Name != "" && pkg.Version.String() != "" {
|
if pkg.Feature.Name != "" && pkg.Version.String() != "" {
|
||||||
packagesMap[pkg.Key()] = pkg
|
packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg
|
||||||
pkg = nil
|
pkg.Feature.Name = ""
|
||||||
|
pkg.Version = types.Version{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the map to a slice
|
// Convert the map to a slice
|
||||||
packages := make([]*database.Package, 0, len(packagesMap))
|
packages := make([]database.FeatureVersion, 0, len(packagesMap))
|
||||||
for _, pkg := range packagesMap {
|
for _, pkg := range packagesMap {
|
||||||
packages = append(packages, pkg)
|
packages = append(packages, pkg)
|
||||||
}
|
}
|
||||||
@ -111,6 +109,6 @@ func (detector *DpkgPackagesDetector) Detect(data map[string][]byte) ([]*databas
|
|||||||
|
|
||||||
// GetRequiredFiles returns the list of files required for Detect, without
|
// GetRequiredFiles returns the list of files required for Detect, without
|
||||||
// leading /
|
// leading /
|
||||||
func (detector *DpkgPackagesDetector) GetRequiredFiles() []string {
|
func (detector *DpkgFeaturesDetector) GetRequiredFiles() []string {
|
||||||
return []string{"var/lib/dpkg/status"}
|
return []string{"var/lib/dpkg/status"}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package packages
|
package dpkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@ -24,7 +24,7 @@ import (
|
|||||||
var dpkgPackagesTests = []packagesTest{
|
var dpkgPackagesTests = []packagesTest{
|
||||||
// Test an Ubuntu dpkg status file
|
// Test an Ubuntu dpkg status file
|
||||||
packagesTest{
|
packagesTest{
|
||||||
packages: []*database.Package{
|
packages: []database.FeatureVersion{
|
||||||
&database.Package{
|
&database.Package{
|
||||||
Name: "pam", // Two packages from this source are installed, it should only appear one time
|
Name: "pam", // Two packages from this source are installed, it should only appear one time
|
||||||
Version: types.NewVersionUnsafe("1.1.8-3.1ubuntu3"),
|
Version: types.NewVersionUnsafe("1.1.8-3.1ubuntu3"),
|
||||||
@ -44,6 +44,6 @@ var dpkgPackagesTests = []packagesTest{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDpkgPackagesDetector(t *testing.T) {
|
func TestDpkgFeaturesDetector(t *testing.T) {
|
||||||
testPackagesDetector(t, &DpkgPackagesDetector{}, dpkgPackagesTests)
|
feature.TestFeaturesDetector(t, &DpkgFeaturesDetector{}, dpkgPackagesTests)
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package packages
|
package rpm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -21,42 +21,45 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/clair/database"
|
"github.com/coreos/clair/database"
|
||||||
cerrors "github.com/coreos/clair/utils/errors"
|
|
||||||
"github.com/coreos/clair/utils"
|
"github.com/coreos/clair/utils"
|
||||||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||||||
"github.com/coreos/clair/utils/types"
|
"github.com/coreos/clair/utils/types"
|
||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
|
"github.com/coreos/pkg/capnslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RpmPackagesDetector implements PackagesDetector and detects rpm packages
|
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "rpm")
|
||||||
|
|
||||||
|
// RpmFeaturesDetector implements FeaturesDetector and detects rpm packages
|
||||||
// It requires the "rpm" binary to be in the PATH
|
// It requires the "rpm" binary to be in the PATH
|
||||||
type RpmPackagesDetector struct{}
|
type RpmFeaturesDetector struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
detectors.RegisterPackagesDetector("rpm", &RpmPackagesDetector{})
|
detectors.RegisterFeaturesDetector("rpm", &RpmFeaturesDetector{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect detects packages using var/lib/rpm/Packages from the input data
|
// Detect detects packages using var/lib/rpm/Packages from the input data
|
||||||
func (detector *RpmPackagesDetector) Detect(data map[string][]byte) ([]*database.Package, error) {
|
func (detector *RpmFeaturesDetector) Detect(data map[string][]byte) ([]database.FeatureVersion, error) {
|
||||||
f, hasFile := data["var/lib/rpm/Packages"]
|
f, hasFile := data["var/lib/rpm/Packages"]
|
||||||
if !hasFile {
|
if !hasFile {
|
||||||
return []*database.Package{}, nil
|
return []database.FeatureVersion{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map to store packages and ensure their uniqueness
|
// Create a map to store packages and ensure their uniqueness
|
||||||
packagesMap := make(map[string]*database.Package)
|
packagesMap := make(map[string]database.FeatureVersion)
|
||||||
|
|
||||||
// Write the required "Packages" file to disk
|
// Write the required "Packages" file to disk
|
||||||
tmpDir, err := ioutil.TempDir(os.TempDir(), "rpm")
|
tmpDir, err := ioutil.TempDir(os.TempDir(), "rpm")
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("could not create temporary folder for RPM detection: %s", err)
|
log.Errorf("could not create temporary folder for RPM detection: %s", err)
|
||||||
return []*database.Package{}, cerrors.ErrFilesystem
|
return []database.FeatureVersion{}, cerrors.ErrFilesystem
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(tmpDir+"/Packages", f, 0700)
|
err = ioutil.WriteFile(tmpDir+"/Packages", f, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("could not create temporary file for RPM detection: %s", err)
|
log.Errorf("could not create temporary file for RPM detection: %s", err)
|
||||||
return []*database.Package{}, cerrors.ErrFilesystem
|
return []database.FeatureVersion{}, cerrors.ErrFilesystem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query RPM
|
// Query RPM
|
||||||
@ -67,7 +70,7 @@ func (detector *RpmPackagesDetector) Detect(data map[string][]byte) ([]*database
|
|||||||
log.Errorf("could not query RPM: %s. output: %s", err, string(out))
|
log.Errorf("could not query RPM: %s. output: %s", err, string(out))
|
||||||
// Do not bubble up because we probably won't be able to fix it,
|
// Do not bubble up because we probably won't be able to fix it,
|
||||||
// the database must be corrupted
|
// the database must be corrupted
|
||||||
return []*database.Package{}, nil
|
return []database.FeatureVersion{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(string(out)))
|
scanner := bufio.NewScanner(strings.NewReader(string(out)))
|
||||||
@ -92,12 +95,17 @@ func (detector *RpmPackagesDetector) Detect(data map[string][]byte) ([]*database
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add package
|
// Add package
|
||||||
pkg := &database.Package{Name: line[0], Version: version}
|
pkg := database.FeatureVersion{
|
||||||
packagesMap[pkg.Key()] = pkg
|
Feature: database.Feature{
|
||||||
|
Name: line[0],
|
||||||
|
},
|
||||||
|
Version: version,
|
||||||
|
}
|
||||||
|
packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the map to a slice
|
// Convert the map to a slice
|
||||||
packages := make([]*database.Package, 0, len(packagesMap))
|
packages := make([]database.FeatureVersion, 0, len(packagesMap))
|
||||||
for _, pkg := range packagesMap {
|
for _, pkg := range packagesMap {
|
||||||
packages = append(packages, pkg)
|
packages = append(packages, pkg)
|
||||||
}
|
}
|
||||||
@ -107,6 +115,6 @@ func (detector *RpmPackagesDetector) Detect(data map[string][]byte) ([]*database
|
|||||||
|
|
||||||
// GetRequiredFiles returns the list of files required for Detect, without
|
// GetRequiredFiles returns the list of files required for Detect, without
|
||||||
// leading /
|
// leading /
|
||||||
func (detector *RpmPackagesDetector) GetRequiredFiles() []string {
|
func (detector *RpmFeaturesDetector) GetRequiredFiles() []string {
|
||||||
return []string{"var/lib/rpm/Packages"}
|
return []string{"var/lib/rpm/Packages"}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package packages
|
package rpm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@ -25,7 +25,7 @@ var rpmPackagesTests = []packagesTest{
|
|||||||
// Test a CentOS 7 RPM database
|
// Test a CentOS 7 RPM database
|
||||||
// Memo: Use the following command on a RPM-based system to shrink a database: rpm -qa --qf "%{NAME}\n" |tail -n +3| xargs rpm -e --justdb
|
// Memo: Use the following command on a RPM-based system to shrink a database: rpm -qa --qf "%{NAME}\n" |tail -n +3| xargs rpm -e --justdb
|
||||||
packagesTest{
|
packagesTest{
|
||||||
packages: []*database.Package{
|
packages: []database.FeatureVersion{
|
||||||
&database.Package{
|
&database.Package{
|
||||||
Name: "centos-release", // Two packages from this source are installed, it should only appear one time
|
Name: "centos-release", // Two packages from this source are installed, it should only appear one time
|
||||||
Version: types.NewVersionUnsafe("7-1.1503.el7.centos.2.8"),
|
Version: types.NewVersionUnsafe("7-1.1503.el7.centos.2.8"),
|
||||||
@ -41,6 +41,6 @@ var rpmPackagesTests = []packagesTest{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRpmPackagesDetector(t *testing.T) {
|
func TestRpmFeaturesDetector(t *testing.T) {
|
||||||
testPackagesDetector(t, &RpmPackagesDetector{}, rpmPackagesTests)
|
feature.TestFeaturesDetector(t, &RpmFeaturesDetector{}, rpmPackagesTests)
|
||||||
}
|
}
|
@ -26,7 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type packagesTest struct {
|
type packagesTest struct {
|
||||||
packages []*database.Package
|
packages []database.FeatureVersion
|
||||||
data map[string][]byte
|
data map[string][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ func loadFileForTest(name string) []byte {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPackagesDetector(t *testing.T, detector detectors.PackagesDetector, tests []packagesTest) {
|
func testFeaturesDetector(t *testing.T, detector detectors.FeaturesDetector, tests []packagesTest) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
packages, err := detector.Detect(test.data)
|
packages, err := detector.Detect(test.data)
|
||||||
if assert.Nil(t, err) && assert.Len(t, packages, len(test.packages)) {
|
if assert.Nil(t, err) && assert.Len(t, packages, len(test.packages)) {
|
79
worker/detectors/features.go
Normal file
79
worker/detectors/features.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2015 clair authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package detectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The FeaturesDetector interface defines a way to detect packages from input data.
|
||||||
|
type FeaturesDetector interface {
|
||||||
|
// Detect detects a list of FeatureVersion from the input data.
|
||||||
|
Detect(map[string][]byte) ([]database.FeatureVersion, error)
|
||||||
|
// GetRequiredFiles returns the list of files required for Detect, without
|
||||||
|
// leading /.
|
||||||
|
GetRequiredFiles() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
featuresDetectorsLock sync.Mutex
|
||||||
|
featuresDetectors = make(map[string]FeaturesDetector)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterFeaturesDetector makes a FeaturesDetector available for DetectFeatures.
|
||||||
|
func RegisterFeaturesDetector(name string, f FeaturesDetector) {
|
||||||
|
if name == "" {
|
||||||
|
panic("Could not register a FeaturesDetector with an empty name")
|
||||||
|
}
|
||||||
|
if f == nil {
|
||||||
|
panic("Could not register a nil FeaturesDetector")
|
||||||
|
}
|
||||||
|
|
||||||
|
featuresDetectorsLock.Lock()
|
||||||
|
defer featuresDetectorsLock.Unlock()
|
||||||
|
|
||||||
|
if _, alreadyExists := featuresDetectors[name]; alreadyExists {
|
||||||
|
panic(fmt.Sprintf("Detector '%s' is already registered", name))
|
||||||
|
}
|
||||||
|
featuresDetectors[name] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectFeatures detects a list of FeatureVersion using every registered FeaturesDetector.
|
||||||
|
func DetectFeatures(data map[string][]byte) ([]database.FeatureVersion, error) {
|
||||||
|
var packages []database.FeatureVersion
|
||||||
|
|
||||||
|
for _, detector := range featuresDetectors {
|
||||||
|
pkgs, err := detector.Detect(data)
|
||||||
|
if err != nil {
|
||||||
|
return []database.FeatureVersion{}, err
|
||||||
|
}
|
||||||
|
packages = append(packages, pkgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequiredFilesFeatures returns the list of files required for Detect for every
|
||||||
|
// registered FeaturesDetector, without leading /.
|
||||||
|
func GetRequiredFilesFeatures() (files []string) {
|
||||||
|
for _, detector := range featuresDetectors {
|
||||||
|
files = append(files, detector.GetRequiredFiles()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
82
worker/detectors/namespace.go
Normal file
82
worker/detectors/namespace.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2015 clair authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package detectors exposes functions to register and use container
|
||||||
|
// information extractors.
|
||||||
|
package detectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The NamespaceDetector interface defines a way to detect a Namespace from input data.
|
||||||
|
// A namespace is usually made of an Operating System name and its version.
|
||||||
|
type NamespaceDetector interface {
|
||||||
|
// Detect detects a Namespace and its version from input data.
|
||||||
|
Detect(map[string][]byte) *database.Namespace
|
||||||
|
// GetRequiredFiles returns the list of files required for Detect, without
|
||||||
|
// leading /.
|
||||||
|
GetRequiredFiles() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
namespaceDetectorsLock sync.Mutex
|
||||||
|
namespaceDetectors = make(map[string]NamespaceDetector)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterNamespaceDetector provides a way to dynamically register an implementation of a
|
||||||
|
// NamespaceDetector.
|
||||||
|
//
|
||||||
|
// If RegisterNamespaceDetector is called twice with the same name if NamespaceDetector is nil,
|
||||||
|
// or if the name is blank, it panics.
|
||||||
|
func RegisterNamespaceDetector(name string, f NamespaceDetector) {
|
||||||
|
if name == "" {
|
||||||
|
panic("Could not register a NamespaceDetector with an empty name")
|
||||||
|
}
|
||||||
|
if f == nil {
|
||||||
|
panic("Could not register a nil NamespaceDetector")
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceDetectorsLock.Lock()
|
||||||
|
defer namespaceDetectorsLock.Unlock()
|
||||||
|
|
||||||
|
if _, alreadyExists := namespaceDetectors[name]; alreadyExists {
|
||||||
|
panic(fmt.Sprintf("Detector '%s' is already registered", name))
|
||||||
|
}
|
||||||
|
namespaceDetectors[name] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectNamespace finds the OS of the layer by using every registered NamespaceDetector.
|
||||||
|
func DetectNamespace(data map[string][]byte) *database.Namespace {
|
||||||
|
for _, detector := range namespaceDetectors {
|
||||||
|
if namespace := detector.Detect(data); namespace != nil {
|
||||||
|
return namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequiredFilesNamespace returns the list of files required for DetectNamespace for every
|
||||||
|
// registered NamespaceDetector, without leading /.
|
||||||
|
func GetRequiredFilesNamespace() (files []string) {
|
||||||
|
for _, detector := range namespaceDetectors {
|
||||||
|
files = append(files, detector.GetRequiredFiles()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -12,8 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// Package os defines OSDetector for several sources.
|
package aptsources
|
||||||
package os
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -23,22 +22,25 @@ import (
|
|||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AptSourcesOSDetector implements OSDetector and detects the OS from the
|
// AptSourcesNamespaceDetector implements NamespaceDetector and detects the Namespace from the
|
||||||
// /etc/apt/sources.list file.
|
// /etc/apt/sources.list file.
|
||||||
type AptSourcesOSDetector struct{}
|
//
|
||||||
|
// This detector is necessary to determine precise Debian version when it is
|
||||||
|
// an unstable version for instance.
|
||||||
|
type AptSourcesNamespaceDetector struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
detectors.RegisterOSDetector("apt-sources", &AptSourcesOSDetector{})
|
detectors.RegisterNamespaceDetector("apt-sources", &AptSourcesNamespaceDetector{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect tries to detect OS/Version using /etc/apt/sources.list
|
func (detector *AptSourcesNamespaceDetector) Detect(data map[string][]byte) *database.Namespace {
|
||||||
// Necessary to determine precise Debian version when it is an unstable version for instance
|
|
||||||
func (detector *AptSourcesOSDetector) Detect(data map[string][]byte) (OS, version string) {
|
|
||||||
f, hasFile := data["etc/apt/sources.list"]
|
f, hasFile := data["etc/apt/sources.list"]
|
||||||
if !hasFile {
|
if !hasFile {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var OS, version string
|
||||||
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(string(f)))
|
scanner := bufio.NewScanner(strings.NewReader(string(f)))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
// Format: man sources.list | https://wiki.debian.org/SourcesList)
|
// Format: man sources.list | https://wiki.debian.org/SourcesList)
|
||||||
@ -72,10 +74,12 @@ func (detector *AptSourcesOSDetector) Detect(data map[string][]byte) (OS, versio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
if OS != "" && version != "" {
|
||||||
|
return &database.Namespace{Name: OS + ":" + version}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequiredFiles returns the list of files that are required for Detect()
|
func (detector *AptSourcesNamespaceDetector) GetRequiredFiles() []string {
|
||||||
func (detector *AptSourcesOSDetector) GetRequiredFiles() []string {
|
|
||||||
return []string{"etc/apt/sources.list"}
|
return []string{"etc/apt/sources.list"}
|
||||||
}
|
}
|
@ -12,15 +12,19 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package aptsources
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
var aptSourcesOSTests = []osTest{
|
"github.com/coreos/clair/database"
|
||||||
osTest{
|
"github.com/coreos/clair/worker/detectors/namespace"
|
||||||
expectedOS: "debian",
|
)
|
||||||
expectedVersion: "unstable",
|
|
||||||
data: map[string][]byte{
|
var aptSourcesOSTests = []namespace.NamespaceTest{
|
||||||
|
namespace.NamespaceTest{
|
||||||
|
ExpectedNamespace: database.Namespace{Name: "debian:unstable"},
|
||||||
|
Data: map[string][]byte{
|
||||||
"etc/os-release": []byte(
|
"etc/os-release": []byte(
|
||||||
`PRETTY_NAME="Debian GNU/Linux stretch/sid"
|
`PRETTY_NAME="Debian GNU/Linux stretch/sid"
|
||||||
NAME="Debian GNU/Linux"
|
NAME="Debian GNU/Linux"
|
||||||
@ -33,6 +37,6 @@ BUG_REPORT_URL="https://bugs.debian.org/"`),
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAptSourcesOSDetector(t *testing.T) {
|
func TestAptSourcesNamespaceDetector(t *testing.T) {
|
||||||
testOSDetector(t, &AptSourcesOSDetector{}, aptSourcesOSTests)
|
namespace.TestNamespaceDetector(t, &AptSourcesNamespaceDetector{}, aptSourcesOSTests)
|
||||||
}
|
}
|
@ -12,13 +12,14 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package lsbrelease
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,22 +28,24 @@ var (
|
|||||||
lsbReleaseVersionRegexp = regexp.MustCompile(`^DISTRIB_RELEASE=(.*)`)
|
lsbReleaseVersionRegexp = regexp.MustCompile(`^DISTRIB_RELEASE=(.*)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// AptSourcesOSDetector implements OSDetector and detects the OS from the
|
// AptSourcesNamespaceDetector implements NamespaceDetector and detects the Namespace from the
|
||||||
// /etc/lsb-release file.
|
// /etc/lsb-release file.
|
||||||
type LsbReleaseOSDetector struct{}
|
//
|
||||||
|
// This detector is necessary for Ubuntu Precise.
|
||||||
|
type LsbReleaseNamespaceDetector struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
detectors.RegisterOSDetector("lsb-release", &LsbReleaseOSDetector{})
|
detectors.RegisterNamespaceDetector("lsb-release", &LsbReleaseNamespaceDetector{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect tries to detect OS/Version using "/etc/lsb-release"
|
func (detector *LsbReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace {
|
||||||
// Necessary for Ubuntu Precise for instance
|
|
||||||
func (detector *LsbReleaseOSDetector) Detect(data map[string][]byte) (OS, version string) {
|
|
||||||
f, hasFile := data["etc/lsb-release"]
|
f, hasFile := data["etc/lsb-release"]
|
||||||
if !hasFile {
|
if !hasFile {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var OS, version string
|
||||||
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(string(f)))
|
scanner := bufio.NewScanner(strings.NewReader(string(f)))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
@ -66,10 +69,13 @@ func (detector *LsbReleaseOSDetector) Detect(data map[string][]byte) (OS, versio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
if OS != "" && version != "" {
|
||||||
|
return &database.Namespace{Name: OS + ":" + version}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequiredFiles returns the list of files that are required for Detect()
|
// GetRequiredFiles returns the list of files that are required for Detect()
|
||||||
func (detector *LsbReleaseOSDetector) GetRequiredFiles() []string {
|
func (detector *LsbReleaseNamespaceDetector) GetRequiredFiles() []string {
|
||||||
return []string{"etc/lsb-release"}
|
return []string{"etc/lsb-release"}
|
||||||
}
|
}
|
@ -12,15 +12,19 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package lsbrelease
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
var lsbReleaseOSTests = []osTest{
|
"github.com/coreos/clair/database"
|
||||||
osTest{
|
"github.com/coreos/clair/worker/detectors/namespace"
|
||||||
expectedOS: "ubuntu",
|
)
|
||||||
expectedVersion: "12.04",
|
|
||||||
data: map[string][]byte{
|
var lsbReleaseOSTests = []namespace.NamespaceTest{
|
||||||
|
namespace.NamespaceTest{
|
||||||
|
ExpectedNamespace: database.Namespace{Name: "ubuntu:12.04"},
|
||||||
|
Data: map[string][]byte{
|
||||||
"etc/lsb-release": []byte(
|
"etc/lsb-release": []byte(
|
||||||
`DISTRIB_ID=Ubuntu
|
`DISTRIB_ID=Ubuntu
|
||||||
DISTRIB_RELEASE=12.04
|
DISTRIB_RELEASE=12.04
|
||||||
@ -28,10 +32,9 @@ DISTRIB_CODENAME=precise
|
|||||||
DISTRIB_DESCRIPTION="Ubuntu 12.04 LTS"`),
|
DISTRIB_DESCRIPTION="Ubuntu 12.04 LTS"`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
osTest{ // We don't care about the minor version of Debian
|
namespace.NamespaceTest{ // We don't care about the minor version of Debian
|
||||||
expectedOS: "debian",
|
ExpectedNamespace: database.Namespace{Name: "debian:7"},
|
||||||
expectedVersion: "7",
|
Data: map[string][]byte{
|
||||||
data: map[string][]byte{
|
|
||||||
"etc/lsb-release": []byte(
|
"etc/lsb-release": []byte(
|
||||||
`DISTRIB_ID=Debian
|
`DISTRIB_ID=Debian
|
||||||
DISTRIB_RELEASE=7.1
|
DISTRIB_RELEASE=7.1
|
||||||
@ -41,6 +44,6 @@ DISTRIB_DESCRIPTION="Debian 7.1"`),
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLsbReleaseOSDetector(t *testing.T) {
|
func TestLsbReleaseNamespaceDetector(t *testing.T) {
|
||||||
testOSDetector(t, &LsbReleaseOSDetector{}, lsbReleaseOSTests)
|
namespace.TestNamespaceDetector(t, &LsbReleaseNamespaceDetector{}, lsbReleaseOSTests)
|
||||||
}
|
}
|
@ -12,13 +12,14 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package osrelease
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,18 +28,20 @@ var (
|
|||||||
osReleaseVersionRegexp = regexp.MustCompile(`^VERSION_ID=(.*)`)
|
osReleaseVersionRegexp = regexp.MustCompile(`^VERSION_ID=(.*)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// OsReleaseOSDetector implements OSDetector and detects the OS from the
|
// OsReleaseNamespaceDetector implements NamespaceDetector and detects the OS from the
|
||||||
// /etc/os-release and usr/lib/os-release files.
|
// /etc/os-release and usr/lib/os-release files.
|
||||||
type OsReleaseOSDetector struct{}
|
type OsReleaseNamespaceDetector struct{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
detectors.RegisterOSDetector("os-release", &OsReleaseOSDetector{})
|
detectors.RegisterNamespaceDetector("os-release", &OsReleaseNamespaceDetector{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect tries to detect OS/Version using "/etc/os-release" and "/usr/lib/os-release"
|
// Detect tries to detect OS/Version using "/etc/os-release" and "/usr/lib/os-release"
|
||||||
// Typically for Debian / Ubuntu
|
// Typically for Debian / Ubuntu
|
||||||
// /etc/debian_version can't be used, it does not make any difference between testing and unstable, it returns stretch/sid
|
// /etc/debian_version can't be used, it does not make any difference between testing and unstable, it returns stretch/sid
|
||||||
func (detector *OsReleaseOSDetector) Detect(data map[string][]byte) (OS, version string) {
|
func (detector *OsReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace {
|
||||||
|
var OS, version string
|
||||||
|
|
||||||
for _, filePath := range detector.GetRequiredFiles() {
|
for _, filePath := range detector.GetRequiredFiles() {
|
||||||
f, hasFile := data[filePath]
|
f, hasFile := data[filePath]
|
||||||
if !hasFile {
|
if !hasFile {
|
||||||
@ -61,10 +64,13 @@ func (detector *OsReleaseOSDetector) Detect(data map[string][]byte) (OS, version
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
if OS != "" && version != "" {
|
||||||
|
return &database.Namespace{Name: OS + ":" + version}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequiredFiles returns the list of files that are required for Detect()
|
// GetRequiredFiles returns the list of files that are required for Detect()
|
||||||
func (detector *OsReleaseOSDetector) GetRequiredFiles() []string {
|
func (detector *OsReleaseNamespaceDetector) GetRequiredFiles() []string {
|
||||||
return []string{"etc/os-release", "usr/lib/os-release"}
|
return []string{"etc/os-release", "usr/lib/os-release"}
|
||||||
}
|
}
|
@ -12,15 +12,19 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package osrelease
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
var osReleaseOSTests = []osTest{
|
"github.com/coreos/clair/database"
|
||||||
osTest{
|
"github.com/coreos/clair/worker/detectors/namespace"
|
||||||
expectedOS: "debian",
|
)
|
||||||
expectedVersion: "8",
|
|
||||||
data: map[string][]byte{
|
var osReleaseOSTests = []namespace.NamespaceTest{
|
||||||
|
namespace.NamespaceTest{
|
||||||
|
ExpectedNamespace: database.Namespace{Name: "debian:8"},
|
||||||
|
Data: map[string][]byte{
|
||||||
"etc/os-release": []byte(
|
"etc/os-release": []byte(
|
||||||
`PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
|
`PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
|
||||||
NAME="Debian GNU/Linux"
|
NAME="Debian GNU/Linux"
|
||||||
@ -32,10 +36,9 @@ SUPPORT_URL="http://www.debian.org/support/"
|
|||||||
BUG_REPORT_URL="https://bugs.debian.org/"`),
|
BUG_REPORT_URL="https://bugs.debian.org/"`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
osTest{
|
namespace.NamespaceTest{
|
||||||
expectedOS: "ubuntu",
|
ExpectedNamespace: database.Namespace{Name: "ubuntu:15.10"},
|
||||||
expectedVersion: "15.10",
|
Data: map[string][]byte{
|
||||||
data: map[string][]byte{
|
|
||||||
"etc/os-release": []byte(
|
"etc/os-release": []byte(
|
||||||
`NAME="Ubuntu"
|
`NAME="Ubuntu"
|
||||||
VERSION="15.10 (Wily Werewolf)"
|
VERSION="15.10 (Wily Werewolf)"
|
||||||
@ -48,10 +51,9 @@ SUPPORT_URL="http://help.ubuntu.com/"
|
|||||||
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`),
|
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
osTest{ // Doesn't have quotes around VERSION_ID
|
namespace.NamespaceTest{ // Doesn't have quotes around VERSION_ID
|
||||||
expectedOS: "fedora",
|
ExpectedNamespace: database.Namespace{Name: "fedora:20"},
|
||||||
expectedVersion: "20",
|
Data: map[string][]byte{
|
||||||
data: map[string][]byte{
|
|
||||||
"etc/os-release": []byte(
|
"etc/os-release": []byte(
|
||||||
`NAME=Fedora
|
`NAME=Fedora
|
||||||
VERSION="20 (Heisenbug)"
|
VERSION="20 (Heisenbug)"
|
||||||
@ -70,6 +72,6 @@ REDHAT_SUPPORT_PRODUCT_VERSION=20`),
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOsReleaseOSDetector(t *testing.T) {
|
func TestOsReleaseNamespaceDetector(t *testing.T) {
|
||||||
testOSDetector(t, &OsReleaseOSDetector{}, osReleaseOSTests)
|
namespace.TestNamespaceDetector(t, &OsReleaseNamespaceDetector{}, osReleaseOSTests)
|
||||||
}
|
}
|
@ -12,31 +12,32 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package redhatrelease
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var redhatReleaseRegexp = regexp.MustCompile(`(?P<os>[^\s]*) (Linux release|release) (?P<version>[\d]+)`)
|
var redhatReleaseRegexp = regexp.MustCompile(`(?P<os>[^\s]*) (Linux release|release) (?P<version>[\d]+)`)
|
||||||
|
|
||||||
// RedhatReleaseOSDetector implements OSDetector and detects the OS from the
|
// RedhatReleaseNamespaceDetector implements NamespaceDetector and detects the OS from the
|
||||||
// /etc/centos-release, /etc/redhat-release and /etc/system-release files.
|
// /etc/centos-release, /etc/redhat-release and /etc/system-release files.
|
||||||
type RedhatReleaseOSDetector struct{}
|
//
|
||||||
|
|
||||||
func init() {
|
|
||||||
detectors.RegisterOSDetector("redhat-release", &RedhatReleaseOSDetector{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect tries to detect OS/Version using "/etc/centos-release", "/etc/redhat-release" and "/etc/system-release"
|
|
||||||
// Typically for CentOS and Red-Hat like systems
|
// Typically for CentOS and Red-Hat like systems
|
||||||
// eg. CentOS release 5.11 (Final)
|
// eg. CentOS release 5.11 (Final)
|
||||||
// eg. CentOS release 6.6 (Final)
|
// eg. CentOS release 6.6 (Final)
|
||||||
// eg. CentOS Linux release 7.1.1503 (Core)
|
// eg. CentOS Linux release 7.1.1503 (Core)
|
||||||
func (detector *RedhatReleaseOSDetector) Detect(data map[string][]byte) (OS, version string) {
|
type RedhatReleaseNamespaceDetector struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
detectors.RegisterNamespaceDetector("redhat-release", &RedhatReleaseNamespaceDetector{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detector *RedhatReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace {
|
||||||
for _, filePath := range detector.GetRequiredFiles() {
|
for _, filePath := range detector.GetRequiredFiles() {
|
||||||
f, hasFile := data[filePath]
|
f, hasFile := data[filePath]
|
||||||
if !hasFile {
|
if !hasFile {
|
||||||
@ -45,15 +46,14 @@ func (detector *RedhatReleaseOSDetector) Detect(data map[string][]byte) (OS, ver
|
|||||||
|
|
||||||
r := redhatReleaseRegexp.FindStringSubmatch(string(f))
|
r := redhatReleaseRegexp.FindStringSubmatch(string(f))
|
||||||
if len(r) == 4 {
|
if len(r) == 4 {
|
||||||
OS = strings.ToLower(r[1])
|
return &database.Namespace{Name: strings.ToLower(r[1]) + ":" + r[3]}
|
||||||
version = r[3]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequiredFiles returns the list of files that are required for Detect()
|
// GetRequiredFiles returns the list of files that are required for Detect()
|
||||||
func (detector *RedhatReleaseOSDetector) GetRequiredFiles() []string {
|
func (detector *RedhatReleaseNamespaceDetector) GetRequiredFiles() []string {
|
||||||
return []string{"etc/centos-release", "etc/redhat-release", "etc/system-release"}
|
return []string{"etc/centos-release", "etc/redhat-release", "etc/system-release"}
|
||||||
}
|
}
|
@ -12,27 +12,30 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package redhatrelease
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
var redhatReleaseTests = []osTest{
|
"github.com/coreos/clair/database"
|
||||||
osTest{
|
"github.com/coreos/clair/worker/detectors/namespace"
|
||||||
expectedOS: "centos",
|
)
|
||||||
expectedVersion: "6",
|
|
||||||
data: map[string][]byte{
|
var redhatReleaseTests = []namespace.NamespaceTest{
|
||||||
|
namespace.NamespaceTest{
|
||||||
|
ExpectedNamespace: database.Namespace{Name: "centos:6"},
|
||||||
|
Data: map[string][]byte{
|
||||||
"etc/centos-release": []byte(`CentOS release 6.6 (Final)`),
|
"etc/centos-release": []byte(`CentOS release 6.6 (Final)`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
osTest{
|
namespace.NamespaceTest{
|
||||||
expectedOS: "centos",
|
ExpectedNamespace: database.Namespace{Name: "centos:7"},
|
||||||
expectedVersion: "7",
|
Data: map[string][]byte{
|
||||||
data: map[string][]byte{
|
|
||||||
"etc/system-release": []byte(`CentOS Linux release 7.1.1503 (Core)`),
|
"etc/system-release": []byte(`CentOS Linux release 7.1.1503 (Core)`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRedhatReleaseOSDetector(t *testing.T) {
|
func TestRedhatReleaseNamespaceDetector(t *testing.T) {
|
||||||
testOSDetector(t, &RedhatReleaseOSDetector{}, redhatReleaseTests)
|
namespace.TestNamespaceDetector(t, &RedhatReleaseNamespaceDetector{}, redhatReleaseTests)
|
||||||
}
|
}
|
@ -12,25 +12,23 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package os
|
package namespace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/clair/database"
|
||||||
"github.com/coreos/clair/worker/detectors"
|
"github.com/coreos/clair/worker/detectors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type osTest struct {
|
type NamespaceTest struct {
|
||||||
expectedOS string
|
Data map[string][]byte
|
||||||
expectedVersion string
|
ExpectedNamespace database.Namespace
|
||||||
data map[string][]byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOSDetector(t *testing.T, detector detectors.OSDetector, tests []osTest) {
|
func TestNamespaceDetector(t *testing.T, detector detectors.NamespaceDetector, tests []NamespaceTest) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
os, version := detector.Detect(test.data)
|
assert.Equal(t, test.ExpectedNamespace, *detector.Detect(test.Data))
|
||||||
assert.Equal(t, test.expectedOS, os)
|
|
||||||
assert.Equal(t, test.expectedVersion, version)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,81 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Package detectors exposes functions to register and use container
|
|
||||||
// information extractors.
|
|
||||||
package detectors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The OSDetector interface defines a way to detect an Operating System and
|
|
||||||
// its version from input data
|
|
||||||
type OSDetector interface {
|
|
||||||
// Detect detects an Operating System and its version from input data
|
|
||||||
Detect(map[string][]byte) (string, string)
|
|
||||||
// GetRequiredFiles returns the list of files required for Detect, without
|
|
||||||
// leading /
|
|
||||||
GetRequiredFiles() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
osDetectorsLock sync.Mutex
|
|
||||||
osDetectors = make(map[string]OSDetector)
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterOSDetector provides a way to dynamically register an implementation of a
|
|
||||||
// OSDetector.
|
|
||||||
//
|
|
||||||
// If RegisterOSDetector is called twice with the same name if OSDetector is nil,
|
|
||||||
// or if the name is blank, it panics.
|
|
||||||
func RegisterOSDetector(name string, f OSDetector) {
|
|
||||||
if name == "" {
|
|
||||||
panic("Could not register a OSDetector with an empty name")
|
|
||||||
}
|
|
||||||
if f == nil {
|
|
||||||
panic("Could not register a nil OSDetector")
|
|
||||||
}
|
|
||||||
|
|
||||||
osDetectorsLock.Lock()
|
|
||||||
defer osDetectorsLock.Unlock()
|
|
||||||
|
|
||||||
if _, alreadyExists := osDetectors[name]; alreadyExists {
|
|
||||||
panic(fmt.Sprintf("Detector '%s' is already registered", name))
|
|
||||||
}
|
|
||||||
osDetectors[name] = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectOS finds the OS of the layer by using every registered OSDetector
|
|
||||||
func DetectOS(data map[string][]byte) string {
|
|
||||||
for _, detector := range osDetectors {
|
|
||||||
OS, version := detector.Detect(data)
|
|
||||||
if OS != "" && version != "" {
|
|
||||||
return OS + ":" + version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRequiredFilesOS returns the list of files required for Detect for every
|
|
||||||
// registered OSDetector, without leading /
|
|
||||||
func GetRequiredFilesOS() (files []string) {
|
|
||||||
for _, detector := range osDetectors {
|
|
||||||
files = append(files, detector.GetRequiredFiles()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
// Copyright 2015 clair authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package detectors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/coreos/clair/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The PackagesDetector interface defines a way to detect packages from input data
|
|
||||||
type PackagesDetector interface {
|
|
||||||
// Detect detects packages from the input data
|
|
||||||
Detect(map[string][]byte) ([]*database.Package, error)
|
|
||||||
// GetRequiredFiles returns the list of files required for Detect, without
|
|
||||||
// leading /
|
|
||||||
GetRequiredFiles() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
packagesDetectorsLock sync.Mutex
|
|
||||||
packagesDetectors = make(map[string]PackagesDetector)
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterPackagesDetector makes a PackagesDetector available for DetectPackages
|
|
||||||
func RegisterPackagesDetector(name string, f PackagesDetector) {
|
|
||||||
if name == "" {
|
|
||||||
panic("Could not register a PackagesDetector with an empty name")
|
|
||||||
}
|
|
||||||
if f == nil {
|
|
||||||
panic("Could not register a nil PackagesDetector")
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesDetectorsLock.Lock()
|
|
||||||
defer packagesDetectorsLock.Unlock()
|
|
||||||
|
|
||||||
if _, alreadyExists := packagesDetectors[name]; alreadyExists {
|
|
||||||
panic(fmt.Sprintf("Detector '%s' is already registered", name))
|
|
||||||
}
|
|
||||||
packagesDetectors[name] = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectPackages detects packages using every registered PackagesDetector
|
|
||||||
func DetectPackages(data map[string][]byte) ([]*database.Package, error) {
|
|
||||||
var packages []*database.Package
|
|
||||||
|
|
||||||
for _, detector := range packagesDetectors {
|
|
||||||
pkgs, err := detector.Detect(data)
|
|
||||||
if err != nil {
|
|
||||||
return []*database.Package{}, err
|
|
||||||
}
|
|
||||||
packages = append(packages, pkgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return packages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRequiredFilesPackages returns the list of files required for Detect for every
|
|
||||||
// registered PackagesDetector, without leading /
|
|
||||||
func GetRequiredFilesPackages() (files []string) {
|
|
||||||
for _, detector := range packagesDetectors {
|
|
||||||
files = append(files, detector.GetRequiredFiles()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
338
worker/worker.go
338
worker/worker.go
@ -47,259 +47,241 @@ var (
|
|||||||
// has yet to be processed for the current layer.
|
// has yet to be processed for the current layer.
|
||||||
ErrParentUnknown = errors.New("worker: parent layer is unknown, it must be processed first")
|
ErrParentUnknown = errors.New("worker: parent layer is unknown, it must be processed first")
|
||||||
|
|
||||||
// SupportedOS is the list of operating system names that the worker supports.
|
// SupportedNamespacePrefixes is the list of namespace prefixes that the worker supports.
|
||||||
SupportedOS = []string{"debian", "ubuntu", "centos"}
|
SupportedNamespacePrefixes = []string{"debian:", "ubuntu:", "centos:"}
|
||||||
|
|
||||||
// SupportedImageFormat is the list of image formats that the worker supports.
|
// SupportedImageFormat is the list of image formats that the worker supports.
|
||||||
SupportedImageFormat = []string{"Docker", "ACI"}
|
SupportedImageFormat = []string{"Docker", "ACI"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process detects the OS of a layer, the packages it installs/removes, and
|
// Process detects the Namespace of a layer, the features it adds/removes, and
|
||||||
// then stores everything in the database.
|
// then stores everything in the database.
|
||||||
func Process(ID, parentID, path string, imageFormat string) error {
|
func Process(datastore database.Datastore, name, parentName, path, imageFormat string) error {
|
||||||
if ID == "" {
|
// Verify parameters.
|
||||||
return cerrors.NewBadRequestError("could not process a layer which does not have ID")
|
if name == "" {
|
||||||
|
return cerrors.NewBadRequestError("could not process a layer which does not have a name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return cerrors.NewBadRequestError("could not process a layer which does not have a path")
|
return cerrors.NewBadRequestError("could not process a layer which does not have a path")
|
||||||
}
|
}
|
||||||
|
|
||||||
if imageFormat == "" {
|
if imageFormat == "" {
|
||||||
return cerrors.NewBadRequestError("could not process a layer which does not have a specified format")
|
return cerrors.NewBadRequestError("could not process a layer which does not have a format")
|
||||||
} else {
|
|
||||||
isSupported := false
|
|
||||||
for _, format := range SupportedImageFormat {
|
|
||||||
if strings.EqualFold(imageFormat, format) {
|
|
||||||
isSupported = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isSupported {
|
|
||||||
return cerrors.NewBadRequestError("could not process a layer which does not have a supported format")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("layer %s: processing (Location: %s, Engine version: %d, Parent: %s, Format: %s)", ID, utils.CleanURL(path), Version, parentID, imageFormat)
|
isSupported := false
|
||||||
|
for _, format := range SupportedImageFormat {
|
||||||
|
if strings.EqualFold(imageFormat, format) {
|
||||||
|
isSupported = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isSupported {
|
||||||
|
return cerrors.NewBadRequestError("could not process a layer which does not have a supported format")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("layer %s: processing (Location: %s, Engine version: %d, Parent: %s, Format: %s)",
|
||||||
|
name, utils.CleanURL(path), Version, parentName, imageFormat)
|
||||||
|
|
||||||
// Check to see if the layer is already in the database.
|
// Check to see if the layer is already in the database.
|
||||||
layer, err := database.FindOneLayerByID(ID, []string{database.FieldLayerEngineVersion})
|
layer, err := datastore.FindLayer(name, false, false)
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
if err != nil && err != cerrors.ErrNotFound {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var parent *database.Layer
|
if err == cerrors.ErrNotFound {
|
||||||
|
// New layer case.
|
||||||
|
layer = database.Layer{Name: name, EngineVersion: Version}
|
||||||
|
|
||||||
if layer != nil {
|
// Retrieve the parent if it has one.
|
||||||
// The layer is already in the database, check if we need to update it.
|
// We need to get it with its Features in order to diff them.
|
||||||
if layer.EngineVersion >= Version {
|
if parentName != "" {
|
||||||
log.Debugf("layer %s: layer content has already been processed in the past with engine %d. Current engine is %d. skipping analysis", ID, layer.EngineVersion, Version)
|
parent, err := datastore.FindLayer(parentName, true, false)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("layer %s: layer content has been analyzed in the past with engine %d. Current engine is %d. analyzing again", ID, layer.EngineVersion, Version)
|
|
||||||
} else {
|
|
||||||
// The layer is a new one, create a base struct that we will fill.
|
|
||||||
layer = &database.Layer{ID: ID, EngineVersion: Version}
|
|
||||||
|
|
||||||
// Check to make sure that the parent's layer has already been processed.
|
|
||||||
if parentID != "" {
|
|
||||||
parent, err = database.FindOneLayerByID(parentID, []string{database.FieldLayerOS, database.FieldLayerPackages, database.FieldLayerPackages})
|
|
||||||
if err != nil && err != cerrors.ErrNotFound {
|
if err != nil && err != cerrors.ErrNotFound {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if parent == nil {
|
if err == cerrors.ErrNotFound {
|
||||||
log.Warningf("layer %s: the parent layer (%s) is unknown. it must be processed first", ID, parentID)
|
log.Warningf("layer %s: the parent layer (%s) is unknown. it must be processed first", name,
|
||||||
|
parentName)
|
||||||
return ErrParentUnknown
|
return ErrParentUnknown
|
||||||
}
|
}
|
||||||
layer.ParentNode = parent.GetNode()
|
layer.Parent = &parent
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// The layer is already in the database, check if we need to update it.
|
||||||
|
if layer.EngineVersion >= Version {
|
||||||
|
log.Debugf(`layer %s: layer content has already been processed in the past with engine %d.
|
||||||
|
Current engine is %d. skipping analysis`, name, layer.EngineVersion, Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf(`layer %s: layer content has been analyzed in the past with engine %d. Current
|
||||||
|
engine is %d. analyzing again`, name, layer.EngineVersion, Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analyze the content.
|
// Analyze the content.
|
||||||
layer.OS, layer.InstalledPackagesNodes, layer.RemovedPackagesNodes, err = detectContent(ID, path, parent, imageFormat)
|
layer.Namespace, layer.Features, err = detectContent(name, path, imageFormat, layer.Parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return database.InsertLayer(layer)
|
return datastore.InsertLayer(layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectContent downloads a layer's archive, extracts info from it and returns
|
// detectContent downloads a layer's archive and extracts its Namespace and Features.
|
||||||
// an updated Layer struct.
|
func detectContent(name, path, imageFormat string, parent *database.Layer) (namespace *database.Namespace, features []database.FeatureVersion, err error) {
|
||||||
//
|
data, err := detectors.DetectData(path, imageFormat, append(detectors.GetRequiredFilesFeatures(),
|
||||||
// If parent is not nil, database.FieldLayerOS, database.FieldLayerPackages fields must be
|
detectors.GetRequiredFilesNamespace()...), maxFileSize)
|
||||||
// has been selectioned.
|
|
||||||
func detectContent(ID, path string, parent *database.Layer, imageFormat string) (OS string, installedPackagesNodes, removedPackagesNodes []string, err error) {
|
|
||||||
data, err := getLayerData(path, imageFormat)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("layer %s: failed to extract data from %s: %s", ID, utils.CleanURL(path), err)
|
log.Errorf("layer %s: failed to extract data from %s: %s", name, utils.CleanURL(path), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
OS, err = detectOS(data, parent)
|
// Detect namespace.
|
||||||
|
namespace, err = detectNamespace(data, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if OS != "" {
|
if namespace.Name != "" {
|
||||||
log.Debugf("layer %s: OS is %s.", ID, OS)
|
log.Debugf("layer %s: Namespace is %s.", name, namespace.Name)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("layer %s: OS is unknown.", ID)
|
log.Debugf("layer %s: OS is unknown.", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
packageList, err := detectors.DetectPackages(data)
|
// Detect features.
|
||||||
|
features, err = detectFeatures(name, data, namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("layer %s: package list could not be determined: %s", ID, err)
|
log.Errorf("layer %s: package list could not be determined: %s", name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any packages, that layer modified the package list.
|
log.Debugf("layer %s: detected %d features", name, len(features))
|
||||||
if len(packageList) > 0 {
|
|
||||||
// It is possible that the OS could not be detected, in the case of a
|
|
||||||
// first layer setting MAINTAINER only for instance. However, if the OS
|
|
||||||
// is unknown and packages are detected, we have to return an error.
|
|
||||||
if OS == "" {
|
|
||||||
log.Errorf("layer %s: OS is unknown but %d packages have been detected", ID, len(packageList))
|
|
||||||
err = ErrUnsupported
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the layer has no parent, it can only add packages, not remove them.
|
|
||||||
if parent == nil {
|
|
||||||
// Build a list of the layer packages' node values.
|
|
||||||
var installedPackages []*database.Package
|
|
||||||
for _, p := range packageList {
|
|
||||||
p.OS = OS
|
|
||||||
installedPackages = append(installedPackages, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert that list into the database.
|
|
||||||
err = database.InsertPackages(installedPackages)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the InstalledPackageNodes field on content.
|
|
||||||
for _, p := range installedPackages {
|
|
||||||
if p.Node != "" {
|
|
||||||
installedPackagesNodes = append(installedPackagesNodes, p.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
installedPackagesNodes, removedPackagesNodes, err = detectAndInsertInstalledAndRemovedPackages(OS, packageList, parent)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("layer %s: detected %d packages: installs %d and removes %d packages", ID, len(packageList), len(installedPackagesNodes), len(removedPackagesNodes))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLayerData downloads/opens a layer archive and extracts it into memory.
|
func detectNamespace(data map[string][]byte, parent *database.Layer) (namespace *database.Namespace, err error) {
|
||||||
func getLayerData(path string, imageFormat string) (data map[string][]byte, err error) {
|
namespace = detectors.DetectNamespace(data)
|
||||||
data, err = detectors.DetectData(path, imageFormat, append(detectors.GetRequiredFilesPackages(), detectors.GetRequiredFilesOS()...), maxFileSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectOS(data map[string][]byte, parent *database.Layer) (detectedOS string, err error) {
|
|
||||||
detectedOS = detectors.DetectOS(data)
|
|
||||||
|
|
||||||
// Attempt to detect the OS from the parent layer.
|
// Attempt to detect the OS from the parent layer.
|
||||||
if detectedOS == "" && parent != nil {
|
if namespace == nil && parent != nil {
|
||||||
detectedOS, err = parent.OperatingSystem()
|
namespace = parent.Namespace
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the detectedOS is not in the supported OS list, the OS is unsupported.
|
// Ensure that the detected namespace's prefix is supported.
|
||||||
if detectedOS != "" {
|
if namespace != nil {
|
||||||
isSupported := false
|
isSupported := false
|
||||||
for _, osPrefix := range SupportedOS {
|
for _, namespacePrefix := range SupportedNamespacePrefixes {
|
||||||
if strings.HasPrefix(detectedOS, osPrefix) {
|
if strings.HasPrefix(namespace.Name, namespacePrefix) {
|
||||||
isSupported = true
|
isSupported = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isSupported {
|
if !isSupported {
|
||||||
return "", ErrUnsupported
|
return namespace, ErrUnsupported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectAndInsertInstalledAndRemovedPackages finds the installed and removed
|
func detectFeatures(name string, data map[string][]byte, namespace *database.Namespace) (features []database.FeatureVersion, err error) {
|
||||||
// package nodes and inserts the installed packages into the database.
|
// TODO(Quentin-M): We need to pass the parent image DetectFeatures because it's possible that
|
||||||
func detectAndInsertInstalledAndRemovedPackages(detectedOS string, packageList []*database.Package, parent *database.Layer) (installedNodes, removedNodes []string, err error) {
|
// some detectors would need it in order to produce the entire feature list (if they can only
|
||||||
// Get the parent layer's packages.
|
// detect a diff). Also, we should probably pass the detected namespace so detectors could
|
||||||
parentPackageNodes, err := parent.AllPackages()
|
// make their own decision.
|
||||||
|
features, err = detectors.DetectFeatures(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return
|
||||||
}
|
|
||||||
parentPackages, err := database.FindAllPackagesByNodes(parentPackageNodes, []string{database.FieldPackageName, database.FieldPackageVersion})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map detected packages (name:version) string to packages.
|
// Ensure that every feature has a Namespace associated, otherwise associate the detected
|
||||||
packagesNVMapToPackage := make(map[string]*database.Package)
|
// namespace. If there is no detected namespace, we'll throw an error.
|
||||||
for _, p := range packageList {
|
for i := 0; i < len(features); i++ {
|
||||||
packagesNVMapToPackage[p.Name+":"+p.Version.String()] = p
|
if features[i].Feature.Namespace.Name == "" {
|
||||||
}
|
if namespace != nil {
|
||||||
|
features[i].Feature.Namespace = *namespace
|
||||||
// Map parent's packages (name:version) string to nodes.
|
} else {
|
||||||
parentPackagesNVMapToNodes := make(map[string]string)
|
log.Errorf("layer %s: Layer's namespace is unknown but non-namespaced features have been detected", name)
|
||||||
for _, p := range parentPackages {
|
err = ErrUnsupported
|
||||||
parentPackagesNVMapToNodes[p.Name+":"+p.Version.String()] = p.Node
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a list of the parent layer's packages' node values.
|
|
||||||
var parentPackagesNV []string
|
|
||||||
for _, p := range parentPackages {
|
|
||||||
parentPackagesNV = append(parentPackagesNV, p.Name+":"+p.Version.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a list of the layer packages' node values.
|
|
||||||
var layerPackagesNV []string
|
|
||||||
for _, p := range packageList {
|
|
||||||
layerPackagesNV = append(layerPackagesNV, p.Name+":"+p.Version.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the installed and removed packages.
|
|
||||||
removedPackagesNV := utils.CompareStringLists(parentPackagesNV, layerPackagesNV)
|
|
||||||
installedPackagesNV := utils.CompareStringLists(layerPackagesNV, parentPackagesNV)
|
|
||||||
|
|
||||||
// Build a list of all the installed packages.
|
|
||||||
var installedPackages []*database.Package
|
|
||||||
for _, nv := range installedPackagesNV {
|
|
||||||
p, _ := packagesNVMapToPackage[nv]
|
|
||||||
p.OS = detectedOS
|
|
||||||
installedPackages = append(installedPackages, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert that list into the database.
|
|
||||||
err = database.InsertPackages(installedPackages)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the list of installed package nodes.
|
|
||||||
for _, p := range installedPackages {
|
|
||||||
if p.Node != "" {
|
|
||||||
installedNodes = append(installedNodes, p.Node)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the list of removed package nodes.
|
|
||||||
for _, nv := range removedPackagesNV {
|
|
||||||
node, _ := parentPackagesNVMapToNodes[nv]
|
|
||||||
removedNodes = append(removedNodes, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // detectAndInsertInstalledAndRemovedPackages finds the installed and removed
|
||||||
|
// // package nodes and inserts the installed packages into the database.
|
||||||
|
// func detectAndInsertInstalledAndRemovedPackages(detectedOS string, packageList []database.FeatureVersion, parent *database.Layer) (installedNodes, removedNodes []string, err error) {
|
||||||
|
// // Get the parent layer's packages.
|
||||||
|
// parentPackageNodes, err := parent.AllPackages()
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, nil, err
|
||||||
|
// }
|
||||||
|
// parentPackages, err := database.FindAllPackagesByNodes(parentPackageNodes, []string{database.FieldPackageName, database.FieldPackageVersion})
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Map detected packages (name:version) string to packages.
|
||||||
|
// packagesNVMapToPackage := make(map[string]*database.Package)
|
||||||
|
// for _, p := range packageList {
|
||||||
|
// packagesNVMapToPackage[p.Name+":"+p.Version.String()] = p
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Map parent's packages (name:version) string to nodes.
|
||||||
|
// parentPackagesNVMapToNodes := make(map[string]string)
|
||||||
|
// for _, p := range parentPackages {
|
||||||
|
// parentPackagesNVMapToNodes[p.Name+":"+p.Version.String()] = p.Node
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Build a list of the parent layer's packages' node values.
|
||||||
|
// var parentPackagesNV []string
|
||||||
|
// for _, p := range parentPackages {
|
||||||
|
// parentPackagesNV = append(parentPackagesNV, p.Name+":"+p.Version.String())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Build a list of the layer packages' node values.
|
||||||
|
// var layerPackagesNV []string
|
||||||
|
// for _, p := range packageList {
|
||||||
|
// layerPackagesNV = append(layerPackagesNV, p.Name+":"+p.Version.String())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Calculate the installed and removed packages.
|
||||||
|
// removedPackagesNV := utils.CompareStringLists(parentPackagesNV, layerPackagesNV)
|
||||||
|
// installedPackagesNV := utils.CompareStringLists(layerPackagesNV, parentPackagesNV)
|
||||||
|
//
|
||||||
|
// // Build a list of all the installed packages.
|
||||||
|
// var installedPackages []database.FeatureVersion
|
||||||
|
// for _, nv := range installedPackagesNV {
|
||||||
|
// p, _ := packagesNVMapToPackage[nv]
|
||||||
|
// p.OS = detectedOS
|
||||||
|
// installedPackages = append(installedPackages, p)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Insert that list into the database.
|
||||||
|
// err = database.InsertPackages(installedPackages)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Build the list of installed package nodes.
|
||||||
|
// for _, p := range installedPackages {
|
||||||
|
// if p.Node != "" {
|
||||||
|
// installedNodes = append(installedNodes, p.Node)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Build the list of removed package nodes.
|
||||||
|
// for _, nv := range removedPackagesNV {
|
||||||
|
// node, _ := parentPackagesNVMapToNodes[nv]
|
||||||
|
// removedNodes = append(removedNodes, node)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
Loading…
Reference in New Issue
Block a user